MCPcopy Create free account
hub / github.com/colbymchenry/codegraph / start

Method start

src/mcp/daemon.ts:206–305  ·  view source on GitHub ↗

* Bind the socket, kick off engine init, and register signal handlers. The * lockfile body was already written atomically by `tryAcquireDaemonLock`, so * there is nothing to write here. The promise resolves once the server is * listening — the daemon then sticks around until idle/shutdown.

()

Source from the content-addressed store, hash-verified

204 * listening — the daemon then sticks around until idle/shutdown.
205 */
206 async start(): Promise<DaemonStartResult> {
207 // Engine init is deliberately backgrounded — see #172. The first session
208 // to land waits on `ensureInitialized` either way, and unloaded sessions
209 // (cross-project tool calls only) shouldn't pay any open cost.
210 void this.engine.ensureInitialized(this.projectRoot);
211
212 // Walk the ordered socket candidates and bind the first that works. The
213 // in-project path comes first; the deterministic tmpdir path is the fallback
214 // for a filesystem that can't host an AF_UNIX node at all (ExFAT/FAT external
215 // volumes, some network mounts, WSL2 DrvFs → ENOTSUP/EACCES; #997, #974). The
216 // `listen` closure clears a stale socket (left by a SIGKILL'd previous daemon)
217 // before each attempt — safe because we hold the lockfile, so no live daemon
218 // owns it; without it `listen` would wedge on EADDRINUSE.
219 const candidates = getDaemonSocketCandidates(this.projectRoot);
220 const listen = (socketPath: string): Promise<net.Server> =>
221 new Promise<net.Server>((resolve, reject) => {
222 if (process.platform !== 'win32') {
223 try { fs.unlinkSync(socketPath); } catch { /* not-exists is fine */ }
224 }
225 const server = net.createServer((socket) => this.handleConnection(socket));
226 server.once('error', reject);
227 server.listen(socketPath, () => {
228 // POSIX: tighten permissions to user-only — the socket lives under
229 // `.codegraph/` (git-ignored, maybe a shared FS) or tmpdir.
230 if (process.platform !== 'win32') {
231 try { fs.chmodSync(socketPath, 0o600); } catch { /* best-effort */ }
232 }
233 resolve(server);
234 });
235 });
236
237 let bound: { server: net.Server; socketPath: string };
238 try {
239 bound = await bindFirstUsableSocket(candidates, listen, {
240 onRelocate: (from, to, code) =>
241 process.stderr.write(
242 `[CodeGraph daemon] Socket ${from} unusable (${code}); relocating to ${to}.\n`
243 ),
244 });
245 } catch (err) {
246 // Every candidate failed (the last one, or a non-relocatable error like a
247 // racing EADDRINUSE). We already hold the lockfile `tryAcquireDaemonLock`
248 // wrote; release it and any partial sockets so the NEXT launcher doesn't
249 // spin respawning us on a stale lock pointing at our now-dying pid. Then
250 // re-throw so the caller (the bin's try/catch) exits this detached daemon
251 // cleanly and every launcher falls back to direct mode (#974).
252 this.cleanupLockfile();
253 if (process.platform !== 'win32') {
254 for (const candidate of candidates) {
255 try { fs.unlinkSync(candidate); } catch { /* may not exist */ }
256 }
257 }
258 throw err;
259 }
260
261 this.server = bound.server;
262 // Adopt the path we ACTUALLY bound — it may be a tmpdir fallback past an
263 // unusable in-project location. Everything downstream (lockfile, registry,

Callers 1

startDaemonProcessMethod · 0.95

Calls 11

cleanupLockfileMethod · 0.95
armIdleTimerMethod · 0.95
startLivenessTimersMethod · 0.95
stopMethod · 0.95
encodeLockInfoFunction · 0.90
registerDaemonFunction · 0.90
bindFirstUsableSocketFunction · 0.85
ensureInitializedMethod · 0.80
onMethod · 0.65
writeMethod · 0.45

Tested by

no test coverage detected