Public/Steps/Invoke-HDTCommandLineStep.ps1
|
function Invoke-HDTCommandLineStep { <# .SYNOPSIS Runs a native command and classifies its exit code. .DESCRIPTION MDT's Run Command Line, rebuilt on an injected IProcessService. - name: Install the agent type: CommandLine file: setup.exe arguments: /q /norestart workingDirectory: C:\Deploy\Applications successCodes: [0, 1641] rebootCodes: [3010] timeoutMinutes: 30 or `command:` for a single shell line, which is run through %ComSpec% /c so redirection, chaining and built-ins behave the way an administrator expects. The comspec comes from the injected IEnvironmentProvider where the catalog has one, and defaults to cmd.exe otherwise - never from $env:, which a step may not read. EXIT CODES ARE CLASSIFIED AS DATA, which is the rule "native tool exit codes are checked explicitly; $LASTEXITCODE is never assumed to be zero" made into a property of the step: in rebootCodes (default 3010) -> RebootRequested in successCodes (default 0) -> Completed anything else -> Failed, naming the code REBOOTCODES WINS A TIE. An installer that reports 3010 and is also listed as successful is a real configuration, and treating it as success would drop the reboot the installer asked for. Stated here so the precedence is a decision rather than an artefact of check order. A TIMEOUT IS A FAILURE, not a success with no output. The process service kills the process, reports TimedOut and returns -1, and this step says so in the message. LOGGING IS THE STANDARD SHAPE. The native.exec record carries the file and the exit code at Info; the FULL command line is logged only at Debug, because arguments routinely carry credentials and a log that leaked them by default would be worse than no log. Captured output goes through Write-HDTLog, which places it in the step's own file as well as the master. .PARAMETER Step A flattened step from Import-HDTSequenceDocument. Its TimeoutMinutes becomes the process timeout. .PARAMETER Context A New-HDTExecutionContext context. .OUTPUTS A New-HDTStepResult. Data carries the process result. .EXAMPLE Invoke-HDTCommandLineStep -Step $step -Context $context #> [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory = $true)] [ValidateNotNull()] [object] $Step, [Parameter(Mandatory = $true)] [ValidateNotNull()] [object] $Context ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $property = $Step.Property $filePath = '' $argument = '' if ($null -ne $property -and $property.Contains('command') -and -not [string]::IsNullOrWhiteSpace([string] $property['command'])) { $comSpec = 'cmd.exe' if ($null -ne $Context.Service.Environment) { $fromEnvironment = [string] $Context.Service.Environment.GetVariable('ComSpec') if (-not [string]::IsNullOrWhiteSpace($fromEnvironment)) { $comSpec = $fromEnvironment } } $filePath = $comSpec $argument = '/c {0}' -f $property['command'] } elseif ($null -ne $property -and $property.Contains('file') -and -not [string]::IsNullOrWhiteSpace([string] $property['file'])) { $filePath = [string] $property['file'] if ($property.Contains('arguments')) { $argument = [string] $property['arguments'] } } if ([string]::IsNullOrWhiteSpace($filePath)) { $message = "step '{0}' declares neither command nor file. A CommandLine step names one or the other." -f $Step.Name Write-HDTLog -Context $Context.Log -Message $message -Severity Error -Event step.fail -Component 'CommandLine' return (New-HDTStepResult -Status Failed -Message $message) } $workingDirectory = '' if ($null -ne $property -and $property.Contains('workingDirectory')) { $workingDirectory = [string] $property['workingDirectory'] } $successCode = @(0) if ($null -ne $property -and $property.Contains('successCodes')) { $successCode = @(@($property['successCodes']) | ForEach-Object { [int] $_ }) } $rebootCode = @(3010) if ($null -ne $property -and $property.Contains('rebootCodes')) { $rebootCode = @(@($property['rebootCodes']) | ForEach-Object { [int] $_ }) } $timeoutMillisecond = 0 if ([int] $Step.TimeoutMinutes -gt 0) { $timeoutMillisecond = [int] $Step.TimeoutMinutes * 60000 } $commandLine = ('{0} {1}' -f $filePath, $argument).Trim() # DESIGN 4.4.5: the full command line is a Debug-only detail. Write-HDTLog -Context $Context.Log -Severity Debug -Event 'native.exec' -Component 'CommandLine' ` -Message ('running {0}' -f $commandLine) ` -Data ([ordered] @{ commandLine = $commandLine; workingDirectory = $workingDirectory; timeoutMs = $timeoutMillisecond }) $process = $Context.Service.GetRequired('Process', 'CommandLine') $result = $process.Start($filePath, $argument, $workingDirectory, $timeoutMillisecond) foreach ($stream in @($result.StandardOutput, $result.StandardError)) { foreach ($line in @([string] $stream -split "`r?`n")) { if ([string]::IsNullOrWhiteSpace($line)) { continue } Write-HDTLog -Context $Context.Log -Message $line -Component 'CommandLine' -Source $filePath } } if ([bool] $result.TimedOut) { $message = "'{0}' timed out after {1} minute(s) and was stopped." -f $filePath, $Step.TimeoutMinutes Write-HDTLog -Context $Context.Log -Message $message -Severity Error -Event 'native.exec' ` -Component 'CommandLine' ` -Data ([ordered] @{ file = $filePath; exitCode = [int] $result.ExitCode; timedOut = $true; durationMs = [long] $result.DurationMs }) return (New-HDTStepResult -Status Failed -ExitCode ([int] $result.ExitCode) -Message $message -Data $result) } $exitCode = [int] $result.ExitCode $data = [ordered] @{ file = $filePath exitCode = $exitCode timedOut = $false durationMs = [long] $result.DurationMs } # rebootCodes is checked FIRST, deliberately: see the description. if ($rebootCode -contains $exitCode) { $message = "'{0}' returned {1} and asked for a restart." -f $filePath, $exitCode Write-HDTLog -Context $Context.Log -Message $message -Event 'native.exec' -Component 'CommandLine' -Data $data return (New-HDTStepResult -Status RebootRequested -ExitCode $exitCode -Message $message -Data $result) } if ($successCode -contains $exitCode) { $message = "'{0}' returned {1}." -f $filePath, $exitCode Write-HDTLog -Context $Context.Log -Message $message -Event 'native.exec' -Component 'CommandLine' -Data $data return (New-HDTStepResult -Status Completed -ExitCode $exitCode -Message $message -Data $result) } $message = "'{0}' returned {1}, which is not in successCodes ({2}) or rebootCodes ({3})." -f $filePath, $exitCode, ($successCode -join ', '), ($rebootCode -join ', ') Write-HDTLog -Context $Context.Log -Message $message -Severity Error -Event 'native.exec' ` -Component 'CommandLine' -Data $data return (New-HDTStepResult -Status Failed -ExitCode $exitCode -Message $message -Data $result) } |