summaryrefslogtreecommitdiffstats
path: root/lib/ansible/executor/powershell/async_wrapper.ps1
blob: 0cd640fd16fd597eac50eae9f65ea89971c9fef3 (plain)
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
# (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"