Version: 0.2.17 (osx-arm64), guid 019e1325-54c3-7000-b0ec-4c3f7e7d20d4
OS: macOS 26.5.2, Apple Silicon (arm64)
Summary
On macOS, applying an auto-update kills the app on every launch, in an infinite loop. The updater helper is spawned as updater.app, but the file actually shipped/installed is a plain executable named updater (no extension), so the spawn fails with ENOENT. The app then exits anyway, so from the user's point of view wow.export just dies at "Restarting application..." on every single launch (it re-downloads the update and crashes again each time).
runtime.log
[10:50:34] Checking for updater application at /Users/<user>/Library/Application Support/wow.export/.update/updater.app
[10:50:34] Updater exists check: false
[10:50:34] Spawning updater process: /Users/<user>/Library/Application Support/wow.export/updater.app with parent PID 8257
[10:50:34] ERROR: Failed to spawn updater: spawn /Users/<user>/Library/Application Support/wow.export/updater.app ENOENT
[10:50:34] Updater spawned successfully (PID: NaN), detaching...
Root cause
In src/constants.js / the bundled app.js:
var UPDATER_EXT = { win32: ".exe", darwin: ".app" };
...
HELPER: "updater" + (UPDATER_EXT[process.platform] || "")
so on darwin constants.UPDATE.HELPER is updater.app. But what's actually on disk in the install root is a plain Mach-O executable named updater:
-rwxr-xr-x 1 user staff 59884256 updater
and the downloaded update payload doesn't contain updater.app either (the "Updater exists check" against .update/updater.app returns false). So cp.spawn(helperApp, ...) in launchUpdater() fails with ENOENT.
Note that even if a real updater.app bundle existed, cp.spawn() on a .app directory wouldn't work on macOS anyway (it's a directory, not an executable); it would need to spawn the inner Contents/MacOS/... binary or the bare helper executable. Since the shipped artifact is already the bare updater executable, dropping the .app extension for darwin looks like the fix.
Secondary bug: the app exits even though the spawn failed
In launchUpdater(), spawn failure is only surfaced via the async error event, and the throw err inside that handler never propagates to the surrounding try/catch:
const child = cp.spawn(helperApp, [process.pid], { detached: true, stdio: "ignore" });
child.on("error", (err) => {
log.write("ERROR: Failed to spawn updater: %s", err.message);
throw err; // <- lands nowhere useful
});
await new Promise((resolve) => setTimeout(resolve, 100));
log.write("Updater spawned successfully (PID: %d), detaching...", child.pid); // logs PID: NaN
child.unref();
process.exit(); // <- exits even though no updater is running
That's why the log claims "Updater spawned successfully (PID: NaN)" right after the ENOENT, and why the app hard-exits instead of recovering. Guarding process.exit() on the spawn actually having a PID (or checking child.pid !== undefined) would at least let the app fall back to running the current version instead of dying in a loop.
Workaround
Launching with --disable-auto-update avoids the loop entirely.
Version: 0.2.17 (osx-arm64), guid
019e1325-54c3-7000-b0ec-4c3f7e7d20d4OS: macOS 26.5.2, Apple Silicon (arm64)
Summary
On macOS, applying an auto-update kills the app on every launch, in an infinite loop. The updater helper is spawned as
updater.app, but the file actually shipped/installed is a plain executable namedupdater(no extension), so the spawn fails with ENOENT. The app then exits anyway, so from the user's point of view wow.export just dies at "Restarting application..." on every single launch (it re-downloads the update and crashes again each time).runtime.log
Root cause
In
src/constants.js/ the bundled app.js:so on darwin
constants.UPDATE.HELPERisupdater.app. But what's actually on disk in the install root is a plain Mach-O executable namedupdater:and the downloaded update payload doesn't contain
updater.appeither (the "Updater exists check" against.update/updater.appreturns false). Socp.spawn(helperApp, ...)inlaunchUpdater()fails with ENOENT.Note that even if a real
updater.appbundle existed,cp.spawn()on a.appdirectory wouldn't work on macOS anyway (it's a directory, not an executable); it would need to spawn the innerContents/MacOS/...binary or the bare helper executable. Since the shipped artifact is already the bareupdaterexecutable, dropping the.appextension for darwin looks like the fix.Secondary bug: the app exits even though the spawn failed
In
launchUpdater(), spawn failure is only surfaced via the asyncerrorevent, and thethrow errinside that handler never propagates to the surrounding try/catch:That's why the log claims "Updater spawned successfully (PID: NaN)" right after the ENOENT, and why the app hard-exits instead of recovering. Guarding
process.exit()on the spawn actually having a PID (or checkingchild.pid !== undefined) would at least let the app fall back to running the current version instead of dying in a loop.Workaround
Launching with
--disable-auto-updateavoids the loop entirely.