Private/Host.ps1
|
# The calling side of the engine: public cmdlets build a request, the engine runs it in a # dedicated runspace (see Private/Runner.ps1 for why) and the outcome is reported here on the # streams of the calling cmdlet. function Write-TaskSinkItem { <# .SYNOPSIS Re-emits one queued item of a -PassThru run on the streams of the calling cmdlet. #> [CmdletBinding()] param( [Parameter(Mandatory)][System.Management.Automation.PSCmdlet]$Cmdlet, [Parameter(Mandatory)][System.Collections.DictionaryEntry]$Item ) $value = $Item.Value switch ([string]$Item.Key) { 'x' { if ($value -is [System.Management.Automation.ErrorRecord]) { $Cmdlet.WriteError($value) } elseif ($value -is [System.Management.Automation.WarningRecord]) { $Cmdlet.WriteWarning($value.Message) } elseif ($value -is [System.Management.Automation.VerboseRecord]) { Write-Verbose -Message $value.Message -Verbose } elseif ($value -is [System.Management.Automation.DebugRecord]) { Write-Debug -Message $value.Message -Debug } elseif ($value -is [System.Management.Automation.InformationRecord]) { $Cmdlet.WriteInformation($value) } else { $Cmdlet.WriteObject($value, $false) } } 'o' { $Cmdlet.WriteObject($value, $false) } 'i' { $Cmdlet.WriteInformation($value) } 'v' { Write-Verbose -Message ([string]$value) -Verbose } 'e' { $Cmdlet.WriteError($value) } } } function Invoke-TaskEngine { <# .SYNOPSIS Runs an engine request in a dedicated runspace and returns its result table. .DESCRIPTION The request is a hashtable (Action, Names, Options, ...). The engine never throws for task failures; it returns @{ Ok; Data; Message; Code; ExitStatus; Exception }. #> [CmdletBinding()] [OutputType([hashtable])] param( # The calling cmdlet; only needed for -PassThru runs (it re-emits the queued output). [System.Management.Automation.PSCmdlet]$Cmdlet, [Parameter(Mandatory)][hashtable]$Request ) # Classes of two versions in one process do not work together (see PS-TaskFile.psm1). if (Test-TaskModuleChanged) { return @{ Ok = $false; Message = "task: $script:ModuleChanged"; Code = 1; ExitStatus = $null; Exception = $null } } # Assigned in a statement: an `if` expression would enumerate the (empty) collection. $sink = $null if ($Request.PassThru -and $null -ne $Cmdlet) { $sink = [System.Collections.Concurrent.BlockingCollection[object]]::new() } $Request.Sink = $sink $Request.ModuleFile = $script:ModuleFile $Request.Version = $script:ModuleVersion if (-not $Request.ContainsKey('WorkingDirectory')) { $Request.WorkingDirectory = $PWD.ProviderPath } $initialState = [initialsessionstate]::CreateDefault2() $initialState.ImportPSModule([string[]]@($script:ModuleFile)) $runspace = [runspacefactory]::CreateRunspace($Host, $initialState) $shell = $null try { $runspace.Open() $shell = [powershell]::Create() $shell.Runspace = $runspace $null = $shell.AddScript('param($request) & (Get-Module PS-TaskFile) { param($r) [TaskEngine]::Run($r) } $request').AddArgument($Request) $async = $shell.BeginInvoke() $output = $null try { $item = $null while ($true) { if ($null -ne $sink) { if ($sink.TryTake([ref] $item, 20)) { Write-TaskSinkItem -Cmdlet $Cmdlet -Item $item continue } } else { $null = $async.AsyncWaitHandle.WaitOne(20) } if ($async.IsCompleted -and ($null -eq $sink -or $sink.Count -eq 0)) { break } } $output = $shell.EndInvoke($async) } finally { # Reached with a running engine when the caller is stopped (Ctrl+C). if (-not $async.IsCompleted) { $shell.Stop() } } if ($null -ne $output -and $output.Count -gt 0) { return $output[0].psobject.BaseObject } $reason = if ($shell.Streams.Error.Count -gt 0) { $shell.Streams.Error[0].ToString() } else { 'the engine returned no result' } return @{ Ok = $false; Message = "task: internal error: $reason"; Code = 1; ExitStatus = $null; Exception = $null } } finally { if ($null -ne $shell) { $shell.Dispose() } $runspace.Dispose() if ($null -ne $sink) { $sink.Dispose() } } } function Complete-TaskResult { <# .SYNOPSIS Sets $LASTEXITCODE and throws the terminating error for a failed engine result. #> [CmdletBinding()] param( [Parameter(Mandatory)][System.Management.Automation.PSCmdlet]$Cmdlet, [Parameter(Mandatory)][hashtable]$Result, [switch]$PassExitCode ) if ($Result.Ok) { $global:LASTEXITCODE = 0 return } $code = [int]$Result.Code $exitCode = if ($PassExitCode -and $null -ne $Result.ExitStatus) { [int]$Result.ExitStatus } else { $code } $global:LASTEXITCODE = $exitCode $errorId, $category = switch ($code) { 100 { 'TaskfileNotFound', 'ObjectNotFound' } 101 { 'TaskfileAlreadyExists', 'ResourceExists' } 102 { 'TaskfileInvalid', 'InvalidData' } 107 { 'TaskfileVersionNotSupported', 'InvalidData' } 109 { 'TaskfileInvalid', 'InvalidData' } 110 { 'TaskfileIncludeCycle', 'InvalidData' } 200 { 'TaskNotFound', 'ObjectNotFound' } 201 { 'TaskRunFailed', 'InvalidResult' } 202 { 'TaskInternal', 'InvalidOperation' } 203 { 'TaskNameConflict', 'InvalidArgument' } 204 { 'TaskCalledTooManyTimes', 'InvalidOperation' } 205 { 'TaskCancelled', 'OperationStopped' } 206 { 'TaskMissingRequiredVariables', 'InvalidArgument' } 207 { 'TaskVariableNotAllowed', 'InvalidArgument' } default { 'TaskError', 'NotSpecified' } } $exception = [System.InvalidOperationException]::new([string]$Result.Message, $Result.Exception) $exception.Data['TaskCode'] = $code $exception.Data['TaskExitCode'] = $exitCode $record = [System.Management.Automation.ErrorRecord]::new($exception, $errorId, [System.Management.Automation.ErrorCategory]$category, $null) $Cmdlet.ThrowTerminatingError($record) } function New-TaskEngineRequest { <# .SYNOPSIS Builds the request table for Invoke-TaskEngine with every option defaulted. #> [CmdletBinding()] [OutputType([hashtable])] param( [Parameter(Mandatory)][string]$Action, [string[]]$Name, [string]$Taskfile, [string]$Directory, [switch]$Global, # Overrides for the engine options (Force, Silent, Dry, ...). [hashtable]$Options = @{}, # Additional request entries (PassThru, Variables, All, ...). [hashtable]$Extra = @{} ) $request = @{ Action = $Action Names = @($Name | Where-Object { -not [string]::IsNullOrEmpty($_) }) ArgumentList = @() Variables = $null Taskfile = $Taskfile Directory = $Directory Global = $Global.IsPresent WorkingDirectory = $PWD.ProviderPath PassThru = $false Options = @{ Force = $false ForceAll = $false Silent = $false Verbose = $false Dry = $false Parallel = $false Concurrency = 0 Output = '' OutputGroupBegin = '' OutputGroupEnd = '' OutputGroupErrorOnly = $false AssumeYes = $false Failfast = $false Color = [string]::IsNullOrEmpty($env:NO_COLOR) Watch = $false Interval = '' } } foreach ($key in $Options.Keys) { $request.Options[$key] = $Options[$key] } foreach ($key in $Extra.Keys) { $request[$key] = $Extra[$key] } $request } function Get-TaskCompletion { <# .SYNOPSIS Argument completer for the -Name parameter: the runnable tasks of the Taskfile. #> [CmdletBinding()] param( [AllowEmptyString()][string]$WordToComplete, [System.Collections.IDictionary]$BoundParameters ) $location = @{} foreach ($key in 'Taskfile', 'Directory', 'Global') { if ($null -ne $BoundParameters -and $BoundParameters.Contains($key)) { $location[$key] = $BoundParameters[$key] } } $prefix = $WordToComplete.Trim('"', "'") try { $request = New-TaskEngineRequest -Action List @location -Extra @{ All = $true; Sort = 'alphanumeric'; IncludeStatus = $false } $result = Invoke-TaskEngine -Request $request } catch { return } if (-not $result.Ok) { return } foreach ($task in $result.Data) { if ($task.Name -notlike "$prefix*") { continue } $text = if ($task.Name -match '[\s''"`$;|&(){}<>,#]') { "'" + $task.Name.Replace("'", "''") + "'" } else { $task.Name } $toolTip = if ([string]::IsNullOrEmpty($task.Description)) { $task.Name } else { $task.Description } [System.Management.Automation.CompletionResult]::new($text, $task.Name, 'ParameterValue', $toolTip) } } |