diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 16:04:21 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 16:04:21 +0000 |
commit | 8a754e0858d922e955e71b253c139e071ecec432 (patch) | |
tree | 527d16e74bfd1840c85efd675fdecad056c54107 /lib/ansible/executor/powershell/async_wrapper.ps1 | |
parent | Initial commit. (diff) | |
download | ansible-core-upstream/2.14.3.tar.xz ansible-core-upstream/2.14.3.zip |
Adding upstream version 2.14.3.upstream/2.14.3upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | lib/ansible/executor/powershell/async_wrapper.ps1 | 174 |
1 files changed, 174 insertions, 0 deletions
diff --git a/lib/ansible/executor/powershell/async_wrapper.ps1 b/lib/ansible/executor/powershell/async_wrapper.ps1 new file mode 100644 index 0000000..0cd640f --- /dev/null +++ b/lib/ansible/executor/powershell/async_wrapper.ps1 @@ -0,0 +1,174 @@ +# (c) 2018 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +param( + [Parameter(Mandatory = $true)][System.Collections.IDictionary]$Payload +) + +$ErrorActionPreference = "Stop" + +Write-AnsibleLog "INFO - starting async_wrapper" "async_wrapper" + +if (-not $Payload.environment.ContainsKey("ANSIBLE_ASYNC_DIR")) { + Write-AnsibleError -Message "internal error: the environment variable ANSIBLE_ASYNC_DIR is not set and is required for an async task" + $host.SetShouldExit(1) + return +} +$async_dir = [System.Environment]::ExpandEnvironmentVariables($Payload.environment.ANSIBLE_ASYNC_DIR) + +# calculate the result path so we can include it in the worker payload +$jid = $Payload.async_jid +$local_jid = $jid + "." + $pid + +$results_path = [System.IO.Path]::Combine($async_dir, $local_jid) + +Write-AnsibleLog "INFO - creating async results path at '$results_path'" "async_wrapper" + +$Payload.async_results_path = $results_path +[System.IO.Directory]::CreateDirectory([System.IO.Path]::GetDirectoryName($results_path)) > $null + +# we use Win32_Process to escape the current process job, CreateProcess with a +# breakaway flag won't work for psrp as the psrp process does not have breakaway +# rights. Unfortunately we can't read/write to the spawned process as we can't +# inherit the handles. We use a locked down named pipe to send the exec_wrapper +# payload. Anonymous pipes won't work as the spawned process will not be a child +# of the current one and will not be able to inherit the handles + +# pop the async_wrapper action so we don't get stuck in a loop and create new +# exec_wrapper for our async process +$Payload.actions = $Payload.actions[1..99] +$payload_json = ConvertTo-Json -InputObject $Payload -Depth 99 -Compress + +# +$exec_wrapper = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Payload.exec_wrapper)) +$exec_wrapper += "`0`0`0`0" + $payload_json +$payload_bytes = [System.Text.Encoding]::UTF8.GetBytes($exec_wrapper) +$pipe_name = "ansible-async-$jid-$([guid]::NewGuid())" + +# template the async process command line with the payload details +$bootstrap_wrapper = { + # help with debugging errors as we loose visibility of the process output + # from here on + trap { + $wrapper_path = "$($env:TEMP)\ansible-async-wrapper-error-$(Get-Date -Format "yyyy-MM-ddTHH-mm-ss.ffffZ").txt" + $error_msg = "Error while running the async exec wrapper`r`n$($_ | Out-String)`r`n$($_.ScriptStackTrace)" + Set-Content -Path $wrapper_path -Value $error_msg + break + } + + &chcp.com 65001 > $null + + # store the pipe name and no. of bytes to read, these are populated before + # before the process is created - do not remove or changed + $pipe_name = "" + $bytes_length = 0 + + $input_bytes = New-Object -TypeName byte[] -ArgumentList $bytes_length + $pipe = New-Object -TypeName System.IO.Pipes.NamedPipeClientStream -ArgumentList @( + ".", # localhost + $pipe_name, + [System.IO.Pipes.PipeDirection]::In, + [System.IO.Pipes.PipeOptions]::None, + [System.Security.Principal.TokenImpersonationLevel]::Anonymous + ) + try { + $pipe.Connect() + $pipe.Read($input_bytes, 0, $bytes_length) > $null + } + finally { + $pipe.Close() + } + $exec = [System.Text.Encoding]::UTF8.GetString($input_bytes) + $exec_parts = $exec.Split(@("`0`0`0`0"), 2, [StringSplitOptions]::RemoveEmptyEntries) + Set-Variable -Name json_raw -Value $exec_parts[1] + $exec = [ScriptBlock]::Create($exec_parts[0]) + &$exec +} + +$bootstrap_wrapper = $bootstrap_wrapper.ToString().Replace('$pipe_name = ""', "`$pipe_name = `"$pipe_name`"") +$bootstrap_wrapper = $bootstrap_wrapper.Replace('$bytes_length = 0', "`$bytes_length = $($payload_bytes.Count)") +$encoded_command = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($bootstrap_wrapper)) +$pwsh_path = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" +$exec_args = "`"$pwsh_path`" -NonInteractive -NoProfile -ExecutionPolicy Bypass -EncodedCommand $encoded_command" + +# create a named pipe that is set to allow only the current user read access +$current_user = ([Security.Principal.WindowsIdentity]::GetCurrent()).User +$pipe_sec = New-Object -TypeName System.IO.Pipes.PipeSecurity +$pipe_ar = New-Object -TypeName System.IO.Pipes.PipeAccessRule -ArgumentList @( + $current_user, + [System.IO.Pipes.PipeAccessRights]::Read, + [System.Security.AccessControl.AccessControlType]::Allow +) +$pipe_sec.AddAccessRule($pipe_ar) + +Write-AnsibleLog "INFO - creating named pipe '$pipe_name'" "async_wrapper" +$pipe = New-Object -TypeName System.IO.Pipes.NamedPipeServerStream -ArgumentList @( + $pipe_name, + [System.IO.Pipes.PipeDirection]::Out, + 1, + [System.IO.Pipes.PipeTransmissionMode]::Byte, + [System.IO.Pipes.PipeOptions]::Asynchronous, + 0, + 0, + $pipe_sec +) + +try { + Write-AnsibleLog "INFO - creating async process '$exec_args'" "async_wrapper" + $process = Invoke-CimMethod -ClassName Win32_Process -Name Create -Arguments @{CommandLine = $exec_args } + $rc = $process.ReturnValue + + Write-AnsibleLog "INFO - return value from async process exec: $rc" "async_wrapper" + if ($rc -ne 0) { + $error_msg = switch ($rc) { + 2 { "Access denied" } + 3 { "Insufficient privilege" } + 8 { "Unknown failure" } + 9 { "Path not found" } + 21 { "Invalid parameter" } + default { "Other" } + } + throw "Failed to start async process: $rc ($error_msg)" + } + $watchdog_pid = $process.ProcessId + Write-AnsibleLog "INFO - created async process PID: $watchdog_pid" "async_wrapper" + + # populate initial results before we send the async data to avoid result race + $result = @{ + started = 1; + finished = 0; + results_file = $results_path; + ansible_job_id = $local_jid; + _ansible_suppress_tmpdir_delete = $true; + ansible_async_watchdog_pid = $watchdog_pid + } + + Write-AnsibleLog "INFO - writing initial async results to '$results_path'" "async_wrapper" + $result_json = ConvertTo-Json -InputObject $result -Depth 99 -Compress + Set-Content $results_path -Value $result_json + + $np_timeout = $Payload.async_startup_timeout * 1000 + Write-AnsibleLog "INFO - waiting for async process to connect to named pipe for $np_timeout milliseconds" "async_wrapper" + $wait_async = $pipe.BeginWaitForConnection($null, $null) + $wait_async.AsyncWaitHandle.WaitOne($np_timeout) > $null + if (-not $wait_async.IsCompleted) { + $msg = "Ansible encountered a timeout while waiting for the async task to start and connect to the named" + $msg += "pipe. This can be affected by the performance of the target - you can increase this timeout using" + $msg += "WIN_ASYNC_STARTUP_TIMEOUT or just for this host using the win_async_startup_timeout hostvar if " + $msg += "this keeps happening." + throw $msg + } + $pipe.EndWaitForConnection($wait_async) + + Write-AnsibleLog "INFO - writing exec_wrapper and payload to async process" "async_wrapper" + $pipe.Write($payload_bytes, 0, $payload_bytes.Count) + $pipe.Flush() + $pipe.WaitForPipeDrain() +} +finally { + $pipe.Close() +} + +Write-AnsibleLog "INFO - outputting initial async result: $result_json" "async_wrapper" +Write-Output -InputObject $result_json +Write-AnsibleLog "INFO - ending async_wrapper" "async_wrapper" |