MCPcopy
hub / github.com/garrytan/gstack / ensureDaemon

Function ensureDaemon

design/src/daemon-client.ts:89–169  ·  view source on GitHub ↗
(
  opts: EnsureDaemonOptions = {},
)

Source from the content-addressed store, hash-verified

87 * branch — that's a user-actionable situation, not a programming error.
88 */
89export async function ensureDaemon(
90 opts: EnsureDaemonOptions = {},
91): Promise<EnsureDaemonResult> {
92 const verbose = opts.verbose !== false;
93 const expectedVersion = opts.version ?? readPackageVersion();
94 const stateFile = opts.stateFile ?? resolveStateFilePath();
95
96 const existing = readStateFile(stateFile);
97 if (existing) {
98 const health = await healthCheck(existing.port);
99 if (health) {
100 if (health.version === expectedVersion) {
101 log(verbose, `attached to existing daemon pid=${existing.pid} port=${existing.port}`);
102 return { port: existing.port, version: health.version, spawned: false };
103 }
104 // Version mismatch: refuse if active boards exist (Codex finding).
105 if (health.activeBoards > 0) {
106 process.stderr.write(
107 `[design-daemon] WARNING: existing daemon is gstack ${health.version}; this CLI is ${expectedVersion}.\n` +
108 `[design-daemon] ${health.activeBoards} active board(s) detected. Refusing to auto-kill.\n` +
109 `[design-daemon] Submit or close the open boards, then re-run.\n` +
110 `[design-daemon] Or force restart: $D daemon stop (will drop in-memory history).\n`,
111 );
112 process.exit(1);
113 }
114 // No active boards — safe to graceful-shutdown and respawn.
115 log(verbose, `daemon version mismatch (${health.version} vs ${expectedVersion}); shutting down`);
116 await gracefulShutdownExistingDaemon(existing.port);
117 await killByPidWithIdentity(existing.pid, existing.cmdlineMarker, verbose);
118 } else {
119 // State file points at an unresponsive port. Either the daemon
120 // crashed or the PID got reused. Identity-verify before any SIGTERM
121 // so we don't kill an unrelated process (Codex finding).
122 log(verbose, `state file present (pid=${existing.pid}) but /health unresponsive`);
123 await killByPidWithIdentity(existing.pid, existing.cmdlineMarker, verbose);
124 }
125 }
126
127 // Spawn under exclusive lock; re-read state INSIDE the lock so we don't
128 // race a concurrent CLI that won the lock first.
129 const lockPath = resolveLockFilePath(stateFile);
130 const release = acquireLock(lockPath);
131 if (!release) {
132 // Another process is starting the daemon. Wait for it.
133 log(verbose, "another CLI is spawning the daemon; waiting…");
134 const start = Date.now();
135 while (Date.now() - start < MAX_START_WAIT_MS) {
136 const fresh = readStateFile(stateFile);
137 if (fresh) {
138 const h = await healthCheck(fresh.port);
139 if (h) return { port: fresh.port, version: h.version, spawned: false };
140 }
141 await delay(POLL_INTERVAL_MS);
142 }
143 throw new Error("Timed out waiting for concurrent daemon spawn");
144 }
145
146 try {

Callers 2

publishToDaemonFunction · 0.90

Calls 10

resolveStateFilePathFunction · 0.90
readStateFileFunction · 0.90
healthCheckFunction · 0.90
resolveLockFilePathFunction · 0.90
acquireLockFunction · 0.90
readPackageVersionFunction · 0.85
killByPidWithIdentityFunction · 0.85
logFunction · 0.70
spawnDaemonFunction · 0.70

Tested by

no test coverage detected