diff --git a/apps/desktop/src/main/index.mjs b/apps/desktop/src/main/index.mjs index d32f35b2..1499d00e 100644 --- a/apps/desktop/src/main/index.mjs +++ b/apps/desktop/src/main/index.mjs @@ -819,34 +819,56 @@ app.whenReady().then(async () => { if (requestedUserDataDirectory) throw new Error("Local data reset is unavailable with a custom user-data directory"); if (localDataResetScheduled) return { scheduled: true }; - localDataResetScheduled = true; - isQuitting = true; - shutdownCleanupStarted = true; - if (sidecarRestartTimer) { - clearTimeout(sidecarRestartTimer); - sidecarRestartTimer = null; - } - tray?.destroy(); - try { - await stopSidecar(); - await Promise.allSettled([ - session.defaultSession.clearStorageData(), - session.defaultSession.clearCache(), - ]); - scheduleMacLocalDataReset({ + await scheduleMacLocalDataReset({ appDataDirectory: app.getPath("appData"), - executablePath: process.execPath, + executablePath: app.getPath("exe"), parentPid: process.pid, userDataDirectory: app.getPath("userData"), }); } catch (error) { - localDataResetScheduled = false; - isQuitting = false; - shutdownCleanupStarted = false; + await writeDiagnostic("local-data-reset.schedule-failed", { + message: error instanceof Error ? error.message : String(error), + }); throw error; } + // Only begin shutting down once the detached reset helper has definitely + // started. From this point on, exiting lets that helper remove userData and + // relaunch the app, so non-critical cleanup failures must not strand the + // application in a half-stopped state. + localDataResetScheduled = true; + isQuitting = true; + shutdownCleanupStarted = true; + const forcedExitTimer = setTimeout(() => app.exit(0), 5000); + forcedExitTimer.unref(); + if (sidecarRestartTimer) { + clearTimeout(sidecarRestartTimer); + sidecarRestartTimer = null; + } + try { + tray?.destroy(); + tray = null; + } catch (error) { + void writeDiagnostic("local-data-reset.tray-cleanup-failed", { + message: error instanceof Error ? error.message : String(error), + }); + } + await stopSidecar().catch((error) => writeDiagnostic("local-data-reset.sidecar-stop-failed", { + message: error instanceof Error ? error.message : String(error), + })); + const storageResults = await Promise.allSettled([ + Promise.resolve().then(() => session.defaultSession.clearStorageData()), + Promise.resolve().then(() => session.defaultSession.clearCache()), + ]); + const storageFailure = storageResults.find((result) => result.status === "rejected"); + if (storageFailure) { + void writeDiagnostic("local-data-reset.storage-cleanup-failed", { + message: storageFailure.reason instanceof Error ? storageFailure.reason.message : String(storageFailure.reason), + }); + } + + clearTimeout(forcedExitTimer); setTimeout(() => app.exit(0), 50).unref(); return { scheduled: true }; }); diff --git a/apps/desktop/src/main/local-data-reset.mjs b/apps/desktop/src/main/local-data-reset.mjs index 4a809835..1dc689d5 100644 --- a/apps/desktop/src/main/local-data-reset.mjs +++ b/apps/desktop/src/main/local-data-reset.mjs @@ -6,7 +6,7 @@ parent_pid="$1" user_data_dir="$2" application_path="$3" attempt=0 -while /bin/kill -0 "$parent_pid" 2>/dev/null && [ "$attempt" -lt 300 ]; do +while /bin/kill -0 "$parent_pid" 2>/dev/null && [ "$attempt" -lt 3000 ]; do /bin/sleep 0.1 attempt=$((attempt + 1)) done @@ -49,7 +49,7 @@ export const macApplicationBundlePath = (executablePath) => { throw new Error("EdgeEver must be running from a macOS application bundle"); }; -export const scheduleMacLocalDataReset = ({ +export const scheduleMacLocalDataReset = async ({ appDataDirectory, executablePath, parentPid, @@ -73,6 +73,10 @@ export const scheduleMacLocalDataReset = ({ stdio: "ignore", }, ); + await new Promise((resolveSpawn, rejectSpawn) => { + helper.once("spawn", resolveSpawn); + helper.once("error", rejectSpawn); + }); helper.unref(); return { applicationPath, target }; }; diff --git a/apps/desktop/src/main/local-data-reset.test.mjs b/apps/desktop/src/main/local-data-reset.test.mjs index 9427a568..07e15de6 100644 --- a/apps/desktop/src/main/local-data-reset.test.mjs +++ b/apps/desktop/src/main/local-data-reset.test.mjs @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; +import { EventEmitter } from "node:events"; import { macApplicationBundlePath, managedUserDataDirectory, @@ -32,16 +33,19 @@ describe("desktop local data reset", () => { expect(() => macApplicationBundlePath("/usr/local/bin/edgeever")).toThrow(); }); - test("passes paths as shell arguments instead of interpolating them", () => { + test("passes paths as shell arguments instead of interpolating them", async () => { let invocation; let unrefCalled = false; - const result = scheduleMacLocalDataReset({ + const resultPromise = scheduleMacLocalDataReset({ appDataDirectory: "/Users/example/Library/Application Support", executablePath: "/Applications/EdgeEver.app/Contents/MacOS/EdgeEver", parentPid: 42, spawnProcess: (...args) => { invocation = args; - return { unref: () => { unrefCalled = true; } }; + const helper = new EventEmitter(); + helper.unref = () => { unrefCalled = true; }; + queueMicrotask(() => helper.emit("spawn")); + return helper; }, userDataDirectory: "/Users/example/Library/Application Support/EdgeEver data", }); @@ -55,10 +59,28 @@ describe("desktop local data reset", () => { expect(invocation[1][1]).not.toContain("/Applications/EdgeEver.app"); expect(invocation[1][1]).not.toContain("EdgeEver data"); expect(invocation[2]).toEqual({ detached: true, stdio: "ignore" }); + const result = await resultPromise; expect(unrefCalled).toBe(true); expect(result).toEqual({ applicationPath: "/Applications/EdgeEver.app", target: "/Users/example/Library/Application Support/EdgeEver data", }); }); + + test("rejects when the reset helper cannot be started", async () => { + const resultPromise = scheduleMacLocalDataReset({ + appDataDirectory: "/Users/example/Library/Application Support", + executablePath: "/Applications/EdgeEver.app/Contents/MacOS/EdgeEver", + parentPid: 42, + spawnProcess: () => { + const helper = new EventEmitter(); + helper.unref = () => {}; + queueMicrotask(() => helper.emit("error", new Error("spawn failed"))); + return helper; + }, + userDataDirectory: "/Users/example/Library/Application Support/EdgeEver", + }); + + await expect(resultPromise).rejects.toThrow("spawn failed"); + }); });