mirror of https://github.com/ansible/ansible.git
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.
226 lines
8.9 KiB
PowerShell
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() }
|
|
}
|