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

175 lines
7.3 KiB
PowerShell

# (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"