1import * as Duration from "effect/Duration";
2import * as Effect from "effect/Effect";
3import * as HostProcess from "@t3tools/shared/HostProcess";
4
5const DEFAULT_POLL_INTERVAL = Duration.seconds(2);
6
7// Capture before config and bootstrap reads to detect launcher loss during startup.
8const startupParentPid = process.ppid;
9
10/**
11 * Any parent change signals launcher loss, including adoption by a Linux subreaper;
12 * otherwise an orphan backend keeps its ownership lock and blocks replacement launches.
13 * Skip init-launched backends and Windows, which retains the exited parent's id.
14 */
15export const awaitLauncherLoss = Effect.fn("launcherWatch.awaitLauncherLoss")(function* (options?: {
16 readonly launcherPid?: number;
17 readonly getParentPid?: () => number;
18 readonly interval?: Duration.Input;
19}): Effect.fn.Return<void> {
20 const platform = yield* HostProcess.Platform;
21 const launcherPid = options?.launcherPid ?? startupParentPid;
22 const getParentPid = options?.getParentPid ?? (() => process.ppid);
23 const interval = options?.interval ?? DEFAULT_POLL_INTERVAL;
24 if (platform === "win32" || launcherPid <= 1) return yield* Effect.never;
25 const poll: Effect.Effect<void> = Effect.suspend(() => {
26 const parentPid = getParentPid();
27 return parentPid === launcherPid
28 ? Effect.flatMap(Effect.sleep(interval), () => poll)
29 : Effect.logWarning("desktop launcher is gone; shutting down the backend", {
30 launcherPid,
31 parentPid,
32 });
33 });
34 return yield* poll;
35});