You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ansible/lib/ansible/executor/powershell/async_wrapper.ps1

226 lines
8.9 KiB
PowerShell

# (c) 2025 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#AnsibleRequires -CSharpUtil Ansible._Async
using namespace System.Collections
using namespace System.Diagnostics
using namespace System.IO
using namespace System.IO.Pipes
using namespace System.Text
using namespace System.Threading
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[string]
$AsyncDir,
[Parameter(Mandatory)]
[string]
$AsyncJid,
[Parameter(Mandatory)]
[int]
$StartupTimeout
)
Import-CSharpUtil -Name 'Ansible._Async.cs'
$AsyncDir = [Environment]::ExpandEnvironmentVariables($AsyncDir)
if (-not [Directory]::Exists($asyncDir)) {
$null = [Directory]::CreateDirectory($asyncDir)
}
$parentProcessId = 0
$parentProcessHandle = $stdoutReader = $stderrReader = $stdinPipe = $stdoutPipe = $stderrPipe = $asyncProcess = $waitHandle = $null
try {
$utf8 = [UTF8Encoding]::new($false)
$stdinPipe = [AnonymousPipeServerStream]::new([PipeDirection]::Out, [HandleInheritability]::Inheritable)
$stdoutPipe = [AnonymousPipeServerStream]::new([PipeDirection]::In, [HandleInheritability]::Inheritable)
$stderrPipe = [AnonymousPipeServerStream]::new([PipeDirection]::In, [HandleInheritability]::Inheritable)
$stdoutReader = [StreamReader]::new($stdoutPipe, $utf8, $false)
$stderrReader = [StreamReader]::new($stderrPipe, $utf8, $false)
$clientWaitHandle = $waitHandle = [Ansible._Async.AsyncUtil]::CreateInheritableEvent()
$stdinHandle = $stdinPipe.ClientSafePipeHandle
$stdoutHandle = $stdoutPipe.ClientSafePipeHandle
$stderrHandle = $stderrPipe.ClientSafePipeHandle
$executable = if ($PSVersionTable.PSVersion -lt '6.0') {
'powershell.exe'
}
else {
'pwsh.exe'
}
$executablePath = Join-Path -Path $PSHome -ChildPath $executable
# We need to escape the job of the current process to allow the async
# process to outlive the Windows job. If the current process is not part of
# a job or job allows us to breakaway we can spawn the process directly.
# Otherwise we use WMI Win32_Process.Create to create a process as our user
# outside the job and use that as the async process parent. The winrm and
# ssh connection plugin allows breaking away from the job but psrp does not.
if (-not [Ansible._Async.AsyncUtil]::CanCreateBreakawayProcess()) {
# We hide the console window and suspend the process to avoid it running
# anything. We only need the process to be created outside the job and not
# for it to run.
$psi = New-CimInstance -ClassName Win32_ProcessStartup -ClientOnly -Property @{
CreateFlags = [uint32]4 # CREATE_SUSPENDED
ShowWindow = [uint16]0 # SW_HIDE
}
$procInfo = Invoke-CimMethod -ClassName Win32_Process -Name Create -Arguments @{
CommandLine = $executablePath
ProcessStartupInformation = $psi
}
$rc = $procInfo.ReturnValue
if ($rc -ne 0) {
$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 parent process: $rc $msg"
}
# WMI returns a UInt32, we want the signed equivalent of those bytes.
$parentProcessId = [Convert]::ToInt32(
[Convert]::ToString($procInfo.ProcessId, 16),
16)
$parentProcessHandle = [Ansible._Async.AsyncUtil]::OpenProcessAsParent($parentProcessId)
$clientWaitHandle = [Ansible._Async.AsyncUtil]::DuplicateHandleToProcess($waitHandle, $parentProcessHandle)
$stdinHandle = [Ansible._Async.AsyncUtil]::DuplicateHandleToProcess($stdinHandle, $parentProcessHandle)
$stdoutHandle = [Ansible._Async.AsyncUtil]::DuplicateHandleToProcess($stdoutHandle, $parentProcessHandle)
$stderrHandle = [Ansible._Async.AsyncUtil]::DuplicateHandleToProcess($stderrHandle, $parentProcessHandle)
$stdinPipe.DisposeLocalCopyOfClientHandle()
$stdoutPipe.DisposeLocalCopyOfClientHandle()
$stderrPipe.DisposeLocalCopyOfClientHandle()
}
$localJid = "$AsyncJid.$pid"
$resultsPath = [Path]::Combine($AsyncDir, $localJid)
$bootstrapWrapper = Get-AnsibleScript -Name bootstrap_wrapper.ps1
$execAction = Get-AnsibleExecWrapper -EncodeInputOutput
$execAction.Parameters.ActionParameters = @{
ResultPath = $resultsPath
WaitHandleId = [Int64]$clientWaitHandle.DangerousGetHandle()
}
$execWrapper = @{
name = 'exec_wrapper-async.ps1'
script = $execAction.ScriptInfo.Script
params = $execAction.Parameters
} | ConvertTo-Json -Compress -Depth 99
$asyncInput = "$execWrapper`n`0`0`0`0`n$($execAction.InputData)"
$encCommand = [Convert]::ToBase64String([Encoding]::Unicode.GetBytes($bootstrapWrapper.Script))
$asyncCommand = "`"$executablePath`" -NonInteractive -NoProfile -ExecutionPolicy Bypass -EncodedCommand $encCommand"
$asyncProcess = [Ansible._Async.AsyncUtil]::CreateAsyncProcess(
$executablePath,
$asyncCommand,
$stdinHandle,
$stdoutHandle,
$stderrHandle,
$clientWaitHandle,
$parentProcessHandle,
$stdoutReader,
$stderrReader)
# We need to write the result file before the process is started to ensure
# it can read the file.
$result = @{
started = 1
finished = 0
results_file = $resultsPath
ansible_job_id = $localJid
_ansible_suppress_tmpdir_delete = $true
ansible_async_watchdog_pid = $asyncProcess.ProcessId
}
$resultJson = ConvertTo-Json -InputObject $result -Depth 99 -Compress
[File]::WriteAllText($resultsPath, $resultJson, $utf8)
if ($parentProcessHandle) {
[Ansible._Async.AsyncUtil]::CloseHandleInProcess($stdinHandle, $parentProcessHandle)
[Ansible._Async.AsyncUtil]::CloseHandleInProcess($stdoutHandle, $parentProcessHandle)
[Ansible._Async.AsyncUtil]::CloseHandleInProcess($stderrHandle, $parentProcessHandle)
[Ansible._Async.AsyncUtil]::CloseHandleInProcess($clientWaitHandle, $parentProcessHandle)
}
else {
$stdinPipe.DisposeLocalCopyOfClientHandle()
$stdoutPipe.DisposeLocalCopyOfClientHandle()
$stderrPipe.DisposeLocalCopyOfClientHandle()
}
[Ansible._Async.AsyncUtil]::ResumeThread($asyncProcess.Thread)
# If writing to the pipe fails the process has already ended.
$procAlive = $true
$procIn = [StreamWriter]::new($stdinPipe, $utf8)
try {
$procIn.WriteLine($asyncInput)
$procIn.Flush()
$procIn.Dispose()
}
catch [IOException] {
$procAlive = $false
}
if ($procAlive) {
# Wait for the process to signal it has started the async task or if it
# has ended early/timed out.
$waitTimespan = [TimeSpan]::FromSeconds($StartupTimeout)
$handleIdx = [WaitHandle]::WaitAny(
@(
[Ansible._Async.ManagedWaitHandle]::new($waitHandle),
[Ansible._Async.ManagedWaitHandle]::new($asyncProcess.Process)
),
$waitTimespan)
if ($handleIdx -eq [WaitHandle]::WaitTimeout) {
$msg = -join @(
"Ansible encountered a timeout while waiting for the async task to start and signal it has started. "
"This can be affected by the performance of the target - you can increase this timeout using "
"WIN_ASYNC_STARTUP_TIMEOUT or just for this host using the ansible_win_async_startup_timeout hostvar "
"if this keeps happening."
)
throw $msg
}
$procAlive = $handleIdx -eq 0
}
if ($procAlive) {
$resultJson
}
else {
# If the process had ended before it signaled it was ready, we return
# back the raw output and hope it contains an error.
Remove-Item -LiteralPath $resultsPath -ErrorAction Ignore
$stdout = $asyncProcess.StdoutReader.GetAwaiter().GetResult()
$stderr = $asyncProcess.StderrReader.GetAwaiter().GetResult()
$rc = [Ansible._Async.AsyncUtil]::GetProcessExitCode($asyncProcess.Process)
$host.UI.WriteLine($stdout)
Write-PowerShellClixmlStderr -Output $stderr
$host.SetShouldExit($rc)
}
}
finally {
if ($parentProcessHandle) { $parentProcessHandle.Dispose() }
if ($parentProcessId) {
Stop-Process -Id $parentProcessId -Force -ErrorAction Ignore
}
if ($stdoutReader) { $stdoutReader.Dispose() }
if ($stderrReader) { $stderrReader.Dispose() }
if ($stdinPipe) { $stdinPipe.Dispose() }
if ($stdoutPipe) { $stdoutPipe.Dispose() }
if ($stderrPipe) { $stderrPipe.Dispose() }
if ($asyncProcess) { $asyncProcess.Dispose() }
if ($waitHandle) { $waitHandle.Dispose() }
}