// Builds the batch script that installs a downloaded Theseus update AFTER // the browser has exited. Pure function, no Electron — main.js writes the // result next to its user data and starts it detached from will-quit; the // tests run it against a dummy process and a fake setup. // // Why a helper at all: on 2026-09-11 an update ran while the app was still // shutting down. The NSIS installer's uninstall-old-version step had moved // the whole old install into its temp folder when both NSIS processes died // ~8 s in, and the install step never wrote a file — the user was left with // a folder missing app.asar and ffmpeg.dll. The exact killer was never // identified, so this removes every overlap with our own lifetime: // // 1. poll until our PID is gone (tasklist), then a grace period for // Chromium's child processes to follow; // 2. start the installer from a process that is not in any job of ours // (a detached cmd.exe survives the app exiting — verified — whereas a // detached powershell.exe silently does nothing without a console, and // a non-detached child is killed with the app); // 3. wait for the installer and, if resources\app.asar is missing from the // install dir afterwards, run it once more — the installer is // idempotent and a second pass repairs a torn install. // // Batch specifics: `timeout` refuses to run without a console, so sleeps are // `ping -n 127.0.0.1`; the setup is launched with `start "" /wait` // so the script blocks until the installer exits. The script deletes itself. function buildUpdateHelperCmd({ pid, setupPath, installDir, args = ["/S", "--force-run"], graceSec = 2, maxWaitSec = 120 }) { if (!Number.isInteger(pid) || pid <= 0) throw new Error("pid required"); if (typeof setupPath !== "string" || !setupPath || /["\r\n%]/.test(setupPath)) throw new Error("setupPath required (no quotes, percent signs or newlines)"); if (typeof installDir !== "string" || !installDir || /["\r\n%]/.test(installDir)) throw new Error("installDir required (no quotes, percent signs or newlines)"); const argStr = args.map(String).join(" "); if (/["\r\n%]/.test(argStr)) throw new Error("installer args must not contain quotes, percent signs or newlines"); const grace = Math.max(0, graceSec | 0) + 1; const maxIter = Math.max(1, maxWaitSec | 0); // Everything runs inside a console-less cmd.exe, which has two traps // (both hit while writing this): child console programs' redirected // stdout comes back EMPTY (tasklist, wmic, even powershell — so no // "tasklist | find" style probing), and `start /wait` is unreliable. What // does work there: exit codes, timing, cmd-internal redirection, and a // child powershell.exe blocking in Wait-Process. So the wait is a child // PowerShell that returns once our PID is gone (or after maxWaitSec), the // installer is invoked directly (from a batch file cmd.exe waits for it, // and no ShellExecute means no mark-of-the-web prompt), and tools are // addressed by full path so a Unix toolchain on PATH can't shadow them. const S32 = "%SystemRoot%\\System32"; const PS = `${S32}\\WindowsPowerShell\\v1.0\\powershell.exe`; return [ "@echo off", "setlocal", `set "SETUP=${setupPath}"`, `set "ASAR=${installDir}\\resources\\app.asar"`, `"${PS}" -NoProfile -NonInteractive -Command "Wait-Process -Id ${pid} -Timeout ${maxIter} -ErrorAction SilentlyContinue"`, `"${S32}\\ping.exe" -n ${grace} 127.0.0.1 >nul`, `"%SETUP%" ${argStr}`, `if not exist "%ASAR%" (`, ` "${S32}\\ping.exe" -n 4 127.0.0.1 >nul`, ` "%SETUP%" ${argStr}`, ")", "endlocal", // Self-delete without cmd.exe's "The batch file cannot be found" when it // tries to read the next line: the (goto) 2>nul idiom ends the batch // context first, then del runs on the same line. `(goto) 2>nul & del "%~f0"`, "", ].join("\r\n"); } module.exports = { buildUpdateHelperCmd };