- marker helpers move to scripts/desktop-update/marker.ps1, dot-sourced first (windows.ps1 2113 -> 1930 lines); a hand-off that cannot load it exits 3 having changed nothing. It ships in the checkout next to windows.ps1 and runtime.ps1, which is where the Desktop runs them from. - creation time comes from Win32_Process.CreationDate (limited query rights), cached per pid while it lives: Get-Process .StartTime is access denied for SYSTEM/elevated/other-user pids under PowerShell 5.1, so a pid reused by a service after a reboot read as a live owner forever. Alive with an unreadable time is live only inside the 20-minute ceiling. - -DesktopPid runs adopt the Desktop's live bridge claim only; a refused run exits 2 without a result file and only reopens a Desktop that is gone. - positional parse identical to the other readers; claims publish the complete body with an exclusive hard link (rename fallback); a fresh empty marker is a claim in flight; the delegate line always carries ct. - a follow-up step whose process tree cannot be quiesced after the commit point is ok:true + a manual warning (no relaunch, marker kept live), not "still on the previous version". - the interrupted post-update tail is always a manual warning: re-running hermes update is the only remedy, so the copy says exactly that. - the launcher probe bound is a -ProbeTimeoutSeconds parameter (default 60), not the HERMES_UPDATE_PROBE_TIMEOUT_SECONDS environment variable. (cherry picked from commit 65663dc6a64f942c6385b7de3ba13cdecd393387)
87 lines
4.7 KiB
PowerShell
87 lines
4.7 KiB
PowerShell
function Invoke-HermesProbe {
|
|
param([Parameter(Mandatory = $true)][string]$Exe, [string[]]$Arguments = @())
|
|
# Every launcher probe is bounded (60 s): a launcher that hangs (a stuck
|
|
# import, an AV scan) fails the probe instead of parking the hand-off
|
|
# forever after the Desktop already closed. Timeout kills the probe tree.
|
|
# windows.ps1 -ProbeTimeoutSeconds sets the bound for tests.
|
|
$timeout = if ($HermesProbeTimeoutSeconds -gt 0) { $HermesProbeTimeoutSeconds } else { 60 }
|
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
$psi.FileName = $Exe
|
|
$psi.Arguments = (@($Arguments) | ForEach-Object { '"{0}"' -f ($_ -replace '"', '\"') }) -join ' '
|
|
$psi.UseShellExecute = $false
|
|
$psi.CreateNoWindow = $true
|
|
$psi.RedirectStandardInput = $true
|
|
$psi.RedirectStandardOutput = $true
|
|
$psi.RedirectStandardError = $true
|
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
|
|
$process = [System.Diagnostics.Process]::Start($psi)
|
|
try { $process.StandardInput.Close() } catch {}
|
|
$stdout = $process.StandardOutput.ReadToEndAsync()
|
|
$stderr = $process.StandardError.ReadToEndAsync()
|
|
if (-not $process.WaitForExit($timeout * 1000)) {
|
|
& taskkill.exe /T /F /PID $process.Id 2>&1 | Out-Null
|
|
return @{ Code = 124; Output = ''; TimedOut = $true }
|
|
}
|
|
# A descendant may hold the pipes open; never wait on it.
|
|
$text = if ($stdout.Wait(5000)) { $stdout.Result } else { '' }
|
|
[void]$stderr
|
|
return @{ Code = $process.ExitCode; Output = $text; TimedOut = $false }
|
|
}
|
|
|
|
function Get-HermesRuntimeCommand {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$InstallRoot,
|
|
[string]$Module = 'hermes_cli.main'
|
|
)
|
|
|
|
# The launcher owns interpreter/ABI and generation selection. Never infer
|
|
# PM's store layout or borrow a different installation's PATH command.
|
|
foreach ($name in @('hermes.exe', 'hermes.cmd')) {
|
|
$launcher = Join-Path $InstallRoot ".hermes\bin\$name"
|
|
if (Test-Path -LiteralPath $launcher -PathType Leaf) {
|
|
$probe = Invoke-HermesProbe $launcher @('--print-runtime-command', '--module', $Module)
|
|
if ($probe.TimedOut) { throw "Installation launcher did not answer within the probe timeout: $launcher" }
|
|
if ($probe.Code) { throw "Installation launcher failed (exit $($probe.Code)): $launcher" }
|
|
$command = @(($probe.Output | ConvertFrom-Json))
|
|
if ($command.Count -lt 2 -or @($command | Where-Object { $_ -isnot [string] -or -not $_ }).Count) {
|
|
throw "Installation launcher returned invalid command: $launcher"
|
|
}
|
|
return $command
|
|
}
|
|
}
|
|
|
|
# Earlier PM installers published only to user-bin. The established
|
|
# --version surface reports the bound source root; never trust PATH alone.
|
|
if (Test-Path -LiteralPath (Join-Path $InstallRoot 'hermes_cli/_launchers.py') -PathType Leaf) {
|
|
$directories = @(
|
|
(Join-Path $env:HERMES_HOME 'bin'),
|
|
(Join-Path (Split-Path -Parent $InstallRoot) 'bin')
|
|
)
|
|
if ($env:LOCALAPPDATA) { $directories += Join-Path $env:LOCALAPPDATA 'hermes/bin' }
|
|
foreach ($directory in ($directories | Select-Object -Unique)) {
|
|
foreach ($name in @('hermes.exe', 'hermes.cmd')) {
|
|
$legacy = Join-Path $directory $name
|
|
if (-not (Test-Path -LiteralPath $legacy -PathType Leaf)) { continue }
|
|
$probe = Invoke-HermesProbe $legacy @('--version')
|
|
$version = $probe.Output
|
|
if ($probe.TimedOut -or $probe.Code -or $version -notmatch '(?m)^Install directory: (.+)\r?$') { continue }
|
|
$reported = [IO.Path]::GetFullPath($Matches[1].Trim()).TrimEnd('\', '/')
|
|
$expected = [IO.Path]::GetFullPath($InstallRoot).TrimEnd('\', '/')
|
|
if (-not [string]::Equals($reported, $expected, [StringComparison]::OrdinalIgnoreCase)) { continue }
|
|
if ($Module -ne 'hermes_cli.main') {
|
|
throw "This older installation needs its launcher refreshed before running $Module. Run the update through $legacy."
|
|
}
|
|
return @($legacy)
|
|
}
|
|
}
|
|
}
|
|
|
|
# Only an older, pre-PM checkout may use the historical interpreter.
|
|
if (-not (Test-Path -LiteralPath (Join-Path $InstallRoot 'pm') -PathType Container)) {
|
|
foreach ($directory in @('venv', '.venv')) {
|
|
$python = Join-Path $InstallRoot "$directory\Scripts\python.exe"
|
|
if (Test-Path -LiteralPath $python -PathType Leaf) { return @($python, '-m', $Module) }
|
|
}
|
|
}
|
|
throw "Installation launcher is missing under $InstallRoot\.hermes\bin. Repair this installation."
|
|
} |