mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-31 12:19:48 +02:00
[eric] windows install reliability: NSIS recovery layers + PS5.1 build-script ASCII fix
Three changes that make Windows installs/upgrades stop stalling on the 'OpenSwarm cannot be closed' modal -- independent of the cloud OAuth work happening on the Mac side.
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
; Bulletproof install/upgrade recovery for the "OpenSwarm cannot be closed"
|
||||
; failure cascade.
|
||||
;
|
||||
; Three layers, each catching a different failure mode:
|
||||
;
|
||||
; Layer A pre-extract orphan kill (customInit)
|
||||
; Kills any OpenSwarm.exe + any python.exe / node.exe rooted
|
||||
; inside the install dir. Stops the dialog from EVER firing
|
||||
; because of subprocess holds.
|
||||
;
|
||||
; Layer B bulk-delete heavy dirs with REBOOTOK (customRemoveFiles)
|
||||
; Replaces NSIS's per-file delete loop on the four heavy dirs
|
||||
; (python-env, router, backend, app.asar) with a single
|
||||
; RMDir /r /REBOOTOK each. Faster than per-file (no Defender
|
||||
; lock-retry waterfall) and locked files defer to next boot
|
||||
; via MOVEFILE_DELAY_UNTIL_REBOOT instead of stalling.
|
||||
;
|
||||
; Layer C electron-builder's per-file silent retry stays as the safety
|
||||
; net for the remaining ~5% of files (anything not in the four
|
||||
; heavy dirs). No custom code needed — built into the framework.
|
||||
;
|
||||
; All layers run with the user's permissions (per-user install at
|
||||
; %LOCALAPPDATA%). No admin elevation, no system-wide changes.
|
||||
; taskkill targets only user-owned processes, wmic delete is filtered
|
||||
; to install-dir-rooted processes.
|
||||
|
||||
!macro customInit
|
||||
; ---- Layer A: pre-extract orphan kill ----
|
||||
;
|
||||
; Fires before NSIS extracts anything. The two killers below cover
|
||||
; all the process-lock cases observed in the field:
|
||||
;
|
||||
; 1. App is running (user double-clicked installer without quitting)
|
||||
; → taskkill /F /IM OpenSwarm.exe /T cascades through children
|
||||
; 2. App crashed and left orphan python.exe / node.exe with no parent
|
||||
; to taskkill via PID → wmic finds them by ExecutablePath substring
|
||||
; and deletes them
|
||||
;
|
||||
; Both are safe (filter to install-dir-rooted processes only) and
|
||||
; both no-op silently if no matching processes exist.
|
||||
|
||||
nsExec::Exec 'taskkill /F /IM OpenSwarm.exe /T'
|
||||
Pop $0 ; discard exit code; non-fatal if no process matched
|
||||
|
||||
; wmic where-clause: match anything under the per-user install dir.
|
||||
; The single backslash in '%\\Programs\\OpenSwarm\\%' becomes a
|
||||
; literal backslash after NSIS's escape, then SQL LIKE pattern.
|
||||
nsExec::Exec 'wmic process where "ExecutablePath like ''%\\Programs\\OpenSwarm\\%''" delete'
|
||||
Pop $0
|
||||
|
||||
; Brief pause so Windows kernel releases handles before NSIS tries
|
||||
; to delete the binaries in customRemoveFiles below. ~1.5s is enough
|
||||
; in practice; longer here doesn't help and slows clean installs.
|
||||
Sleep 1500
|
||||
!macroend
|
||||
|
||||
!macro customRemoveFiles
|
||||
; ---- Layer B: bulk-delete heavy dirs with REBOOTOK fallback ----
|
||||
;
|
||||
; Runs in upgrade installs (no-op on first install since the dirs
|
||||
; don't exist yet). Replaces NSIS's per-file delete loop for the
|
||||
; four directories that contain ~95% of the file count, which is
|
||||
; where Defender + Search indexer contention is concentrated.
|
||||
;
|
||||
; RMDir /r recursively removes the directory tree.
|
||||
; /REBOOTOK schedules deletion via MOVEFILE_DELAY_UNTIL_REBOOT for
|
||||
; any file that's currently locked — Windows commits the deletion
|
||||
; on next boot before any process can re-acquire the handle.
|
||||
; Net effect: no file lock can block the install. Worst case is
|
||||
; one stale file lingering until reboot, which is invisible to
|
||||
; the user since the new install writes fresh files alongside.
|
||||
;
|
||||
; The standard NSIS per-file loop still runs after this for the
|
||||
; remaining ~5% of files (the few files at $INSTDIR\ root level),
|
||||
; with electron-builder's built-in retry as safety net.
|
||||
|
||||
RMDir /r /REBOOTOK "$INSTDIR\resources\python-env"
|
||||
RMDir /r /REBOOTOK "$INSTDIR\resources\router"
|
||||
RMDir /r /REBOOTOK "$INSTDIR\resources\backend"
|
||||
RMDir /r /REBOOTOK "$INSTDIR\resources\debugger"
|
||||
RMDir /r /REBOOTOK "$INSTDIR\resources\frontend"
|
||||
Delete /REBOOTOK "$INSTDIR\resources\app.asar"
|
||||
Delete /REBOOTOK "$INSTDIR\resources\elevate.exe"
|
||||
Delete /REBOOTOK "$INSTDIR\OpenSwarm.exe"
|
||||
!macroend
|
||||
@@ -82,7 +82,8 @@
|
||||
"createStartMenuShortcut": true,
|
||||
"shortcutName": "OpenSwarm",
|
||||
"deleteAppDataOnUninstall": false,
|
||||
"artifactName": "OpenSwarm-Setup-${arch}.${ext}"
|
||||
"artifactName": "OpenSwarm-Setup-${arch}.${ext}",
|
||||
"include": "build/installer-recovery.nsh"
|
||||
},
|
||||
"extraResources": [
|
||||
{
|
||||
|
||||
@@ -79,7 +79,7 @@ if (-not (Test-Path (Join-Path $UvBinDir 'uvx.exe'))) {
|
||||
try {
|
||||
Invoke-WebRequest -Uri $UvUrl -OutFile $TmpZip -UseBasicParsing
|
||||
Expand-Archive -Path $TmpZip -DestinationPath $TmpExtract -Force
|
||||
# Only copy uvx.exe — skip uv.exe entirely.
|
||||
# Only copy uvx.exe -- skip uv.exe entirely.
|
||||
Get-ChildItem -Path $TmpExtract -Recurse -Filter 'uvx.exe' | Select-Object -First 1 | ForEach-Object { Copy-Item $_.FullName (Join-Path $UvBinDir 'uvx.exe') -Force }
|
||||
Write-Host "uvx.exe downloaded and bundled."
|
||||
} finally {
|
||||
@@ -113,7 +113,7 @@ New-Item -ItemType Directory -Force -Path $McpBundleDir | Out-Null
|
||||
# Single-file CJS bundle. Output path: mcp-bundles\<output>.js. Use for
|
||||
# packages that don't read sibling files at runtime. The import.meta.url
|
||||
# polyfill is applied uniformly because nearly every modern ESM package
|
||||
# uses createRequire(import.meta.url) somewhere — without the polyfill,
|
||||
# uses createRequire(import.meta.url) somewhere -- without the polyfill,
|
||||
# esbuild's ESM->CJS transform leaves import.meta.url as undefined and
|
||||
# the bundle crashes at module load.
|
||||
function Build-McpBundleSingle($PackageName, $EntrySubpath, $OutputName) {
|
||||
@@ -290,7 +290,7 @@ function Copy-Excluded($Source, $Dest, $Exclude) {
|
||||
foreach ($d in $Exclude.Dirs) { $args += '/XD'; $args += $d }
|
||||
foreach ($f in $Exclude.Files) { $args += '/XF'; $args += $f }
|
||||
& robocopy @args | Out-Null
|
||||
# robocopy exit codes 0–7 are success
|
||||
# robocopy exit codes 0-7 are success
|
||||
if ($LASTEXITCODE -ge 8) { throw "robocopy failed ($Source -> $Dest, exit $LASTEXITCODE)" }
|
||||
$global:LASTEXITCODE = 0
|
||||
}
|
||||
@@ -325,7 +325,7 @@ if (Test-Path $DevEnvPath) {
|
||||
Set-Content -Path $ShipEnvPath -Value $kept
|
||||
Write-Host "Staged OAuth credentials: $($kept.Count) keys (release secrets + personal API keys excluded)"
|
||||
} else {
|
||||
Write-Host "WARNING: backend\.env not found — packaged build will have no OAuth credentials configured."
|
||||
Write-Host "WARNING: backend\.env not found -- packaged build will have no OAuth credentials configured."
|
||||
Set-Content -Path $ShipEnvPath -Value '' -NoNewline
|
||||
}
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $Staging 'backend\data\tools') | Out-Null
|
||||
|
||||
Reference in New Issue
Block a user