Skip to content

Commit 61480bc

Browse files
committed
Supervised daemon reclaims a stale manifest after reboot
A real reboot test (with integration data in the DB) surfaced an intermittent crash-loop: across a reboot the pre-reboot server.json persists with the old pid, and pids recycle — so isPidAlive(oldPid) often returns true for an unrelated process, making the startup guard treat it as a live-but-unreachable server and refuse to start. Under launchd/systemd KeepAlive that loops forever and the daemon never serves. Since the OS service manager is the real singleton (kickstart -k kills the old instance before starting the new), a supervised daemon now force-removes any pre-existing manifest and reclaims it instead of asserting. The daemon-pointer check already required reachability, so only the manifest path needed this.
1 parent 6b0d8d1 commit 61480bc

3 files changed

Lines changed: 51 additions & 7 deletions

File tree

apps/cli/src/local-server-manifest.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,25 @@ export const removeLocalServerManifestIfOwnedBy = (input: {
7474
yield* fs.remove(manifestPath, { force: true });
7575
});
7676

77+
/**
78+
* Remove the server manifest unconditionally. Used by an OS-supervised daemon
79+
* to reclaim a stale `server.json` left by a previous boot: across a reboot the
80+
* recorded pid is meaningless (pids recycle, so it may now belong to an
81+
* unrelated process), and launchd/systemd already guarantee a single supervised
82+
* instance — so any pre-existing manifest is stale and the supervised daemon
83+
* owns it.
84+
*/
85+
export const removeLocalServerManifest = (): Effect.Effect<
86+
void,
87+
PlatformError,
88+
FileSystem.FileSystem | Path.Path
89+
> =>
90+
Effect.gen(function* () {
91+
const fs = yield* FileSystem.FileSystem;
92+
const path = yield* Path.Path;
93+
yield* fs.remove(localServerManifestPath(path), { force: true });
94+
});
95+
7796
const StartupLockPayload = Schema.Struct({
7897
pid: Schema.Number,
7998
});

apps/cli/src/main.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ import {
9595
readLocalServerManifest,
9696
readServiceKey,
9797
releaseLocalServerStartLock,
98+
removeLocalServerManifest,
9899
removeLocalServerManifestIfOwnedBy,
99100
removeServiceKey,
100101
resolveExecutorDataDir,
@@ -898,7 +899,19 @@ const runDaemonSession = (input: {
898899
let token: string | null = null;
899900

900901
try {
901-
yield* assertNoOtherActiveLocalServer();
902+
// A supervised daemon (launchd/systemd) is the OS-guaranteed singleton
903+
// — kickstart -k kills the old instance before starting the new — so any
904+
// server.json from a previous boot is stale. Reclaim it rather than
905+
// refusing: across a reboot the recorded pid may have been recycled by
906+
// an unrelated process, which would otherwise make the "is one already
907+
// running?" check treat it as alive-but-unreachable, refuse to start,
908+
// and crash-loop under KeepAlive. (Found by a real reboot test with
909+
// integration data in the DB.)
910+
if (process.env.EXECUTOR_SUPERVISED) {
911+
yield* removeLocalServerManifest().pipe(Effect.ignore);
912+
} else {
913+
yield* assertNoOtherActiveLocalServer();
914+
}
902915

903916
const existing = yield* readDaemonPointer({ hostname: daemonHost, scopeId });
904917

apps/cli/src/service.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,9 @@ const runCommand = (
9595
env?: Record<string, string | undefined>,
9696
): Effect.Effect<CommandResult, Error> =>
9797
Effect.callback<CommandResult, Error>((resume) => {
98-
const options = env ? { encoding: "utf8" as const, env: { ...process.env, ...env } } : { encoding: "utf8" as const };
98+
const options = env
99+
? { encoding: "utf8" as const, env: { ...process.env, ...env } }
100+
: { encoding: "utf8" as const };
99101
execFile(cmd, [...args], options, (error, stdout, stderr) => {
100102
// A string `code` (ENOENT etc.) means the command could not be spawned.
101103
if (error && typeof (error as { code?: unknown }).code === "string") {
@@ -389,7 +391,9 @@ const makeSystemdBackend = (): ServiceBackend => {
389391
// it if the caller's environment lacks it (e.g. a non-login shell) so
390392
// install is robust regardless of how it was invoked.
391393
const username = userInfo().username;
392-
const sdEnv = { XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR ?? `/run/user/${currentUid()}` };
394+
const sdEnv = {
395+
XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR ?? `/run/user/${currentUid()}`,
396+
};
393397
yield* runCommand("systemctl", ["--user", "daemon-reload"], sdEnv).pipe(Effect.ignore);
394398
const enable = yield* runCommand(
395399
"systemctl",
@@ -398,7 +402,9 @@ const makeSystemdBackend = (): ServiceBackend => {
398402
);
399403
if (enable.code !== 0) {
400404
return yield* Effect.fail(
401-
new Error(`systemctl --user enable failed (exit ${enable.code}): ${enable.stderr.trim()}`),
405+
new Error(
406+
`systemctl --user enable failed (exit ${enable.code}): ${enable.stderr.trim()}`,
407+
),
402408
);
403409
}
404410
// Enable lingering so the user manager — and this enabled service —
@@ -412,7 +418,9 @@ const makeSystemdBackend = (): ServiceBackend => {
412418
Effect.gen(function* () {
413419
const fs = yield* FileSystem.FileSystem;
414420
const path = yield* Path.Path;
415-
const sdEnv = { XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR ?? `/run/user/${currentUid()}` };
421+
const sdEnv = {
422+
XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR ?? `/run/user/${currentUid()}`,
423+
};
416424
yield* runCommand("systemctl", ["--user", "disable", "--now", unitName], sdEnv).pipe(
417425
Effect.ignore,
418426
);
@@ -426,7 +434,9 @@ const makeSystemdBackend = (): ServiceBackend => {
426434
Effect.gen(function* () {
427435
const fs = yield* FileSystem.FileSystem;
428436
const path = yield* Path.Path;
429-
const sdEnv = { XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR ?? `/run/user/${currentUid()}` };
437+
const sdEnv = {
438+
XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR ?? `/run/user/${currentUid()}`,
439+
};
430440
const registered = yield* fs.exists(systemdUnitPath(path));
431441
const active = yield* runCommand("systemctl", ["--user", "is-active", unitName], sdEnv);
432442
const running = active.stdout.trim() === "active";
@@ -452,7 +462,9 @@ const makeSystemdBackend = (): ServiceBackend => {
452462
}),
453463
restart: () =>
454464
Effect.gen(function* () {
455-
const sdEnv = { XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR ?? `/run/user/${currentUid()}` };
465+
const sdEnv = {
466+
XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR ?? `/run/user/${currentUid()}`,
467+
};
456468
const result = yield* runCommand("systemctl", ["--user", "restart", unitName], sdEnv);
457469
if (result.code !== 0) {
458470
return yield* Effect.fail(

0 commit comments

Comments
 (0)