Files
awoooi/scripts/reboot-recovery/tests/agent99-bounded-process-replay.ps1
Your Name 3a81d9bedc
Some checks failed
CD Pipeline / select-latest-carrier (push) Successful in 2m2s
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 6m29s
CD Pipeline / revalidate-deploy-carrier (push) Successful in 29s
CD Pipeline / build-and-deploy (push) Failing after 42s
CD Pipeline / revalidate-post-deploy-carrier (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped
fix(agent99): harden control loop recovery
2026-07-19 13:32:44 +08:00

346 lines
20 KiB
PowerShell

[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ControlPlaneSource
)
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
function Get-ExactFunctionSource {
param([object]$Ast, [string]$Name)
$matches = @($Ast.FindAll({
param($node)
$node -is [Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq $Name
}, $true))
if ($matches.Count -ne 1) { throw "function_identity_mismatch:$Name" }
[string]$matches[0].Extent.Text
}
function Assert-Replay {
param([bool]$Condition, [string]$Reason)
if (-not $Condition) { throw $Reason }
}
$tokens = $null
$errors = $null
$ast = [Management.Automation.Language.Parser]::ParseInput(
$ControlPlaneSource,
[ref]$tokens,
[ref]$errors
)
if ($errors.Count -ne 0) { throw "control_plane_parse_failed" }
foreach ($name in @(
"Get-AgentProcessDescendants",
"ConvertTo-AgentNativeProcessArgument",
"ConvertTo-AgentProcessIdentitySnapshot",
"Get-AgentCreationBoundedDescendants",
"Test-AgentProcessSnapshotMatch",
"Select-AgentProcessTreeIdentitySnapshot",
"Get-AgentProcessTreeIdentitySnapshot",
"Get-AgentProcessIdentityReadback",
"Test-AgentProcessIdentityActive",
"Wait-AgentProcessTreeIdentityAbsent",
"Stop-AgentBoundedProcessSnapshot",
"Read-AgentBoundedTextFile",
"Invoke-AgentBoundedProcess"
)) {
. ([ScriptBlock]::Create((Get-ExactFunctionSource $ast $name)))
}
$root = Join-Path $env:TEMP ("agent99-bounded-process-replay-" + [guid]::NewGuid().ToString("N"))
$normal = $null
$timeout = $null
$flood = $null
$rootOnly = $null
$rootOnlyChildIdentity = $null
$orphanRace = $null
$orphanRaceChildIdentity = $null
$pidReuseSelection = $null
$creationBoundSelection = $null
$stopReadbackFailure = $null
$stopReadbackFailureChildIdentity = $null
$cimFailure = $null
$cimFailureChildIdentity = $null
$cleanupVerified = $false
try {
New-Item -ItemType Directory -Path $root -Force | Out-Null
$powershell = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe"
$cmd = Join-Path $env:SystemRoot "System32\cmd.exe"
$normal = Invoke-AgentBoundedProcess `
-FilePath $cmd `
-Arguments @("/d", "/c", "echo", "AG99_BOUNDED_OK") `
-TimeoutSeconds 5 `
-MaximumOutputBytes 4096
Assert-Replay ($normal.ok -and $normal.completed -and $normal.stdout -match "AG99_BOUNDED_OK") "normal_process_failed"
$childPath = Join-Path $root "child.ps1"
$parentPath = Join-Path $root "parent.ps1"
[IO.File]::WriteAllText($childPath, 'Start-Sleep -Seconds 30', (New-Object Text.UTF8Encoding($false)))
[IO.File]::WriteAllText($parentPath, @'
param([string]$ChildPath)
$powershell = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe"
$arguments = "-NoProfile -NonInteractive -File `"$ChildPath`""
Start-Process -FilePath $powershell -ArgumentList $arguments -WindowStyle Hidden | Out-Null
Start-Sleep -Seconds 30
'@, (New-Object Text.UTF8Encoding($false)))
$timeout = Invoke-AgentBoundedProcess `
-FilePath $powershell `
-Arguments @("-NoProfile", "-NonInteractive", "-File", $parentPath, "-ChildPath", $childPath) `
-TimeoutSeconds 2 `
-MaximumOutputBytes 4096
Assert-Replay ($timeout.timedOut -and -not $timeout.completed) "timeout_not_detected"
Assert-Replay ($timeout.processTreeStopped -and -not $timeout.treeReadbackFailed) "timeout_tree_not_stopped"
Assert-Replay ([int]$timeout.killedIdentityCount -ge 2) "timeout_descendant_not_observed"
$floodPath = Join-Path $root "flood.ps1"
[IO.File]::WriteAllText($floodPath, @'
for ($index = 0; $index -lt 1000; $index++) {
[Console]::Out.Write(('X' * 1024))
}
Start-Sleep -Seconds 5
'@, (New-Object Text.UTF8Encoding($false)))
$flood = Invoke-AgentBoundedProcess `
-FilePath $powershell `
-Arguments @("-NoProfile", "-NonInteractive", "-File", $floodPath) `
-TimeoutSeconds 10 `
-MaximumOutputBytes 4096
Assert-Replay ($flood.outputLimitExceeded -and -not $flood.completed) "streaming_output_limit_not_detected"
Assert-Replay ([long]$flood.capturedBytes -le [long]$flood.maximumOutputBytes) "captured_output_exceeded_contract"
Assert-Replay ($flood.processTreeStopped -and -not $flood.treeReadbackFailed) "flood_tree_not_stopped"
$rootOnlyChildPidPath = Join-Path $root "root-only-child.pid"
$rootOnlyChildPath = Join-Path $root "root-only-child.ps1"
$rootOnlyParentPath = Join-Path $root "root-only-parent.ps1"
[IO.File]::WriteAllText($rootOnlyChildPath, @'
param([string]$PidPath)
[IO.File]::WriteAllText($PidPath, [string]$PID, (New-Object Text.UTF8Encoding($false)))
Start-Sleep -Seconds 30
'@, (New-Object Text.UTF8Encoding($false)))
[IO.File]::WriteAllText($rootOnlyParentPath, @'
param([string]$ChildPath, [string]$ChildPidPath)
$powershell = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe"
$arguments = "-NoProfile -NonInteractive -File `"$ChildPath`" -PidPath `"$ChildPidPath`""
Start-Process -FilePath $powershell -ArgumentList $arguments -WindowStyle Hidden | Out-Null
Start-Sleep -Seconds 30
'@, (New-Object Text.UTF8Encoding($false)))
$rootOnly = Invoke-AgentBoundedProcess `
-FilePath $powershell `
-Arguments @("-NoProfile", "-NonInteractive", "-File", $rootOnlyParentPath, "-ChildPath", $rootOnlyChildPath, "-ChildPidPath", $rootOnlyChildPidPath) `
-TimeoutSeconds 2 `
-MaximumOutputBytes 4096 `
-TerminationScope Root
Assert-Replay ($rootOnly.timedOut -and $rootOnly.forcedTerminationPerformed) "root_only_timeout_not_detected"
Assert-Replay ($rootOnly.processScopeStopped -and -not $rootOnly.processTreeStopped) "root_only_scope_not_preserved"
Assert-Replay ([int]$rootOnly.preservedDescendantCount -ge 1) "root_only_descendant_not_preserved"
Assert-Replay (Test-Path -LiteralPath $rootOnlyChildPidPath -PathType Leaf) "root_only_child_pid_missing"
$rootOnlyChildPid = [int]([IO.File]::ReadAllText($rootOnlyChildPidPath))
$rootOnlyChildProcess = Get-CimInstance Win32_Process -Filter "ProcessId = $rootOnlyChildPid" -ErrorAction Stop
$rootOnlyChildIdentity = [pscustomobject]@{
processId = [int]$rootOnlyChildProcess.ProcessId
parentProcessId = [int]$rootOnlyChildProcess.ParentProcessId
creationUtc = ([datetime]$rootOnlyChildProcess.CreationDate).ToUniversalTime().ToString("o")
name = [string]$rootOnlyChildProcess.Name
}
Assert-Replay (Test-AgentProcessIdentityActive $rootOnlyChildIdentity) "root_only_child_not_active_after_parent_stop"
$orphanRaceChildPidPath = Join-Path $root "orphan-race-child.pid"
$orphanRaceParentPath = Join-Path $root "orphan-race-parent.ps1"
[IO.File]::WriteAllText($orphanRaceParentPath, @'
param([string]$ChildPath, [string]$ChildPidPath)
$powershell = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe"
$arguments = "-NoProfile -NonInteractive -File `"$ChildPath`" -PidPath `"$ChildPidPath`""
Start-Process -FilePath $powershell -ArgumentList $arguments -WindowStyle Hidden | Out-Null
$stopwatch = [Diagnostics.Stopwatch]::StartNew()
while (-not (Test-Path -LiteralPath $ChildPidPath) -and $stopwatch.Elapsed.TotalSeconds -lt 5) {
Start-Sleep -Milliseconds 50
}
Start-Sleep -Seconds 1
'@, (New-Object Text.UTF8Encoding($false)))
$orphanStartInfo = New-Object Diagnostics.ProcessStartInfo
$orphanStartInfo.FileName = $powershell
$orphanStartInfo.Arguments = "-NoProfile -NonInteractive -File `"$orphanRaceParentPath`" -ChildPath `"$rootOnlyChildPath`" -ChildPidPath `"$orphanRaceChildPidPath`""
$orphanStartInfo.UseShellExecute = $false
$orphanStartInfo.CreateNoWindow = $true
$orphanRoot = New-Object Diagnostics.Process
$orphanRoot.StartInfo = $orphanStartInfo
Assert-Replay $orphanRoot.Start() "orphan_race_parent_start_failed"
$orphanRootInfo = Get-CimInstance Win32_Process -Filter "ProcessId = $([int]$orphanRoot.Id)" -ErrorAction Stop | Select-Object -First 1
$orphanRootIdentity = ConvertTo-AgentProcessIdentitySnapshot $orphanRootInfo
Assert-Replay ([bool]$orphanRootIdentity) "orphan_race_root_identity_capture_failed"
Assert-Replay $orphanRoot.WaitForExit(8000) "orphan_race_parent_did_not_exit"
Assert-Replay (Test-Path -LiteralPath $orphanRaceChildPidPath -PathType Leaf) "orphan_race_child_pid_missing"
$orphanIdentities = @(Get-AgentProcessTreeIdentitySnapshot $orphanRootIdentity $orphanRoot.ExitTime.ToUniversalTime())
Assert-Replay ($orphanIdentities.Count -ge 1) "orphan_race_descendant_snapshot_missing"
Assert-Replay (@($orphanIdentities | Where-Object { [int]$_.processId -eq [int]$orphanRoot.Id }).Count -eq 0) "orphan_race_root_unexpectedly_present"
$orphanRaceChildIdentity = @($orphanIdentities | Select-Object -First 1)[0]
$orphanRace = Stop-AgentBoundedProcessSnapshot $orphanRoot $orphanIdentities "Tree"
Assert-Replay ($orphanRace.processTreeStopped -and $orphanRace.processScopeStopped) "orphan_race_tree_false_green"
Assert-Replay (-not (Test-AgentProcessIdentityActive $orphanRaceChildIdentity)) "orphan_race_child_still_active"
$pidReuseRootCreated = [datetime]::UtcNow.AddMinutes(-5)
$pidReuseRootExited = $pidReuseRootCreated.AddSeconds(2)
$pidReuseExpectedRoot = [pscustomobject]@{
processId = 9001
parentProcessId = 7000
creationUtc = $pidReuseRootCreated.ToString("o")
name = "powershell.exe"
}
$pidReuseProcesses = @(
[pscustomobject]@{ ProcessId = 9001; ParentProcessId = 8000; CreationDate = $pidReuseRootExited.AddSeconds(1); Name = "powershell.exe" },
[pscustomobject]@{ ProcessId = 9002; ParentProcessId = 9001; CreationDate = $pidReuseRootCreated.AddSeconds(1); Name = "cmd.exe" },
[pscustomobject]@{ ProcessId = 9003; ParentProcessId = 9002; CreationDate = $pidReuseRootExited.AddSeconds(1); Name = "conhost.exe" },
[pscustomobject]@{ ProcessId = 9004; ParentProcessId = 9001; CreationDate = $pidReuseRootExited.AddSeconds(2); Name = "not-agent99.exe" },
[pscustomobject]@{ ProcessId = 9005; ParentProcessId = 9002; CreationDate = $pidReuseRootCreated.AddMilliseconds(500); Name = "older-than-parent.exe" }
)
$pidReuseSelection = @(Select-AgentProcessTreeIdentitySnapshot $pidReuseProcesses $pidReuseExpectedRoot $pidReuseRootExited)
$pidReuseSelectedIds = @($pidReuseSelection | ForEach-Object { [int]$_.processId } | Sort-Object)
Assert-Replay (($pidReuseSelectedIds -join ",") -eq "9002,9003") "pid_reuse_selection_not_identity_bounded"
$liveRootCreated = [datetime]::UtcNow.AddMinutes(-1)
$liveRootIdentity = [pscustomobject]@{ processId = 9101; parentProcessId = 7000; creationUtc = $liveRootCreated.ToString("o"); name = "powershell.exe" }
$liveRootProcesses = @(
[pscustomobject]@{ ProcessId = 9101; ParentProcessId = 7000; CreationDate = $liveRootCreated; Name = "powershell.exe" },
[pscustomobject]@{ ProcessId = 9102; ParentProcessId = 9101; CreationDate = $liveRootCreated.AddSeconds(1); Name = "valid-child.exe" },
[pscustomobject]@{ ProcessId = 9103; ParentProcessId = 9101; CreationDate = $liveRootCreated.AddSeconds(-1); Name = "older-unrelated.exe" }
)
$creationBoundSelection = @(Select-AgentProcessTreeIdentitySnapshot $liveRootProcesses $liveRootIdentity ([datetime]::MinValue))
$creationBoundIds = @($creationBoundSelection | ForEach-Object { [int]$_.processId } | Sort-Object)
Assert-Replay (($creationBoundIds -join ",") -eq "9101,9102") "live_root_child_creation_bound_failed"
$stopReadbackChildPidPath = Join-Path $root "stop-readback-failure-child.pid"
function Get-AgentProcessIdentityReadback { [pscustomobject]@{ known = $false; active = $false; reason = "synthetic_stop_readback_failure" } }
$stopReadbackFailure = Invoke-AgentBoundedProcess `
-FilePath $powershell `
-Arguments @("-NoProfile", "-NonInteractive", "-File", $rootOnlyParentPath, "-ChildPath", $rootOnlyChildPath, "-ChildPidPath", $stopReadbackChildPidPath) `
-TimeoutSeconds 2 `
-MaximumOutputBytes 4096
. ([ScriptBlock]::Create((Get-ExactFunctionSource $ast "Get-AgentProcessIdentityReadback")))
Assert-Replay ($stopReadbackFailure.timedOut -and $stopReadbackFailure.identityReadbackFailed) "stop_readback_failure_not_detected"
Assert-Replay ($stopReadbackFailure.treeReadbackFailed -and $stopReadbackFailure.rootFallbackTerminationUsed) "stop_readback_failure_not_fail_closed"
Assert-Replay ($stopReadbackFailure.processScopeStopped -and -not $stopReadbackFailure.processTreeStopped) "stop_readback_failure_tree_false_green"
Assert-Replay (Test-Path -LiteralPath $stopReadbackChildPidPath -PathType Leaf) "stop_readback_failure_child_pid_missing"
$stopReadbackChildPid = [int]([IO.File]::ReadAllText($stopReadbackChildPidPath))
$stopReadbackChildProcess = Get-CimInstance Win32_Process -Filter "ProcessId = $stopReadbackChildPid" -ErrorAction Stop
$stopReadbackFailureChildIdentity = ConvertTo-AgentProcessIdentitySnapshot $stopReadbackChildProcess
Assert-Replay (Test-AgentProcessIdentityActive $stopReadbackFailureChildIdentity) "stop_readback_failure_child_unexpectedly_stopped"
$cimFailureChildPidPath = Join-Path $root "cim-failure-child.pid"
function Get-AgentProcessTreeIdentitySnapshot { throw "synthetic_cim_failure" }
$cimFailure = Invoke-AgentBoundedProcess `
-FilePath $powershell `
-Arguments @("-NoProfile", "-NonInteractive", "-File", $rootOnlyParentPath, "-ChildPath", $rootOnlyChildPath, "-ChildPidPath", $cimFailureChildPidPath) `
-TimeoutSeconds 2 `
-MaximumOutputBytes 4096
. ([ScriptBlock]::Create((Get-ExactFunctionSource $ast "Get-AgentProcessTreeIdentitySnapshot")))
. ([ScriptBlock]::Create((Get-ExactFunctionSource $ast "Get-AgentProcessIdentityReadback")))
Assert-Replay ($cimFailure.timedOut -and $cimFailure.treeReadbackFailed) "cim_failure_not_detected"
Assert-Replay ($cimFailure.rootFallbackTerminationUsed -and $cimFailure.processScopeStopped) "cim_failure_root_not_stopped"
Assert-Replay (-not $cimFailure.processTreeStopped) "cim_failure_tree_false_green"
Assert-Replay (Test-Path -LiteralPath $cimFailureChildPidPath -PathType Leaf) "cim_failure_child_pid_missing"
$cimFailureChildPid = [int]([IO.File]::ReadAllText($cimFailureChildPidPath))
$cimFailureChildProcess = Get-CimInstance Win32_Process -Filter "ProcessId = $cimFailureChildPid" -ErrorAction Stop
$cimFailureChildIdentity = [pscustomobject]@{
processId = [int]$cimFailureChildProcess.ProcessId
parentProcessId = [int]$cimFailureChildProcess.ParentProcessId
creationUtc = ([datetime]$cimFailureChildProcess.CreationDate).ToUniversalTime().ToString("o")
name = [string]$cimFailureChildProcess.Name
}
Assert-Replay (Test-AgentProcessIdentityActive $cimFailureChildIdentity) "cim_failure_child_unexpectedly_stopped"
} finally {
. ([ScriptBlock]::Create((Get-ExactFunctionSource $ast "Get-AgentProcessTreeIdentitySnapshot")))
if ($rootOnlyChildIdentity -and (Test-AgentProcessIdentityActive $rootOnlyChildIdentity)) {
& "$env:SystemRoot\System32\taskkill.exe" /PID ([int]$rootOnlyChildIdentity.processId) /T /F 2>&1 | Out-Null
[void](Wait-AgentProcessTreeIdentityAbsent @($rootOnlyChildIdentity) 8)
}
if ($cimFailureChildIdentity -and (Test-AgentProcessIdentityActive $cimFailureChildIdentity)) {
& "$env:SystemRoot\System32\taskkill.exe" /PID ([int]$cimFailureChildIdentity.processId) /T /F 2>&1 | Out-Null
[void](Wait-AgentProcessTreeIdentityAbsent @($cimFailureChildIdentity) 8)
}
if ($orphanRaceChildIdentity -and (Test-AgentProcessIdentityActive $orphanRaceChildIdentity)) {
& "$env:SystemRoot\System32\taskkill.exe" /PID ([int]$orphanRaceChildIdentity.processId) /T /F 2>&1 | Out-Null
[void](Wait-AgentProcessTreeIdentityAbsent @($orphanRaceChildIdentity) 8)
}
if ($stopReadbackFailureChildIdentity -and (Test-AgentProcessIdentityActive $stopReadbackFailureChildIdentity)) {
& "$env:SystemRoot\System32\taskkill.exe" /PID ([int]$stopReadbackFailureChildIdentity.processId) /T /F 2>&1 | Out-Null
[void](Wait-AgentProcessTreeIdentityAbsent @($stopReadbackFailureChildIdentity) 8)
}
Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue
$cleanupVerified = [bool](
-not (Test-Path -LiteralPath $root) -and
(-not $rootOnlyChildIdentity -or -not (Test-AgentProcessIdentityActive $rootOnlyChildIdentity)) -and
(-not $orphanRaceChildIdentity -or -not (Test-AgentProcessIdentityActive $orphanRaceChildIdentity)) -and
(-not $stopReadbackFailureChildIdentity -or -not (Test-AgentProcessIdentityActive $stopReadbackFailureChildIdentity)) -and
(-not $cimFailureChildIdentity -or -not (Test-AgentProcessIdentityActive $cimFailureChildIdentity))
)
}
[pscustomobject]@{
schemaVersion = "agent99_bounded_process_replay_v2"
ok = [bool]($normal.ok -and $timeout.timedOut -and $timeout.processTreeStopped -and $flood.outputLimitExceeded -and $flood.processTreeStopped -and $rootOnly.timedOut -and $rootOnly.processScopeStopped -and -not $rootOnly.processTreeStopped -and $orphanRace.processTreeStopped -and $pidReuseSelection.Count -eq 2 -and $creationBoundSelection.Count -eq 2 -and $stopReadbackFailure.identityReadbackFailed -and -not $stopReadbackFailure.processTreeStopped -and $cimFailure.timedOut -and $cimFailure.treeReadbackFailed -and $cimFailure.rootFallbackTerminationUsed -and $cimFailure.processScopeStopped -and -not $cimFailure.processTreeStopped -and $cleanupVerified)
normal = [pscustomobject]@{
exitCode = [int]$normal.exitCode
completed = [bool]$normal.completed
}
timeout = [pscustomobject]@{
exitCode = [int]$timeout.exitCode
timedOut = [bool]$timeout.timedOut
processTreeStopped = [bool]$timeout.processTreeStopped
killedIdentityCount = [int]$timeout.killedIdentityCount
}
flood = [pscustomobject]@{
exitCode = [int]$flood.exitCode
outputLimitExceeded = [bool]$flood.outputLimitExceeded
capturedBytes = [long]$flood.capturedBytes
maximumOutputBytes = [long]$flood.maximumOutputBytes
processTreeStopped = [bool]$flood.processTreeStopped
}
rootOnly = [pscustomobject]@{
exitCode = [int]$rootOnly.exitCode
timedOut = [bool]$rootOnly.timedOut
terminationScope = [string]$rootOnly.terminationScope
processScopeStopped = [bool]$rootOnly.processScopeStopped
processTreeStopped = [bool]$rootOnly.processTreeStopped
preservedDescendantCount = [int]$rootOnly.preservedDescendantCount
}
orphanRace = [pscustomobject]@{
rootExitedBeforeSnapshot = $true
descendantSnapshotCount = [int]$orphanIdentities.Count
processScopeStopped = [bool]$orphanRace.processScopeStopped
processTreeStopped = [bool]$orphanRace.processTreeStopped
}
pidReuseSelection = [pscustomobject]@{
reusedRootExcluded = [bool](@($pidReuseSelection | Where-Object { [int]$_.processId -eq 9001 }).Count -eq 0)
originalChildIncluded = [bool](@($pidReuseSelection | Where-Object { [int]$_.processId -eq 9002 }).Count -eq 1)
originalGrandchildIncluded = [bool](@($pidReuseSelection | Where-Object { [int]$_.processId -eq 9003 }).Count -eq 1)
reusedRootChildExcluded = [bool](@($pidReuseSelection | Where-Object { [int]$_.processId -eq 9004 }).Count -eq 0)
intermediateOlderProcessExcluded = [bool](@($pidReuseSelection | Where-Object { [int]$_.processId -eq 9005 }).Count -eq 0)
}
creationBounds = [pscustomobject]@{
liveRootIncluded = [bool](@($creationBoundSelection | Where-Object { [int]$_.processId -eq 9101 }).Count -eq 1)
validChildIncluded = [bool](@($creationBoundSelection | Where-Object { [int]$_.processId -eq 9102 }).Count -eq 1)
olderUnrelatedChildExcluded = [bool](@($creationBoundSelection | Where-Object { [int]$_.processId -eq 9103 }).Count -eq 0)
}
stopReadbackFailure = [pscustomobject]@{
identityReadbackFailed = [bool]$stopReadbackFailure.identityReadbackFailed
treeReadbackFailed = [bool]$stopReadbackFailure.treeReadbackFailed
rootFallbackTerminationUsed = [bool]$stopReadbackFailure.rootFallbackTerminationUsed
processScopeStopped = [bool]$stopReadbackFailure.processScopeStopped
processTreeStopped = [bool]$stopReadbackFailure.processTreeStopped
}
cimFailure = [pscustomobject]@{
exitCode = [int]$cimFailure.exitCode
timedOut = [bool]$cimFailure.timedOut
treeReadbackFailed = [bool]$cimFailure.treeReadbackFailed
rootFallbackTerminationUsed = [bool]$cimFailure.rootFallbackTerminationUsed
processScopeStopped = [bool]$cimFailure.processScopeStopped
processTreeStopped = [bool]$cimFailure.processTreeStopped
}
cleanupVerified = $cleanupVerified
productionPathWritePerformed = $false
vmPowerChangePerformed = $false
hostRebootPerformed = $false
secretValuesRead = $false
} | ConvertTo-Json -Compress -Depth 5