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
|
# (c) 2025 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
using namespace Microsoft.Win32.SafeHandles
using namespace System.Collections
using namespace System.IO
using namespace System.Management.Automation
using namespace System.Text
using namespace System.Threading
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[string]
$ResultPath,
[Parameter(Mandatory)]
[int]
$Timeout,
[Parameter(Mandatory)]
[Int64]
$WaitHandleId
)
if (-not (Test-Path -LiteralPath $ResultPath)) {
throw "async result file at '$ResultPath' does not exist"
}
$result = Get-Content -LiteralPath $ResultPath | ConvertFrom-Json | Convert-JsonObject
# The intermediate script is used so that things are set up like it normally
# is. The new Runspace is used to ensure we can stop it once the async time is
# exceeded.
$execInfo = Get-AnsibleExecWrapper -ManifestAsParam -IncludeScriptBlock
$ps = [PowerShell]::Create()
$null = $ps.AddScript(@'
[CmdletBinding()]
param([ScriptBlock]$ScriptBlock, $Param)
& $ScriptBlock.Ast.GetScriptBlock() @Param
'@).AddParameters(
@{
ScriptBlock = $execInfo.ScriptInfo.ScriptBlock
Param = $execInfo.Parameters
})
# It is important we run with the invocation settings so that it has access
# to the same PSHost. The pipeline input also needs to be marked as complete
# so the exec_wrapper isn't waiting for input indefinitely.
$pipelineInput = [PSDataCollection[object]]::new()
$pipelineInput.Complete()
$invocationSettings = [PSInvocationSettings]@{
Host = $host
}
# 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($pipelineInput, $invocationSettings, $null, $null)
$jobAsyncResult.AsyncWaitHandle.WaitOne($Timeout * 1000) > $null
$result.finished = $true
if ($jobAsyncResult.IsCompleted) {
$jobOutput = @($ps.EndInvoke($jobAsyncResult) | Out-String) -join "`n"
$jobError = $ps.Streams.Error
# write success/output/error to result object
$moduleResultJson = $jobOutput
$startJsonChar = $moduleResultJson.IndexOf([char]'{')
if ($startJsonChar -eq -1) {
throw "No start of json char found in module result"
}
$moduleResultJson = $moduleResultJson.Substring($startJsonChar)
$endJsonChar = $moduleResultJson.LastIndexOf([char]'}')
if ($endJsonChar -eq -1) {
throw "No end of json char found in module result"
}
$trailingJunk = $moduleResultJson.Substring($endJsonChar + 1).Trim()
$moduleResultJson = $moduleResultJson.Substring(0, $endJsonChar + 1)
$moduleResult = $moduleResultJson | ConvertFrom-Json | Convert-JsonObject
# TODO: check for conflicting keys
$result = $result + $moduleResult
if ($trailingJunk) {
if (-not $result.warnings) {
$result.warnings = @()
}
$result.warnings += "Module invocation had junk after the JSON data: $trailingJunk"
}
}
else {
# We can't call Stop() as pwsh won't respond if it is busy calling a .NET
# method. The process end will shut everything down instead.
$ps.BeginStop($null, $null) > $null
throw "timed out waiting for module completion"
}
}
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 = "failure during async watchdog: $_"
# return output back, if available, to Ansible to help with debugging errors
$result.stdout = $jobOutput
$result.stderr = $jobError | Out-String
}
finally {
$resultJson = ConvertTo-Json -InputObject $result -Depth 99 -Compress
Set-Content -LiteralPath $ResultPath -Value $resultJson -Encoding UTF8
}
|