windows async - refactor async wrapper code (#84712)

Refactor the async wrapper and watchdog scripts for Windows. This
attempts to avoid WMI on connection plugins that allow breaking away
from a job like winrm and ssh as an optimisation and changes how WMI is
used so that we can get the error details on a failed process more
easily.

These changes are being made also in preparation for the WDAC
implementation that requires this new execution model where input needs
to be provided through stdin.
pull/84862/head
Jordan Borean 11 months ago committed by GitHub
parent 611d8bdde3
commit 101e2eb19a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,3 @@
minor_changes:
- >-
Windows - refactor the async implementation to better handle errors during bootstrapping and avoid WMI when possible.

@ -1,117 +1,103 @@
# (c) 2018 Ansible Project
# (c) 2025 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
)
using namespace Microsoft.Win32.SafeHandles
using namespace System.Collections
using namespace System.Text
using namespace System.Threading
# help with debugging errors as we don't have visibility of this running process
trap {
$watchdog_path = "$($env:TEMP)\ansible-async-watchdog-error-$(Get-Date -Format "yyyy-MM-ddTHH-mm-ss.ffffZ").txt"
$error_msg = "Error while running the async exec wrapper`r`n$(Format-AnsibleException -ErrorRecord $_)"
Set-Content -Path $watchdog_path -Value $error_msg
break
}
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[IDictionary]
$Payload
)
$ErrorActionPreference = "Stop"
Write-AnsibleLog "INFO - starting async_watchdog" "async_watchdog"
# pop 0th action as entrypoint
$payload.actions = $payload.actions[1..99]
$actions = $Payload.actions
$entrypoint = $payload.($actions[0])
$entrypoint = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($entrypoint))
$entrypoint = [Encoding]::UTF8.GetString([Convert]::FromBase64String($entrypoint))
$resultfile_path = $payload.async_results_path
$max_exec_time_sec = $payload.async_timeout_sec
$resultPath = $payload.async_results_path
$timeoutSec = $payload.async_timeout_sec
$waitHandleId = $payload.async_wait_handle_id
Write-AnsibleLog "INFO - deserializing existing result file args at: '$resultfile_path'" "async_watchdog"
if (-not (Test-Path -Path $resultfile_path)) {
$msg = "result file at '$resultfile_path' does not exist"
Write-AnsibleLog "ERROR - $msg" "async_watchdog"
throw $msg
if (-not (Test-Path -LiteralPath $resultPath)) {
throw "result file at '$resultPath' does not exist"
}
$result_json = Get-Content -Path $resultfile_path -Raw
Write-AnsibleLog "INFO - result file json is: $result_json" "async_watchdog"
$result = ConvertFrom-AnsibleJson -InputObject $result_json
Write-AnsibleLog "INFO - creating async runspace" "async_watchdog"
$rs = [RunspaceFactory]::CreateRunspace()
$rs.Open()
$resultJson = Get-Content -LiteralPath $resultPath -Raw
$result = ConvertFrom-AnsibleJson -InputObject $resultJson
Write-AnsibleLog "INFO - creating async PowerShell pipeline" "async_watchdog"
$ps = [PowerShell]::Create()
$ps.Runspace = $rs
# these functions are set in exec_wrapper
Write-AnsibleLog "INFO - adding global functions to PowerShell pipeline script" "async_watchdog"
$ps.AddScript($script:common_functions).AddStatement() > $null
$ps.AddScript($script:wrapper_functions).AddStatement() > $null
$function_params = @{
$functionParams = @{
Name = "common_functions"
Value = $script:common_functions
Scope = "script"
}
$ps.AddCommand("Set-Variable").AddParameters($function_params).AddStatement() > $null
$ps.AddCommand("Set-Variable").AddParameters($functionParams).AddStatement() > $null
Write-AnsibleLog "INFO - adding $($actions[0]) to PowerShell pipeline script" "async_watchdog"
$ps.AddScript($entrypoint).AddArgument($payload) > $null
Write-AnsibleLog "INFO - async job start, calling BeginInvoke()" "async_watchdog"
$job_async_result = $ps.BeginInvoke()
Write-AnsibleLog "INFO - waiting '$max_exec_time_sec' seconds for async job to complete" "async_watchdog"
$job_async_result.AsyncWaitHandle.WaitOne($max_exec_time_sec * 1000) > $null
$result.finished = 1
if ($job_async_result.IsCompleted) {
Write-AnsibleLog "INFO - async job completed, calling EndInvoke()" "async_watchdog"
$job_output = $ps.EndInvoke($job_async_result)
$job_error = $ps.Streams.Error
Write-AnsibleLog "INFO - raw module stdout:`r`n$($job_output | Out-String)" "async_watchdog"
if ($job_error) {
Write-AnsibleLog "WARN - raw module stderr:`r`n$($job_error | Out-String)" "async_watchdog"
}
# write success/output/error to result object
# TODO: cleanse leading/trailing junk
try {
Write-AnsibleLog "INFO - deserializing Ansible stdout" "async_watchdog"
$module_result = ConvertFrom-AnsibleJson -InputObject $job_output
# Signals async_wrapper that we are ready to start the job and to stop waiting
$waitHandle = [SafeWaitHandle]::new([IntPtr]$waitHandleId, $true)
$waitEvent = [ManualResetEvent]::new($false)
$waitEvent.SafeWaitHandle = $waitHandle
$null = $waitEvent.Set()
$jobOutput = $null
$jobError = $null
try {
$jobAsyncResult = $ps.BeginInvoke()
$jobAsyncResult.AsyncWaitHandle.WaitOne($timeoutSec * 1000) > $null
$result.finished = 1
if ($jobAsyncResult.IsCompleted) {
$jobOutput = $ps.EndInvoke($jobAsyncResult)
$jobError = $ps.Streams.Error
# write success/output/error to result object
# TODO: cleanse leading/trailing junk
$moduleResult = ConvertFrom-AnsibleJson -InputObject $jobOutput
# TODO: check for conflicting keys
$result = $result + $module_result
}
catch {
$result.failed = $true
$result.msg = "failed to parse module output: $($_.Exception.Message)"
# return output back to Ansible to help with debugging errors
$result.stdout = $job_output | Out-String
$result.stderr = $job_error | Out-String
$result = $result + $moduleResult
}
else {
$ps.BeginStop($null, $null) > $null # best effort stop
$result_json = ConvertTo-Json -InputObject $result -Depth 99 -Compress
Set-Content -Path $resultfile_path -Value $result_json
Write-AnsibleLog "INFO - wrote output to $resultfile_path" "async_watchdog"
throw "timed out waiting for module completion"
}
}
else {
Write-AnsibleLog "ERROR - reached timeout on async job, stopping job" "async_watchdog"
$ps.BeginStop($null, $null) > $null # best effort stop
# write timeout to result object
catch {
$exception = @(
"$_"
"$($_.InvocationInfo.PositionMessage)"
"+ CategoryInfo : $($_.CategoryInfo)"
"+ FullyQualifiedErrorId : $($_.FullyQualifiedErrorId)"
""
"ScriptStackTrace:"
"$($_.ScriptStackTrace)"
if ($_.Exception.StackTrace) {
"$($_.Exception.StackTrace)"
}
) -join ([Environment]::NewLine)
$result.exception = $exception
$result.failed = $true
$result.msg = "timed out waiting for module completion"
$result_json = ConvertTo-Json -InputObject $result -Depth 99 -Compress
Set-Content -Path $resultfile_path -Value $result_json
Write-AnsibleLog "INFO - wrote timeout to '$resultfile_path'" "async_watchdog"
$result.msg = "failure during async watchdog: $_"
# return output back, if available, to Ansible to help with debugging errors
$result.stdout = $jobOutput | Out-String
$result.stderr = $jobError | Out-String
}
finally {
$resultJson = ConvertTo-Json -InputObject $result -Depth 99 -Compress
Set-Content -LiteralPath $resultPath -Value $resultJson -Encoding UTF8
}
# in the case of a hung pipeline, this will cause the process to stay alive until it's un-hung...
#$rs.Close() | Out-Null
Write-AnsibleLog "INFO - ending async_watchdog" "async_watchdog"

@ -1,174 +1,254 @@
# (c) 2018 Ansible Project
# (c) 2025 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
#AnsibleRequires -CSharpUtil Ansible._Async
using namespace System.Collections
using namespace System.ComponentModel
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)]
[IDictionary]
$Payload
)
$ErrorActionPreference = "Stop"
Write-AnsibleLog "INFO - starting async_wrapper" "async_wrapper"
$utf8 = [UTF8Encoding]::new($false)
$newTmp = [Environment]::ExpandEnvironmentVariables($Payload.module_args["_ansible_remote_tmp"])
$asyncDef = $utf8.GetString([Convert]::FromBase64String($Payload.csharp_utils["Ansible._Async"]))
# Ansible.ModuleUtils.AddType handles this but has some extra overhead, as we
# don't need any of the extra checks we just use Add-Type manually here.
$addTypeParams = @{
TypeDefinition = $asyncDef
}
if ($PSVersionTable.PSVersion -ge '6.0') {
$addTypeParams.CompilerOptions = '/unsafe'
}
else {
$referencedAssemblies = @(
[Win32Exception].Assembly.Location
)
$addTypeParams.CompilerParameters = [CodeDom.Compiler.CompilerParameters]@{
CompilerOptions = "/unsafe"
TempFiles = [CodeDom.Compiler.TempFileCollection]::new($newTmp, $false)
}
$addTypeParams.CompilerParameters.ReferencedAssemblies.AddRange($referencedAssemblies)
}
$origLib = $env:LIB
$env:LIB = $null
Add-Type @addTypeParams 5>$null
$env:LIB = $origLib
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
$asyncDir = [Environment]::ExpandEnvironmentVariables($Payload.environment.ANSIBLE_ASYNC_DIR)
if (-not [Directory]::Exists($asyncDir)) {
$null = [Directory]::CreateDirectory($asyncDir)
}
$parentProcessId = 0
$parentProcessHandle = $stdoutReader = $stderrReader = $stdinPipe = $stdoutPipe = $stderrPipe = $asyncProcess = $waitHandle = $null
try {
$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"
}
&chcp.com 65001 > $null
# 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()
}
# 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
$localJid = "$($Payload.async_jid).$pid"
$resultsPath = [Path]::Combine($asyncDir, $localJid)
$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
}
$Payload.async_results_path = $resultsPath
$Payload.async_wait_handle_id = [Int64]$clientWaitHandle.DangerousGetHandle()
$Payload.actions = $Payload.actions[1..99]
$payloadJson = ConvertTo-Json -InputObject $Payload -Depth 99 -Compress
$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
)
# We can't use our normal bootstrap_wrapper.ps1 as it uses $input. We need
# to use [Console]::In.ReadToEnd() to ensure it respects the codepage set
# at the start of the script. As we are spawning this process with an
# explicit new console we can guarantee there is a console present.
$bootstrapWrapper = {
[Console]::InputEncoding = [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
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)"
$inData = [Console]::In.ReadToEnd()
$execWrapper, $json_raw = $inData.Split(@("`0`0`0`0"), 2, [StringSplitOptions]::RemoveEmptyEntries)
& ([ScriptBlock]::Create($execWrapper))
}
$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
$execWrapper = $utf8.GetString([Convert]::FromBase64String($Payload.exec_wrapper))
$encCommand = [Convert]::ToBase64String([Encoding]::Unicode.GetBytes($bootstrapWrapper))
$asyncCommand = "`"$executablePath`" -NonInteractive -NoProfile -ExecutionPolicy Bypass -EncodedCommand $encCommand"
$asyncInput = "$execWrapper`0`0`0`0$payloadJson"
$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 = $results_path
ansible_job_id = $local_jid
results_file = $resultsPath
ansible_job_id = $localJid
_ansible_suppress_tmpdir_delete = $true
ansible_async_watchdog_pid = $watchdog_pid
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
}
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
if ($procAlive) {
# Wait for the process to signal it has started the async task or if it
# has ended early/timed out.
$startupTimeout = [TimeSpan]::FromSeconds($Payload.async_startup_timeout)
$handleIdx = [WaitHandle]::WaitAny(
@(
[Ansible._Async.ManagedWaitHandle]::new($waitHandle),
[Ansible._Async.ManagedWaitHandle]::new($asyncProcess.Process)
),
$startupTimeout)
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
}
$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()
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 SilentlyContinue
$stdout = $asyncProcess.StdoutReader.GetAwaiter().GetResult()
$stderr = $asyncProcess.StderrReader.GetAwaiter().GetResult()
$rc = [Ansible._Async.AsyncUtil]::GetProcessExitCode($asyncProcess.Process)
$host.UI.WriteLine($stdout)
$host.UI.WriteErrorLine($stderr)
$host.SetShouldExit($rc)
}
}
finally {
$pipe.Close()
if ($parentProcessHandle) { $parentProcessHandle.Dispose() }
if ($parentProcessId) {
Stop-Process -Id $parentProcessId -Force -ErrorAction SilentlyContinue
}
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() }
}
Write-AnsibleLog "INFO - outputting initial async result: $result_json" "async_wrapper"
Write-Output -InputObject $result_json
Write-AnsibleLog "INFO - ending async_wrapper" "async_wrapper"

@ -0,0 +1,516 @@
using Microsoft.Win32.SafeHandles;
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Security;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
// Used by async_wrapper.ps1, not for general use.
namespace Ansible._Async
{
internal class NativeHelpers
{
public const int CREATE_SUSPENDED = 0x00000004;
public const int CREATE_NEW_CONSOLE = 0x00000010;
public const int CREATE_UNICODE_ENVIRONMENT = 0x00000400;
public const int EXTENDED_STARTUPINFO_PRESENT = 0x00080000;
public const int CREATE_BREAKAWAY_FROM_JOB = 0x01000000;
public const int DUPLICATE_CLOSE_SOURCE = 0x00000001;
public const int DUPLICATE_SAME_ACCESS = 0x00000002;
public const int JobObjectBasicLimitInformation = 2;
public const int JOB_OBJECT_LIMIT_BREAKAWAY_OK = 0x00000800;
public const int PROCESS_DUP_HANDLE = 0x00000040;
public const int PROCESS_CREATE_PROCESS = 0x00000080;
public const int PROC_THREAD_ATTRIBUTE_PARENT_PROCESS = 0x00020000;
public const int PROC_THREAD_ATTRIBUTE_HANDLE_LIST = 0x00020002;
public const int STARTF_USESHOWWINDOW = 0x00000001;
public const int STARTF_USESTDHANDLES = 0x00000100;
public const short SW_HIDE = 0;
[StructLayout(LayoutKind.Sequential)]
public struct JOBOBJECT_BASIC_LIMIT_INFORMATION
{
public long PerProcessUserTimeLimit;
public long PerJobUserTimeLimit;
public int LimitFlags;
public IntPtr MinimumWorkingSetSize;
public IntPtr MaximumWorkingSetSize;
public int ActiveProcessLimit;
public UIntPtr Affinity;
public int PriorityClass;
public int SchedulingClass;
}
[StructLayout(LayoutKind.Sequential)]
public struct SECURITY_ATTRIBUTES
{
public int nLength;
public IntPtr lpSecurityDescriptor;
public int bInheritHandle;
}
[StructLayout(LayoutKind.Sequential)]
public struct STARTUPINFO
{
public int cb;
public IntPtr lpReserved;
public IntPtr lpDesktop;
public IntPtr lpTitle;
public int dwX;
public int dwY;
public int dwXSize;
public int dwYSize;
public int dwXCountChars;
public int dwYCountChars;
public int dwFillAttribute;
public int dwFlags;
public short wShowWindow;
public short cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
[StructLayout(LayoutKind.Sequential)]
public struct STARTUPINFOEX
{
public STARTUPINFO startupInfo;
public IntPtr lpAttributeList;
}
[StructLayout(LayoutKind.Sequential)]
public struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public int dwProcessId;
public int dwThreadId;
}
}
internal class NativeMethods
{
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr CreateEventW(
ref NativeHelpers.SECURITY_ATTRIBUTES lpEventAttributes,
bool bManualReset,
bool bInitialState,
IntPtr lpName);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool CreateProcessW(
[MarshalAs(UnmanagedType.LPWStr)] string lpApplicationName,
StringBuilder lpCommandLine,
IntPtr lpProcessAttributes,
IntPtr lpThreadAttributes,
bool bInheritHandles,
int dwCreationFlags,
IntPtr lpEnvironment,
IntPtr lpCurrentDirectory,
ref NativeHelpers.STARTUPINFOEX lpStartupInfo,
out NativeHelpers.PROCESS_INFORMATION lpProcessInformation);
[DllImport("kernel32.dll")]
public static extern void DeleteProcThreadAttributeList(
IntPtr lpAttributeList);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool DuplicateHandle(
IntPtr hSourceProcessHandle,
IntPtr hSourceHandle,
IntPtr hTargetProcessHandle,
out IntPtr lpTargetHandle,
int dwDesiredAccess,
bool bInheritHandle,
int dwOptions);
[DllImport("kernel32.dll")]
public static extern IntPtr GetCurrentProcess();
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool GetExitCodeProcess(
IntPtr hProcess,
out int lpExitCode);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool InitializeProcThreadAttributeList(
IntPtr lpAttributeList,
int dwAttributeCount,
int dwFlags,
ref IntPtr lpSize);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool IsProcessInJob(
IntPtr ProcessHandle,
IntPtr JobHandle,
out bool Result);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr OpenProcess(
Int32 dwDesiredAccess,
bool bInheritHandle,
Int32 dwProcessId);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool QueryInformationJobObject(
IntPtr hJob,
int JobObjectInformationClass,
ref NativeHelpers.JOBOBJECT_BASIC_LIMIT_INFORMATION lpJobObjectInformation,
int cbJobObjectInformationLength,
IntPtr lpReturnLength);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern int ResumeThread(
IntPtr hThread);
[DllImport("kernel32.dll", SetLastError = true)]
public static unsafe extern bool UpdateProcThreadAttribute(
SafeProcThreadAttrList lpAttributeList,
int dwFlags,
UIntPtr Attribute,
void* lpValue,
UIntPtr cbSize,
IntPtr lpPreviousValue,
IntPtr lpReturnSize);
}
public class ProcessInformation : IDisposable
{
public SafeWaitHandle Process { get; private set; }
public SafeWaitHandle Thread { get; private set; }
public int ProcessId { get; private set; }
public int ThreadId { get; private set; }
public Task<string> StdoutReader { get; private set; }
public Task<string> StderrReader { get; private set; }
public ProcessInformation(
SafeWaitHandle process,
SafeWaitHandle thread,
int processId,
int threadId,
Task<string> stdoutReader,
Task<string> stderrReader)
{
Process = process;
Thread = thread;
ProcessId = processId;
ThreadId = threadId;
StdoutReader = stdoutReader;
StderrReader = stderrReader;
}
public void Dispose()
{
Process.Dispose();
Thread.Dispose();
GC.SuppressFinalize(this);
}
~ProcessInformation() { Dispose(); }
}
public class Win32Exception : System.ComponentModel.Win32Exception
{
private string _msg;
public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
public Win32Exception(int errorCode, string message) : base(errorCode)
{
_msg = String.Format("{0} ({1}, Win32ErrorCode {2} - 0x{2:X8})", message, base.Message, errorCode);
}
public override string Message { get { return _msg; } }
public static explicit operator Win32Exception(string message) { return new Win32Exception(message); }
}
public class ManagedWaitHandle : WaitHandle
{
public ManagedWaitHandle(SafeWaitHandle handle)
{
SafeWaitHandle = handle;
}
}
internal sealed class SafeProcThreadAttrList : SafeHandle
{
public SafeProcThreadAttrList(IntPtr handle) : base(handle, true) { }
public override bool IsInvalid { get { return handle == IntPtr.Zero; } }
protected override bool ReleaseHandle()
{
NativeMethods.DeleteProcThreadAttributeList(handle);
Marshal.FreeHGlobal(handle);
return true;
}
}
public class AsyncUtil
{
public static bool CanCreateBreakawayProcess()
{
bool isInJob;
if (!NativeMethods.IsProcessInJob(NativeMethods.GetCurrentProcess(), IntPtr.Zero, out isInJob))
{
throw new Win32Exception("IsProcessInJob() failed");
}
if (!isInJob)
{
return true;
}
NativeHelpers.JOBOBJECT_BASIC_LIMIT_INFORMATION jobInfo = new NativeHelpers.JOBOBJECT_BASIC_LIMIT_INFORMATION();
bool jobRes = NativeMethods.QueryInformationJobObject(
IntPtr.Zero,
NativeHelpers.JobObjectBasicLimitInformation,
ref jobInfo,
Marshal.SizeOf<NativeHelpers.JOBOBJECT_BASIC_LIMIT_INFORMATION>(),
IntPtr.Zero);
if (!jobRes)
{
throw new Win32Exception("QueryInformationJobObject() failed");
}
return (jobInfo.LimitFlags & NativeHelpers.JOB_OBJECT_LIMIT_BREAKAWAY_OK) != 0;
}
public static ProcessInformation CreateAsyncProcess(
string applicationName,
string commandLine,
SafeHandle stdin,
SafeHandle stdout,
SafeHandle stderr,
SafeHandle mutexHandle,
SafeHandle parentProcess,
StreamReader stdoutReader,
StreamReader stderrReader)
{
StringBuilder commandLineBuffer = new StringBuilder(commandLine);
int creationFlags = NativeHelpers.CREATE_NEW_CONSOLE |
NativeHelpers.CREATE_SUSPENDED |
NativeHelpers.CREATE_UNICODE_ENVIRONMENT |
NativeHelpers.EXTENDED_STARTUPINFO_PRESENT;
if (parentProcess == null)
{
creationFlags |= NativeHelpers.CREATE_BREAKAWAY_FROM_JOB;
}
NativeHelpers.STARTUPINFOEX si = new NativeHelpers.STARTUPINFOEX();
si.startupInfo.cb = Marshal.SizeOf(typeof(NativeHelpers.STARTUPINFOEX));
si.startupInfo.dwFlags = NativeHelpers.STARTF_USESHOWWINDOW | NativeHelpers.STARTF_USESTDHANDLES;
si.startupInfo.wShowWindow = NativeHelpers.SW_HIDE;
si.startupInfo.hStdInput = stdin.DangerousGetHandle();
si.startupInfo.hStdOutput = stdout.DangerousGetHandle();
si.startupInfo.hStdError = stderr.DangerousGetHandle();
int attrCount = 1;
IntPtr rawParentProcessHandle = IntPtr.Zero;
if (parentProcess != null)
{
attrCount++;
rawParentProcessHandle = parentProcess.DangerousGetHandle();
}
using (SafeProcThreadAttrList attrList = CreateProcThreadAttribute(attrCount))
{
si.lpAttributeList = attrList.DangerousGetHandle();
IntPtr[] handlesToInherit = new IntPtr[4]
{
stdin.DangerousGetHandle(),
stdout.DangerousGetHandle(),
stderr.DangerousGetHandle(),
mutexHandle.DangerousGetHandle()
};
unsafe
{
fixed (IntPtr* handlesToInheritPtr = &handlesToInherit[0])
{
UpdateProcThreadAttribute(
attrList,
NativeHelpers.PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
handlesToInheritPtr,
IntPtr.Size * 4);
if (rawParentProcessHandle != IntPtr.Zero)
{
UpdateProcThreadAttribute(
attrList,
NativeHelpers.PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
&rawParentProcessHandle,
IntPtr.Size);
}
NativeHelpers.PROCESS_INFORMATION pi = new NativeHelpers.PROCESS_INFORMATION();
bool res = NativeMethods.CreateProcessW(
applicationName,
commandLineBuffer,
IntPtr.Zero,
IntPtr.Zero,
true,
(int)creationFlags,
IntPtr.Zero,
IntPtr.Zero,
ref si,
out pi);
if (!res)
{
throw new Win32Exception("CreateProcessW() failed");
}
return new ProcessInformation(
new SafeWaitHandle(pi.hProcess, true),
new SafeWaitHandle(pi.hThread, true),
pi.dwProcessId,
pi.dwThreadId,
Task.Run(() => stdoutReader.ReadToEnd()),
Task.Run(() => stderrReader.ReadToEnd()));
}
}
}
}
public static SafeWaitHandle CreateInheritableEvent()
{
NativeHelpers.SECURITY_ATTRIBUTES sa = new NativeHelpers.SECURITY_ATTRIBUTES();
sa.nLength = Marshal.SizeOf(sa);
sa.bInheritHandle = 1;
IntPtr hEvent = NativeMethods.CreateEventW(ref sa, true, false, IntPtr.Zero);
if (hEvent == IntPtr.Zero)
{
throw new Win32Exception("CreateEventW() failed");
}
return new SafeWaitHandle(hEvent, true);
}
public static SafeHandle DuplicateHandleToProcess(
SafeHandle handle,
SafeHandle targetProcess)
{
IntPtr targetHandle;
bool res = NativeMethods.DuplicateHandle(
NativeMethods.GetCurrentProcess(),
handle.DangerousGetHandle(),
targetProcess.DangerousGetHandle(),
out targetHandle,
0,
true,
NativeHelpers.DUPLICATE_SAME_ACCESS);
if (!res)
{
throw new Win32Exception("DuplicateHandle() failed");
}
// This will not dispose the handle, it is assumed
// the caller will close it manually with CloseHandleInProcess.
return new SafeWaitHandle(targetHandle, false);
}
public static void CloseHandleInProcess(
SafeHandle handle,
SafeHandle targetProcess)
{
IntPtr _ = IntPtr.Zero;
bool res = NativeMethods.DuplicateHandle(
targetProcess.DangerousGetHandle(),
handle.DangerousGetHandle(),
IntPtr.Zero,
out _,
0,
false,
NativeHelpers.DUPLICATE_CLOSE_SOURCE);
if (!res)
{
throw new Win32Exception("DuplicateHandle() failed to close handle");
}
}
public static int GetProcessExitCode(SafeHandle process)
{
int exitCode;
bool res = NativeMethods.GetExitCodeProcess(process.DangerousGetHandle(), out exitCode);
if (!res)
{
throw new Win32Exception("GetExitCodeProcess() failed");
}
return exitCode;
}
public static SafeHandle OpenProcessAsParent(int processId)
{
IntPtr hProcess = NativeMethods.OpenProcess(
NativeHelpers.PROCESS_DUP_HANDLE | NativeHelpers.PROCESS_CREATE_PROCESS,
false,
processId);
if (hProcess == IntPtr.Zero)
{
throw new Win32Exception("OpenProcess() failed");
}
return new SafeWaitHandle(hProcess, true);
}
public static void ResumeThread(SafeHandle thread)
{
int res = NativeMethods.ResumeThread(thread.DangerousGetHandle());
if (res == -1)
{
throw new Win32Exception("ResumeThread() failed");
}
}
private static SafeProcThreadAttrList CreateProcThreadAttribute(int count)
{
IntPtr attrSize = IntPtr.Zero;
NativeMethods.InitializeProcThreadAttributeList(IntPtr.Zero, count, 0, ref attrSize);
IntPtr attributeList = Marshal.AllocHGlobal((int)attrSize);
try
{
if (!NativeMethods.InitializeProcThreadAttributeList(attributeList, count, 0, ref attrSize))
{
throw new Win32Exception("InitializeProcThreadAttributeList() failed");
}
return new SafeProcThreadAttrList(attributeList);
}
catch
{
Marshal.FreeHGlobal(attributeList);
throw;
}
}
private static unsafe void UpdateProcThreadAttribute(
SafeProcThreadAttrList attributeList,
int attribute,
void* value,
int size)
{
bool res = NativeMethods.UpdateProcThreadAttribute(
attributeList,
0,
(UIntPtr)attribute,
value,
(UIntPtr)size,
IntPtr.Zero,
IntPtr.Zero);
if (!res)
{
string msg = string.Format("UpdateProcThreadAttribute() failed to set attribute 0x{0:X8}", attribute);
throw new Win32Exception(msg);
}
}
}
}

@ -1,7 +1,5 @@
.github/ISSUE_TEMPLATE/internal_issue.md pymarkdown!skip
lib/ansible/config/base.yml no-unwanted-files
lib/ansible/executor/powershell/async_watchdog.ps1 pslint:PSCustomUseLiteralPath
lib/ansible/executor/powershell/async_wrapper.ps1 pslint:PSCustomUseLiteralPath
lib/ansible/executor/powershell/exec_wrapper.ps1 pslint:PSCustomUseLiteralPath
lib/ansible/keyword_desc.yml no-unwanted-files
lib/ansible/modules/apt.py validate-modules:parameter-invalid

Loading…
Cancel
Save