fix(updater): run the handoff script via systemd-run so it survives the app's scope

On uwsm/Hyprland the app runs in a systemd scope cgroup; a detached child is
swept away when the app quits, which killed the swap mid-flight on every
in-app update attempt. systemd-run gives the installer its own transient
unit that outlives the scope; detached spawn stays as the non-systemd
fallback.
This commit is contained in:
avi 2026-10-04 13:09:55 -05:00
commit 12977e1a77
2 changed files with 35 additions and 7 deletions

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "folio", "name": "folio",
"version": "0.1.7", "version": "0.1.8",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "folio", "name": "folio",
"version": "0.1.7", "version": "0.1.8",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tiptap/core": "^2.27.2", "@tiptap/core": "^2.27.2",

View file

@ -1286,12 +1286,40 @@ handleIpc("folio:performUpdate", async () => {
}), }),
{ mode: 0o700 } { mode: 0o700 }
); );
// IMPORTANT: on systemd desktops (Hyprland/uwsm) the app runs inside a
// systemd scope cgroup, and a plain detached child stays in that cgroup —
// when the app quits, systemd sweeps the scope and kills the swap script
// mid-run (root cause of the "update failed" reports: the handoff died
// before swapping, and the app was relaunched from the old tree).
// systemd-run puts the script in its own transient unit that outlives us;
// verified to survive on this machine. Fall back to a detached spawn for
// non-systemd sessions.
const { spawn } = await import("child_process"); const { spawn } = await import("child_process");
const child = spawn("/bin/sh", [script], { detached: true, stdio: "ignore" }); let handedOff = false;
child.on("error", (err) => { try {
console.error("[folio] updater spawn failed:", err.message); const { execFileSync } = await import("child_process");
}); execFileSync("systemd-run", [
child.unref(); "--user", "--quiet", "--collect",
`--description=Folio updater handoff`,
"/bin/sh", script,
], { timeout: 10_000, stdio: "ignore" });
handedOff = true;
} catch {
try {
const child = spawn("/bin/sh", [script], { detached: true, stdio: "ignore" });
child.on("error", (err) => {
console.error("[folio] updater spawn failed:", err.message);
});
child.unref();
handedOff = true;
} catch (e) {
void e;
}
}
if (!handedOff) {
try { fs.appendFileSync(updateLog, new Date().toISOString() + " FAIL: could not start swap script\n"); } catch { /* ignore */ }
return { error: "Could not start the installer. Your current version is unchanged." };
}
// Quit so the swap script can replace the app directory. // Quit so the swap script can replace the app directory.
setTimeout(() => app.quit(), 300); setTimeout(() => app.quit(), 300);
return { ok: true, version: rel.version }; return { ok: true, version: rel.version };