1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225
|
# (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 = $true
finished = $false
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() }
}
|