private/Invoke-OSDCloudWorkflowTask.ps1

function Invoke-OSDCloudWorkflowTask {
    [CmdletBinding()]
    param (
        [switch]
        $Test
    )
    #=================================================
    $Error.Clear()
    Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Start"
    $ModuleName = $($MyInvocation.MyCommand.Module.Name)
    Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] ModuleName: $ModuleName"
    $ModuleBase = $($MyInvocation.MyCommand.Module.ModuleBase)
    Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] ModuleBase: $ModuleBase"
    $ModuleVersion = $($MyInvocation.MyCommand.Module.Version)
    Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] ModuleVersion: $ModuleVersion"
    #=================================================
    # Set global variables
    [System.String]$global:Architecture = $OSDCloudDevice.ProcessorArchitecture
    [System.Boolean]$global:IsOnBattery = $OSDCloudDevice.IsOnBattery
    [System.Boolean]$global:IsVM = $OSDCloudDevice.IsVM
    [System.Boolean]$global:IsWinPE = $($env:SystemDrive -eq 'X:')
    #=================================================
    $global:OSDCloudWorkflowInvoke = $null
    $global:OSDCloudWorkflowInvoke = [ordered]@{
        Architecture          = $global:Architecture
        ComputerChassisType   = $OSDCloudDevice.ChassisType
        ComputerManufacturer  = $OSDCloudDevice.ComputerManufacturer
        ComputerModel         = $OSDCloudDevice.ComputerModel
        ComputerProduct       = $OSDCloudDevice.ComputerProduct
        ComputerSerialNumber  = $OSDCloudDevice.SerialNumber
        ComputerUUID          = (Get-WmiObject -Class Win32_ComputerSystemProduct).UUID
        DriverPackName        = $global:OSDCloudDeploy.DriverPackName
        DriverPackObject      = $global:OSDCloudDeploy.DriverPackObject
        IsOnBattery           = $global:IsOnBattery
        IsVM                  = $global:IsVM
        IsWinPE               = $global:IsWinPE
        LogsPath              = "$env:TEMP\osdcloud-logs"
        OperatingSystem       = $global:OSDCloudDeploy.OperatingSystem
        OperatingSystemObject = $global:OSDCloudDeploy.OperatingSystemObject
        TimeEnd               = $null
        TimeSpan              = $null
        TimeStart             = [datetime](Get-Date)
    }
    #=================================================
    #region OSDCloud Deployment Analytics
    $eventName = 'osdcloud_deploy'
    function Send-OSDCloudDeployEvent {
        param(
            [Parameter(Mandatory)]
            [string]$EventName,
            [Parameter(Mandatory)]
            [string]$ApiKey,
            [Parameter(Mandatory)]
            [string]$DistinctId,
            [Parameter()]
            [hashtable]$Properties
        )

        try {
            $payload = [ordered]@{
                api_key     = $ApiKey
                event       = $EventName
                properties  = $Properties + @{
                    distinct_id = $DistinctId
                }
                timestamp   = (Get-Date).ToString('o')
            }

            $body = $payload | ConvertTo-Json -Depth 4 -Compress
            Invoke-RestMethod -Method Post `
                -Uri 'https://us.i.posthog.com/capture/' `
                -Body $body `
                -ContentType 'application/json' `
                -TimeoutSec 2 `
                -ErrorAction Stop | Out-Null

            Write-Verbose "[$(Get-Date -format s)] [OSDCloud] Event sent: $EventName"
        } catch {
            Write-Verbose "[$(Get-Date -format s)] [OSDCloud] Failed to send event: $($_.Exception.Message)"
        }
    }
    # UUID
    $deviceUUID = $global:OSDCloudWorkflowInvoke.ComputerUUID
    # Convert the UUID to a hash value to protect user privacyand ensure a consistent identifier across events
    $deviceUUIDHash = [System.BitConverter]::ToString([System.Security.Cryptography.SHA256]::Create().ComputeHash([System.Text.Encoding]::UTF8.GetBytes($deviceUUID))).Replace("-", "")
    [string]$distinctId = $deviceUUIDHash
    if ([string]::IsNullOrWhiteSpace($distinctId)) {
        $distinctId = [System.Guid]::NewGuid().ToString()
    }
    # Device
    $deviceManufacturer = (Get-CimInstance -ClassName CIM_ComputerSystem -ErrorAction Stop).Manufacturer
    $deviceManufacturer = $deviceManufacturer -as [string]
    if ([string]::IsNullOrWhiteSpace($deviceManufacturer)) {
        $deviceManufacturer = 'OEM'
    } else {
        $deviceManufacturer = $deviceManufacturer.Trim()
    }
    $deviceModel = ((Get-CimInstance -ClassName CIM_ComputerSystem).Model).Trim()
    $deviceModel = $deviceModel -as [string]
    if ([string]::IsNullOrWhiteSpace($deviceModel)) {
        $deviceModel = 'OEM'
    } elseif ($deviceModel -match 'OEM|to be filled') {
        $deviceModel = 'OEM'
    }
    $deviceProduct = ((Get-CimInstance -ClassName Win32_BaseBoard).Product).Trim()
    $deviceSystemSKU = ((Get-CimInstance -ClassName CIM_ComputerSystem).SystemSKUNumber).Trim()
    $deviceVersion = ((Get-CimInstance -ClassName Win32_ComputerSystemProduct).Version).Trim()
    if ($deviceManufacturer -match 'Dell') {
        $deviceManufacturer = 'Dell'
        $deviceModelId = $deviceSystemSKU
    }
    if ($deviceManufacturer -match 'Hewlett|Packard|\bHP\b') {
        $deviceManufacturer = 'HP'
        $deviceModelId = $deviceProduct
    }
    if ($deviceManufacturer -match 'Lenovo') {
        $deviceManufacturer = 'Lenovo'
        $deviceModel = $deviceVersion
        $deviceModelId = (Get-CimInstance -ClassName Win32_ComputerSystem | Select-Object -ExpandProperty Model).SubString(0, 4)
    }
    if ($deviceManufacturer -match 'Microsoft') {
        $deviceManufacturer = 'Microsoft'
        # Surface_Book or Surface_Pro_3
        $deviceModelId = $deviceSystemSKU
        # Surface Book or Surface Pro 3
        # $deviceProduct
    }
    if ($deviceManufacturer -match 'Panasonic') { $deviceManufacturer = 'Panasonic' }
    if ($deviceManufacturer -match 'OEM|to be filled') { $deviceManufacturer = 'OEM' }
    # Win32_ComputerSystem
    $deviceSystemFamily = ((Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction Ignore).SystemFamily).Trim()
    # Win32_OperatingSystem
    # $osCaption = (Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Ignore).Caption
    # $osVersion = (Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Ignore).Version

    $computerInfo = Get-ComputerInfo -ErrorAction Ignore
    if ($env:SystemDrive -eq 'X:') {
        $deploymentPhase = 'WinPE'
        $osName = 'Microsoft WindowsPE'
    }
    else {
        $deploymentPhase = 'Windows'
        $osName = [string]$computerInfo.OsName
    }
    $eventProperties = @{
        deploymentPhase             = [string]$deploymentPhase
        deviceManufacturer          = [string]$deviceManufacturer
        deviceModel                 = [string]$deviceModel
        deviceModelId               = [string]$deviceModelId
        deviceProduct               = [string]$deviceProduct
        deviceVersion               = [string]$deviceVersion
        deviceSystemFamily          = [string]$deviceSystemFamily
        deviceSystemSKU             = [string]$deviceSystemSKU
        deviceSystemType            = [string]$computerInfo.CsPCSystemType
        biosFirmwareType            = [string]$computerInfo.BiosFirmwareType
        biosReleaseDate             = [string]$computerInfo.BiosReleaseDate
        biosSMBIOSBIOSVersion       = [string]$computerInfo.BiosSMBIOSBIOSVersion
        keyboardName                = [string](Get-CimInstance -ClassName Win32_Keyboard | Select-Object -ExpandProperty Name)
        keyboardLayout              = [string](Get-CimInstance -ClassName Win32_Keyboard | Select-Object -ExpandProperty Layout)
        winArchitecture             = [string]$env:PROCESSOR_ARCHITECTURE
        winBuildLabEx               = [string]$computerInfo.WindowsBuildLabEx
        winBuildNumber              = [string]$computerInfo.OsBuildNumber
        winCountryCode              = [string]$computerInfo.OsCountryCode
        winEditionId                = [string]$computerInfo.WindowsEditionId
        winInstallationType         = [string]$computerInfo.WindowsInstallationType
        winLanguage                 = [string]$computerInfo.OsLanguage
        winName                     = [string]$osName
        winTimeZone                 = [string]$computerInfo.TimeZone
        winVersion                  = [string]$computerInfo.OsVersion
        osdcloudModuleVersion       = [string]$ModuleVersion
        osdcloudWorkflowName        = [string]$global:OSDCloudDeploy.WorkflowName
        osdcloudWorkflowTaskName    = [string]$global:OSDCloudDeploy.WorkflowTaskName
        osdcloudDriverPackName      = [string]$global:OSDCloudDeploy.DriverPackName
        osdcloudOSName              = [string]$global:OSDCloudDeploy.OperatingSystemObject.OSName
        osdcloudOSVersion           = [string]$global:OSDCloudDeploy.OperatingSystemObject.OSVersion
        osdcloudOSActivationStatus  = [string]$global:OSDCloudDeploy.OperatingSystemObject.OSActivation
        osdcloudOSBuild             = [string]$global:OSDCloudDeploy.OperatingSystemObject.OSBuild
        osdcloudOSBuildVersion      = [string]$global:OSDCloudDeploy.OperatingSystemObject.OSBuildVersion
        osdcloudOSLanguageCode      = [string]$global:OSDCloudDeploy.OperatingSystemObject.OSLanguageCode
    }
    $postApi = 'phc_2h7nQJCo41Hc5C64B2SkcEBZOvJ6mHr5xAHZyjPl3ZK'
    Send-OSDCloudDeployEvent -EventName $eventName -ApiKey $postApi -DistinctId $distinctId -Properties $eventProperties
    #=================================================
    if ($null -ne $global:OSDCloudDeploy.WorkflowTaskObject) {
        Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)]"
        
        foreach ($step in $global:OSDCloudDeploy.WorkflowTaskObject.steps) {
            # Set the current step in the global variable
            $global:OSDCloudCurrentStep = $step
            #=================================================
            # Should we skip this step? (support both 'skip' and legacy 'disable')
            if (($step.skip -eq $true) -or ($step.disable -eq $true)) {
                Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [Skip:True] $($step.name)"
                continue
            }
            #=================================================
            # Can we test this step in full Windows OS (not WinPE)?
            if (($global:IsWinPE -ne $true) -and ($step.testinfullos -ne $true)) {
                Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [Skip:FullOS] $($step.name)"
                continue
            }
            #=================================================
            # Can we pause before this step?
            if ($step.pause -eq $true) {
                Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [Pause:True] $($step.name)"
                Pause
            }
            #=================================================
            # Command or ScriptBlock
            $command = $null
            $commandline = $null
            if ($step.command) {
                $command = $step.command

                if (($command -is [string]) -and ($command.Contains(" "))) {
                    $commandline = $command
                }
                elseif (-not (Get-Command $command -ErrorAction SilentlyContinue)) {
                    Write-Host -ForegroundColor DarkRed "[$(Get-Date -format s)] [Step command does not exist] $($step.command)"
                    continue
                }
            }
            elseif ($step.scriptblock) {
                $command = [scriptblock]::Create($step.scriptblock)
            }
            else {
                Write-Host -ForegroundColor DarkRed "[$(Get-Date -format s)] [Step does not contain a command] $($step.name)"
                continue
            }
            #=================================================
            # Arguments
            $arguments = @()
            if ($step.args) {
                # Only process if args is an array of strings, not an empty object
                if ($step.args -is [array]) {
                    [array]$arguments = @($step.args)
                    $arguments = $arguments | Where-Object { $_ -is [string] } | ForEach-Object { $_.Trim() } # Trim whitespace from arguments
                    $arguments = $arguments | Where-Object { $_ -ne "" } # Remove empty arguments
                }
            }
            #=================================================
            # Parameters
            $parameters = $null
            if ($step.parameters) {
                $parameters = $null
                $parameters = [ordered]@{}
                ($step.parameters).psobject.properties | ForEach-Object { $parameters[$_.Name] = $_.Value }
    
                if ($parameters.Count -eq 0) {
                    $parameters = $null
                }
            }

            # Execute
            if ($step.scriptblock) {
                Write-Host -ForegroundColor DarkCyan "[$(Get-Date -format s)] $($step.name) [ScriptBlock:$($step.scriptblock)]"
                if ($Test) { continue }
                & $command
            } elseif ($commandline) {
                Write-Host -ForegroundColor DarkCyan "[$(Get-Date -format s)] $($step.name)"
                if ($Test) { continue }
                # Parse the command line into a command and arguments to avoid Invoke-Expression
                $parseErrors = $null
                $tokens = [System.Management.Automation.PSParser]::Tokenize($commandline, [ref]$parseErrors)

                if ($parseErrors -and $parseErrors.Count -gt 0) {
                    Write-Error "Failed to parse command line for step '$($step.name)': $commandline"
                    continue
                }

                if (-not $tokens -or $tokens.Count -eq 0) {
                    Write-Error "Empty or invalid command line for step '$($step.name)': $commandline"
                    continue
                }

                # First token is the command; subsequent CommandArgument tokens are arguments
                $exeToken = $tokens | Where-Object { $_.Type -eq 'Command' } | Select-Object -First 1
                if (-not $exeToken) {
                    Write-Error "No executable command found in command line for step '$($step.name)': $commandline"
                    continue
                }

                $exe = $exeToken.Content
                $cmdArgs = @()
                foreach ($tok in $tokens) {
                    if ($tok -eq $exeToken) { continue }
                    if ($tok.Type -eq 'CommandArgument') {
                        $cmdArgs += $tok.Content
                    }
                }

                & $exe @cmdArgs
            } elseif ($command -and ($arguments.Count -ge 1) -and ($parameters.Count -ge 1)) {
                Write-Host -ForegroundColor DarkCyan "[$(Get-Date -format s)] $($step.name) [Arguments:$arguments]"
                ($parameters | Out-String).Trim()
                if ($Test) { continue }
                & $command @parameters @arguments
            } elseif ($command -and ($arguments.Count -ge 1)) {
                Write-Host -ForegroundColor DarkCyan "[$(Get-Date -format s)] $($step.name) [Arguments:$arguments]"
                if ($Test) { continue }
                & $command @arguments
            } elseif ($command -and ($parameters.Count -ge 1)) {
                Write-Host -ForegroundColor DarkCyan "[$(Get-Date -format s)] $($step.name)"
                ($parameters | Out-String).Trim()
                if ($Test) { continue }
                & $command @parameters
            } elseif ($command) {
                Write-Host -ForegroundColor DarkCyan "[$(Get-Date -format s)] $($step.name)"
                if ($Test) { continue }
                & $command
            } else {
                Write-Host -ForegroundColor DarkCyan "[$(Get-Date -format s)] No command to execute."
                continue
            }
        }
        # End of workflow steps
        Write-Host -ForegroundColor Green "[$(Get-Date -format s)] Workflow Task execution done."
        #=================================================
        # End the function
        $Message = "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] End"
        Write-Verbose -Message $Message; Write-Debug -Message $Message
        #=================================================
    }
}