mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-09 13:21:42 +00:00
fix(update): prevent and self-heal half-updated venvs on Windows (#57659)
Root-causes the July 2026 Windows incident chain (locked _brotlicffi.pyd / _sodium.pyd during install, then 'No module named annotated_doc' with 'hermes update' insisting 'Already up to date!'): - hermes update: probe venv core imports even when the checkout is current; a half-updated venv (dep sync killed mid-flight by a locked .pyd) is now detected and repaired instead of being reported as up to date - hermes update (Windows): after pausing gateways, refuse to mutate the venv while other processes run from the venv interpreter (the Desktop backend runs as python.exe so the hermes.exe shim guard never saw it); --force keeps the old behavior - install.ps1 venv stage: disarm gateway autostart Scheduled Tasks before the kill sweep (they respawn the gateway inside the kill->delete window), make the sweep a bounded loop requiring 3 clean passes, and rename-then- delete the old venv (a rename succeeds even with mapped DLLs) with stale- dir cleanup on the next run - desktop updater: 'venv shim still locked after 15s' now ABORTS the update hand-off (restarting our backend, surfacing the holder to the user) instead of 'proceeding anyway (force)' into guaranteed venv corruption; the unlock wait also re-kills respawned backends each poll tick
This commit is contained in:
parent
741bd9ba42
commit
b14d75f8af
4 changed files with 507 additions and 20 deletions
|
|
@ -1614,6 +1614,26 @@ function Install-Venv {
|
|||
if ($env:OS -eq "Windows_NT") {
|
||||
$myPid = $PID
|
||||
Write-Info "Stopping any running hermes processes before recreating venv..."
|
||||
# Disarm respawners FIRST: the gateway autostart Scheduled Task and
|
||||
# the Startup-folder entry both relaunch a killed gateway within
|
||||
# seconds, and losing that race re-locks the venv's .pyd files
|
||||
# between our kill sweep and Remove-Item (the July 2026
|
||||
# _brotlicffi.pyd incident). schtasks /End stops a running task
|
||||
# instance; /Change /DISABLE stops it from re-firing mid-install.
|
||||
# Re-enabled after the venv is recreated (below). Best-effort: a
|
||||
# missing task just errors quietly.
|
||||
$gatewayTasksDisabled = @()
|
||||
try {
|
||||
schtasks /Query /FO CSV 2>$null | ConvertFrom-Csv | Where-Object { $_.TaskName -like '*Hermes_Gateway*' } | ForEach-Object {
|
||||
$tn = $_.TaskName
|
||||
schtasks /End /TN $tn 2>$null | Out-Null
|
||||
schtasks /Change /TN $tn /DISABLE 2>$null | Out-Null
|
||||
$gatewayTasksDisabled += $tn
|
||||
Write-Info " disabled gateway autostart task $tn for the duration of the install"
|
||||
}
|
||||
} catch {
|
||||
Write-Warn "Could not enumerate gateway scheduled tasks: $($_.Exception.Message)"
|
||||
}
|
||||
# The launcher CLI (hermes.exe) plus its child tree.
|
||||
& taskkill /F /T /IM hermes.exe /FI "PID ne $myPid" 2>$null | Out-Null
|
||||
# taskkill /IM hermes.exe is NOT enough: the gateway/agent that a
|
||||
|
|
@ -1632,27 +1652,68 @@ function Install-Venv {
|
|||
# ExecutablePath for a process it cannot inspect (a different session)
|
||||
# instead of throwing, so an unreadable process is skipped rather than
|
||||
# aborting the whole sweep.
|
||||
#
|
||||
# The sweep is a bounded LOOP, not single-shot: supervised processes
|
||||
# (the Desktop app's backend, a watchdog-managed gateway) respawn in
|
||||
# the window between one kill pass and the delete. Each pass re-
|
||||
# enumerates; three consecutive clean passes (or the attempt cap)
|
||||
# ends the loop.
|
||||
$venvPrefix = [System.IO.Path]::GetFullPath((Join-Path $InstallDir "venv")).TrimEnd('\') + '\'
|
||||
try {
|
||||
Get-CimInstance Win32_Process -ErrorAction Stop |
|
||||
Where-Object { $_.ProcessId -ne $myPid -and $_.ExecutablePath -and $_.ExecutablePath.StartsWith($venvPrefix, [System.StringComparison]::OrdinalIgnoreCase) } |
|
||||
ForEach-Object {
|
||||
Write-Info " stopping PID $($_.ProcessId) ($($_.Name)) running from venv"
|
||||
Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
} catch {
|
||||
Write-Warn "Could not enumerate venv processes: $($_.Exception.Message)"
|
||||
$cleanPasses = 0
|
||||
for ($sweep = 0; $sweep -lt 10 -and $cleanPasses -lt 3; $sweep++) {
|
||||
$found = 0
|
||||
try {
|
||||
Get-CimInstance Win32_Process -ErrorAction Stop |
|
||||
Where-Object { $_.ProcessId -ne $myPid -and $_.ExecutablePath -and $_.ExecutablePath.StartsWith($venvPrefix, [System.StringComparison]::OrdinalIgnoreCase) } |
|
||||
ForEach-Object {
|
||||
$found++
|
||||
Write-Info " stopping PID $($_.ProcessId) ($($_.Name)) running from venv"
|
||||
Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
} catch {
|
||||
Write-Warn "Could not enumerate venv processes: $($_.Exception.Message)"
|
||||
break
|
||||
}
|
||||
if ($found -eq 0) { $cleanPasses++ } else { $cleanPasses = 0 }
|
||||
Start-Sleep -Milliseconds 400
|
||||
}
|
||||
Start-Sleep -Milliseconds 800
|
||||
}
|
||||
Remove-Item -Recurse -Force "venv" -ErrorAction SilentlyContinue
|
||||
# A killed process can take a moment to release its file handles, so a
|
||||
# first Remove-Item may still hit a locked .pyd. Retry once after a short
|
||||
# pause before giving up and letting the stage fail loudly.
|
||||
if (Test-Path "venv") {
|
||||
Start-Sleep -Seconds 2
|
||||
Remove-Item -Recurse -Force "venv"
|
||||
# Rename-then-delete: on Windows a directory RENAME succeeds even while
|
||||
# files inside it are mapped as DLLs (only in-place delete/replace of
|
||||
# the mapped file is denied, and only same-volume renames are atomic
|
||||
# moves). Moving the old venv aside means `uv venv` can create a fresh
|
||||
# one immediately even if some straggler still holds a .pyd from the
|
||||
# old tree; the renamed dir is deleted best-effort (now, and by the
|
||||
# cleanup pass below on the NEXT install if a handle outlives this one).
|
||||
$staleName = "venv.stale.{0}" -f (Get-Date -Format "yyyyMMddHHmmss")
|
||||
$renamed = $false
|
||||
try {
|
||||
Rename-Item -Path "venv" -NewName $staleName -ErrorAction Stop
|
||||
$renamed = $true
|
||||
} catch {
|
||||
Write-Warn "Could not rename venv aside ($($_.Exception.Message)); falling back to in-place delete"
|
||||
}
|
||||
if ($renamed) {
|
||||
Remove-Item -Recurse -Force $staleName -ErrorAction SilentlyContinue
|
||||
if (Test-Path $staleName) {
|
||||
Write-Warn "Old venv parked at $staleName (a process still holds files in it); it will be cleaned up on the next install"
|
||||
}
|
||||
} else {
|
||||
Remove-Item -Recurse -Force "venv" -ErrorAction SilentlyContinue
|
||||
# A killed process can take a moment to release its file handles, so a
|
||||
# first Remove-Item may still hit a locked .pyd. Retry once after a short
|
||||
# pause before giving up and letting the stage fail loudly.
|
||||
if (Test-Path "venv") {
|
||||
Start-Sleep -Seconds 2
|
||||
Remove-Item -Recurse -Force "venv"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Clean up parked venvs from previous installs whose handles have since
|
||||
# been released. Best-effort — a still-held tree just stays for next time.
|
||||
Get-ChildItem -Directory -Filter "venv.stale.*" -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
Remove-Item -Recurse -Force $_.FullName -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# uv creates the venv and pins the Python version in one step. uv emits
|
||||
|
|
@ -1684,6 +1745,18 @@ function Install-Venv {
|
|||
|
||||
Pop-Location
|
||||
|
||||
# Re-arm the gateway autostart tasks disabled during the venv teardown.
|
||||
# Same function scope, so the list survives even under the stage-per-
|
||||
# process bootstrap. Deliberately NOT started here — dependencies aren't
|
||||
# installed yet; the task fires normally on next logon and `hermes update`
|
||||
# / the gateway resume path handles the immediate restart.
|
||||
if ($gatewayTasksDisabled -and $gatewayTasksDisabled.Count -gt 0) {
|
||||
foreach ($tn in $gatewayTasksDisabled) {
|
||||
schtasks /Change /TN $tn /ENABLE 2>$null | Out-Null
|
||||
}
|
||||
Write-Info "Re-enabled gateway autostart task(s): $($gatewayTasksDisabled -join ', ')"
|
||||
}
|
||||
|
||||
Write-Success "Virtual environment ready (Python $PythonVersion)"
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue