File: async_watchdog.ps1

package info (click to toggle)
ansible-core 2.19.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 32,944 kB
  • sloc: python: 181,408; cs: 4,929; sh: 4,661; xml: 34; makefile: 21
file content (132 lines) | stat: -rw-r--r-- 4,440 bytes parent folder | download
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
}