From 4f07615b13d889bab95b6ca5d8e41efc0457d76f Mon Sep 17 00:00:00 2001 From: MiTHRAL Date: Tue, 21 Apr 2026 23:01:35 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20Windows=20updater=20=E2=80=94=20defer=20?= =?UTF-8?q?file=20copy=20to=20batch=20script=20after=20app=20exits?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- src/native/update-window.ts | 2 +- src/native/updater.ts | 46 +++++++++++++++++++++++++++---------- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src/native/update-window.ts b/src/native/update-window.ts index bc4be42..8f5dc08 100644 --- a/src/native/update-window.ts +++ b/src/native/update-window.ts @@ -74,6 +74,6 @@ export function setUpdateError(msg: string) { } ipcMain.on("upd-restart", () => { - app.relaunch(); + if (process.platform !== "win32") app.relaunch(); app.exit(0); }); diff --git a/src/native/updater.ts b/src/native/updater.ts index a817318..d114f3c 100644 --- a/src/native/updater.ts +++ b/src/native/updater.ts @@ -1,6 +1,6 @@ import { Notification, app, ipcMain } from "electron"; import { exec } from "child_process"; -import { createWriteStream, mkdirSync } from "fs"; +import { createWriteStream, mkdirSync, writeFileSync } from "fs"; import { dirname, join } from "path"; import { tmpdir } from "os"; @@ -96,19 +96,41 @@ async function downloadAndInstall(url: string, version: string) { setUpdateProgress(95); console.log(`[updater] download complete, extracting to ${installDir}`); - await new Promise((resolve, reject) => { - const cmd = - process.platform === "win32" - ? `powershell -Command "Expand-Archive -Force -Path '${zipPath}' -DestinationPath '${extractDir}'; $sub = (Get-ChildItem '${extractDir}' | Select-Object -First 1).FullName; Copy-Item -Recurse -Force \\"$sub\\*\\" '${installDir}'"` - : `unzip -o "${zipPath}" -d "${extractDir}" && SUBDIR=$(ls "${extractDir}" | head -1) && rm -f "${installDir}/sanctum" && cp -rT "${extractDir}/$SUBDIR" "${installDir}"`; - - exec(cmd, { shell: process.platform === "win32" ? undefined : "/bin/bash" }, (err, _stdout, stderr) => { - if (err) { console.error("[updater] extract failed:", stderr); reject(err); } - else resolve(); + if (process.platform === "win32") { + // Extract zip while the app is still running (no locked files yet) + await new Promise((resolve, reject) => { + const cmd = `powershell -Command "Expand-Archive -Force -Path '${zipPath}' -DestinationPath '${extractDir}'"`; + exec(cmd, (err, _stdout, stderr) => { + if (err) { console.error("[updater] extract failed:", stderr); reject(err); } + else resolve(); + }); }); - }); - setUpdateReady(version); + // Write a batch script that runs after we exit: waits, copies, relaunches + const batchPath = join(tmpDir, "apply-update.bat"); + const bat = [ + "@echo off", + "timeout /t 3 /nobreak >nul", + `for /d %%D in ("${extractDir}\\*") do set SUB=%%D`, + `xcopy /E /Y /I "%SUB%\\*" "${installDir}\\"`, + `start "" "${join(installDir, "sanctum.exe")}"`, + `del "%~f0"`, + ].join("\r\n"); + writeFileSync(batchPath, bat); + + // Launch batch detached so it outlives this process + exec(`cmd /C start /B "" "${batchPath}"`); + setUpdateReady(version); // restart button will just app.exit(0) — batch relaunches + } else { + await new Promise((resolve, reject) => { + const cmd = `unzip -o "${zipPath}" -d "${extractDir}" && SUBDIR=$(ls "${extractDir}" | head -1) && rm -f "${installDir}/sanctum" && cp -rT "${extractDir}/$SUBDIR" "${installDir}"`; + exec(cmd, { shell: "/bin/bash" }, (err, _stdout, stderr) => { + if (err) { console.error("[updater] extract failed:", stderr); reject(err); } + else resolve(); + }); + }); + setUpdateReady(version); + } } function notify(title: string, body: string) {