* 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.
()
| 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, |
no test coverage detected