How Daemons Work

How Daemons Work

Synthesis. The full lifecycle from "user runs a CLI command" to "daemon is running and serving requests" to "daemon shuts down cleanly when no longer needed."

The lifecycle in pictures

   First CLI invocation
        │
        ▼
   ┌──────────────────────────────────────────────────────┐
   │  Probe socket — daemon up?                           │
   │  ├─ yes → connect, send request, exit                │
   │  └─ no  → continue                                   │
   └────────────────────┬─────────────────────────────────┘
                        │
                        ▼
   ┌──────────────────────────────────────────────────────┐
   │  Read PID file — process alive?                      │
   │  ├─ yes (but socket missing) → race / crash recovery │
   │  └─ no  → fork new daemon                            │
   └────────────────────┬─────────────────────────────────┘
                        │
                        ▼
   ┌──────────────────────────────────────────────────────┐
   │  Spawn daemon (re-exec self with daemon subcommand)  │
   │  ├─ detach from controlling terminal (setsid)        │
   │  ├─ chdir("/") to release the parent's cwd           │
   │  ├─ close stdin/stdout/stderr; reopen to /dev/null   │
   │  ├─ atomic-write PID file                            │
   │  ├─ flock the PID file (single-instance enforcement) │
   │  ├─ bind Unix socket (or named pipe on Windows)      │
   │  ├─ start tracing-subscriber → logs to file          │
   │  ├─ trap SIGTERM / SIGINT → graceful shutdown        │
   │  └─ enter accept loop                                │
   └────────────────────┬─────────────────────────────────┘
                        │
                        ▼
   ┌──────────────────────────────────────────────────────┐
   │  Client side: poll for socket to appear (~100ms × N) │
   │  ├─ found → connect                                  │
   │  └─ timeout (2s) → error: daemon failed to start     │
   └────────────────────┬─────────────────────────────────┘
                        │
                        ▼
                 Send request, get response, exit

Subsequent CLI invocations (microseconds later, hours later, the next day): probe → connect → request → response → exit. The daemon is just there.

What the daemon is doing while idle

Even when no client is connected, the daemon may be:

Idle daemons consume RAM (often the biggest cost) and minimal CPU (a few epoll/kqueue syscalls per second). The trade is "always-warm responsiveness" against "always-resident process."

Graceful shutdown

The full ritual when the daemon receives SIGTERM (or mxr shutdown):

1. Stop accepting new IPC connections (close listener)
2. Cancel new background tasks (set shutdown flag, watch::Sender::send(true))
3. Wait for in-flight work to complete or hit deadline:
   - Finish active IPC requests (each has a timeout)
   - Drain pending writes to durable storage (SQLite/TOML)
   - Disconnect from external services (close DAP adapter with 1s timeout)
4. Close children: for each spawned subprocess (codelldb, smtp connection),
   send SIGTERM, wait briefly, send SIGKILL if needed
5. Remove PID file
6. Exit with status 0

The deadline matters. Init systems wait ~30 seconds after SIGTERM, then send SIGKILL. Your shutdown handler must complete within that window or get killed mid-flight, leaving stale state.

Crash recovery

The daemon will crash. Patterns that survive:

Per-instance scoping

A single binary may run multiple daemon instances. mxr and lazydap both use this:

$LAZYDAP_INSTANCE=project-a → /tmp/lazydap/instance-a.sock + instance-a.pid
$LAZYDAP_INSTANCE=project-b → /tmp/lazydap/instance-b.sock + instance-b.pid

The instance key (often hashed from project root) ensures different projects get different daemons. Two cd into different repos = two daemons.

This is how lazydap supports multiple debug sessions concurrently across projects without sharing state.

Logging

Daemons can't write to stdout — they're detached. Options:

Log levels via env: RUST_LOG=info,lazydap=debug.

What a daemon CAN'T do

How mxr and lazydap embody this

Both:

The daemons are nearly identical at the orchestration level. The differences are entirely in what they do — mxr syncs email, lazydap drives DAP adapters. The daemon machinery is interchangeable.

See also (atomic notes)

Other syntheses (sibling MOCs in the vault)

The other "How X actually works" synthesis notes — each is the entry point to its own cluster:

Further reading