diff --git a/electron/build/installer-recovery.nsh b/electron/build/installer-recovery.nsh index f5dbd123..778235d2 100644 --- a/electron/build/installer-recovery.nsh +++ b/electron/build/installer-recovery.nsh @@ -79,8 +79,18 @@ ; the install still completes; user just pays the cold-start tax on ; first launch, same as before this macro existed. - nsExec::Exec '"$INSTDIR\OpenSwarm.exe" --prewarm' - Pop $0 ; discard exit code; prewarm is best-effort + ; Skip prewarm on SILENT installs. CI's installer verification AND production + ; auto-updates both run the installer with /S, and nsExec::Exec is synchronous + ; with no upper bound - launching the freshly-extracted, not-yet-signed + ; OpenSwarm.exe (which loads python.exe + node.exe) provokes a cold Windows + ; Defender scan that can stall the silent install for minutes (it hung the CI + ; installer check, and would do the same to a user's auto-update). Interactive + ; first-time installs, where the cold-start win actually lands, still prewarm. + ${If} ${Silent} + ${Else} + nsExec::Exec '"$INSTDIR\OpenSwarm.exe" --prewarm' + Pop $0 ; discard exit code; prewarm is best-effort + ${EndIf} !macroend !macro customRemoveFiles diff --git a/scripts/ci/verify-installer.js b/scripts/ci/verify-installer.js index 6c0d91b3..683e7d86 100644 --- a/scripts/ci/verify-installer.js +++ b/scripts/ci/verify-installer.js @@ -99,15 +99,24 @@ function runDestructive(setup) { } process.stdout.write('\n[destructive] installing silently...\n'); killRunning(); - const inst = spawnSync(setup, ['/S'], { stdio: 'inherit' }); - if (inst.status !== 0 && inst.status !== null) bad(`Setup.exe /S exited ${inst.status}`); - // oneClick installer returns before files settle; wait for the exe to appear. - const deadline = Date.now() + 120000; - while (Date.now() < deadline && !exists(path.join(INSTALL_DIR, 'OpenSwarm.exe'))) { execSync('powershell -NoProfile -Command "Start-Sleep -Milliseconds 1000"'); } + const installedExe = path.join(INSTALL_DIR, 'OpenSwarm.exe'); + // Blocking: spawnSync returns only once the installer has fully finished + // (files + registry uninstall entry + shortcuts are all written, the last of + // which assertInstalled checks). The timeout is the safety net: the silent + // path now skips the unbounded --prewarm step (see installer-recovery.nsh), so + // a clean extract finishes well inside this, but if a future installer change + // reintroduces a synchronous stall this fails the gate in minutes instead of + // hanging the whole job until it is force-cancelled. + const inst = spawnSync(setup, ['/S'], { stdio: 'inherit', timeout: 300000 }); + if (inst.error) bad(`Setup.exe /S did not complete: ${inst.error.message}`); + else if (inst.status !== 0 && inst.status !== null) bad(`Setup.exe /S exited ${inst.status}`); + // oneClick installer can return microseconds before the exe handle settles. + const deadline = Date.now() + 30000; + while (Date.now() < deadline && !exists(installedExe)) { execSync('powershell -NoProfile -Command "Start-Sleep -Milliseconds 1000"'); } + killRunning(); // defensive: ensure nothing the installer launched is holding the dir assertInstalled(); // Launch the INSTALLED exe through the full gate (boot/serve/provenance/etc.). - const installedExe = path.join(INSTALL_DIR, 'OpenSwarm.exe'); if (exists(installedExe)) { process.stdout.write('\n[destructive] verifying the INSTALLED app...\n'); const v = spawnSync(process.execPath, [path.join(__dirname, 'verify-all.js'), '--app', installedExe], { stdio: 'inherit' });