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.
113 lines
3.5 KiB
PowerShell
113 lines
3.5 KiB
PowerShell
# (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)
|
|
$jobError = $ps.Streams.Error
|
|
|
|
# write success/output/error to result object
|
|
# TODO: cleanse leading/trailing junk
|
|
$moduleResult = $jobOutput | ConvertFrom-Json | Convert-JsonObject
|
|
# TODO: check for conflicting keys
|
|
$result = $result + $moduleResult
|
|
}
|
|
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 | Out-String
|
|
$result.stderr = $jobError | Out-String
|
|
}
|
|
finally {
|
|
$resultJson = ConvertTo-Json -InputObject $result -Depth 99 -Compress
|
|
Set-Content -LiteralPath $ResultPath -Value $resultJson -Encoding UTF8
|
|
}
|