Skills/long-running-job-monitor/scripts/Start-DetachedPowerShell.ps1
|
<#
.SYNOPSIS Starts an encoded PowerShell payload as a fully detached process. .DESCRIPTION Uses Start-Process on Windows. On non-Windows systems, uses nohup through sh so the child survives the launching shell. The payload owns its output logging; this launcher returns process and result metadata. .EXAMPLE $encodedCommand = [Convert]::ToBase64String( [Text.Encoding]::Unicode.GetBytes("'hello'") ) ./Start-DetachedPowerShell.ps1 -EncodedCommand $encodedCommand #> [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [ValidatePattern('^[A-Za-z0-9+/]+={0,2}$')] [string] $EncodedCommand, [ValidateNotNullOrEmpty()] [string] $ResultPath = (Join-Path $env:TEMP ( 'detached-{0}.exit' -f [guid]::NewGuid().ToString('N') )) ) $ErrorActionPreference = 'Stop' if (Test-Path -LiteralPath $ResultPath) { throw "ResultPath already exists: $ResultPath" } $resultDirectory = Split-Path -Path $ResultPath -Parent if (-not (Test-Path -LiteralPath $resultDirectory -PathType Container)) { New-Item -ItemType Directory -Path $resultDirectory -Force | Out-Null } $escapedResultPath = $ResultPath.Replace("'", "''") $innerEncodedCommand = $EncodedCommand $wrapper = @" `$ErrorActionPreference = 'Stop' try { `$innerPwsh = (Get-Command -Name pwsh -ErrorAction Stop).Source & `$innerPwsh -NoProfile -NonInteractive -EncodedCommand '$innerEncodedCommand' `$innerExitCode = `$LASTEXITCODE `$resultValue = if (`$innerExitCode -eq 0) { '0' } else { '1' } [IO.File]::WriteAllText('$escapedResultPath', `$resultValue) exit `$innerExitCode } catch { [IO.File]::WriteAllText('$escapedResultPath', '1') throw } "@ $EncodedCommand = [Convert]::ToBase64String( [Text.Encoding]::Unicode.GetBytes($wrapper) ) $pwshPath = (Get-Command -Name pwsh -ErrorAction Stop).Source $isWindowsPlatform = [Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT if ($isWindowsPlatform) { $process = Start-Process -FilePath $pwshPath -ArgumentList @( '-NoProfile' '-NonInteractive' '-EncodedCommand' $EncodedCommand ) -WindowStyle Hidden -PassThru [pscustomobject]@{ ProcessId = $process.Id Platform = 'Windows' Detached = $true ResultPath = $ResultPath } return } $nohupPath = (Get-Command -Name nohup -ErrorAction Stop).Source $shellPath = (Get-Command -Name sh -ErrorAction Stop).Source function ConvertTo-PosixSingleQuoted { [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [string] $Value ) $singleQuote = [string][char]39 $doubleQuote = [string][char]34 $embeddedSingleQuote = "$singleQuote$doubleQuote$singleQuote$doubleQuote$singleQuote" $singleQuote + $Value.Replace($singleQuote, $embeddedSingleQuote) + $singleQuote } $quotedNohup = ConvertTo-PosixSingleQuoted -Value $nohupPath $quotedPwsh = ConvertTo-PosixSingleQuoted -Value $pwshPath $shellCommand = @( $quotedNohup $quotedPwsh '-NoProfile' '-NonInteractive' '-EncodedCommand' $EncodedCommand '</dev/null' '>/dev/null' '2>&1' '&' 'echo $!' ) -join ' ' $processIdText = (& $shellPath -c $shellCommand | Select-Object -Last 1).Trim() if ($LASTEXITCODE -ne 0 -or $processIdText -notmatch '^\d+$') { throw "Failed to start detached PowerShell through nohup. Output: '$processIdText'." } [pscustomobject]@{ ProcessId = [int]$processIdText Platform = 'Unix' Detached = $true ResultPath = $ResultPath } |