301 lines
6.2 KiB
Markdown
301 lines
6.2 KiB
Markdown
# Daemon Patterns
|
|
|
|
Deep dive into @outfitter/daemon patterns.
|
|
|
|
## Creating a Daemon
|
|
|
|
```typescript
|
|
import {
|
|
createDaemon,
|
|
getLockPath,
|
|
} from "@outfitter/daemon";
|
|
import { createLogger, createConsoleSink } from "@outfitter/logging";
|
|
|
|
const logger = createLogger({
|
|
name: "my-daemon",
|
|
level: "info",
|
|
sinks: [createConsoleSink()],
|
|
});
|
|
|
|
const daemon = createDaemon({
|
|
name: "my-daemon",
|
|
pidFile: getLockPath("my-daemon"),
|
|
logger,
|
|
shutdownTimeout: 10000, // 10s graceful shutdown
|
|
});
|
|
```
|
|
|
|
## Lifecycle Hooks
|
|
|
|
```typescript
|
|
// Called before start
|
|
daemon.onBeforeStart(async () => {
|
|
logger.info("Preparing to start...");
|
|
await initializeDatabase();
|
|
});
|
|
|
|
// Called after start
|
|
daemon.onAfterStart(async () => {
|
|
logger.info("Daemon started successfully");
|
|
});
|
|
|
|
// Called on shutdown (SIGTERM, SIGINT)
|
|
daemon.onShutdown(async () => {
|
|
logger.info("Shutting down...");
|
|
await closeConnections();
|
|
await flushBuffers();
|
|
});
|
|
|
|
// Start the daemon
|
|
const result = await daemon.start();
|
|
if (result.isErr()) {
|
|
logger.error("Failed to start", { error: result.error });
|
|
process.exit(1);
|
|
}
|
|
```
|
|
|
|
## IPC Server
|
|
|
|
### Setting Up IPC
|
|
|
|
```typescript
|
|
import {
|
|
createIpcServer,
|
|
getSocketPath,
|
|
} from "@outfitter/daemon";
|
|
|
|
const ipcServer = createIpcServer(getSocketPath("my-daemon"));
|
|
|
|
// Handle messages
|
|
ipcServer.onMessage(async (msg) => {
|
|
const message = msg as { type: string; payload?: unknown };
|
|
|
|
switch (message.type) {
|
|
case "status":
|
|
return {
|
|
status: "ok",
|
|
uptime: process.uptime(),
|
|
version: "1.0.0",
|
|
};
|
|
|
|
case "reload":
|
|
await reloadConfig();
|
|
return { success: true };
|
|
|
|
case "metrics":
|
|
return getMetrics();
|
|
|
|
default:
|
|
return { error: "Unknown command" };
|
|
}
|
|
});
|
|
|
|
// Register cleanup
|
|
daemon.onShutdown(async () => {
|
|
await ipcServer.close();
|
|
});
|
|
|
|
// Start listening
|
|
await ipcServer.listen();
|
|
logger.info("IPC listening", { socket: getSocketPath("my-daemon") });
|
|
```
|
|
|
|
### IPC Client
|
|
|
|
```typescript
|
|
import {
|
|
createIpcClient,
|
|
getSocketPath,
|
|
} from "@outfitter/daemon";
|
|
|
|
const client = createIpcClient(getSocketPath("my-daemon"));
|
|
|
|
await client.connect();
|
|
|
|
// Send message and get response
|
|
const status = await client.send<{
|
|
status: string;
|
|
uptime: number;
|
|
}>({ type: "status" });
|
|
|
|
console.log("Daemon status:", status);
|
|
|
|
// Clean up
|
|
client.close();
|
|
```
|
|
|
|
## Health Checks
|
|
|
|
### Defining Checks
|
|
|
|
```typescript
|
|
import { createHealthChecker } from "@outfitter/daemon";
|
|
import { Result } from "@outfitter/contracts";
|
|
|
|
const healthChecker = createHealthChecker([
|
|
{
|
|
name: "memory",
|
|
check: async () => {
|
|
const used = process.memoryUsage().heapUsed / 1024 / 1024;
|
|
return used < 500
|
|
? Result.ok(undefined)
|
|
: Result.err(new Error(`High memory: ${used.toFixed(2)}MB`));
|
|
},
|
|
},
|
|
{
|
|
name: "database",
|
|
check: async () => {
|
|
try {
|
|
await db.ping();
|
|
return Result.ok(undefined);
|
|
} catch (error) {
|
|
return Result.err(new Error("Database unreachable"));
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "disk",
|
|
check: async () => {
|
|
const free = await getDiskSpace();
|
|
return free > 100 * 1024 * 1024 // 100MB
|
|
? Result.ok(undefined)
|
|
: Result.err(new Error("Low disk space"));
|
|
},
|
|
},
|
|
]);
|
|
```
|
|
|
|
### Exposing Health via IPC
|
|
|
|
```typescript
|
|
ipcServer.onMessage(async (msg) => {
|
|
if (msg.type === "health") {
|
|
const result = await healthChecker.check();
|
|
return {
|
|
healthy: result.isOk(),
|
|
checks: result.isOk() ? result.value : result.error,
|
|
};
|
|
}
|
|
});
|
|
```
|
|
|
|
### Periodic Health Checks
|
|
|
|
```typescript
|
|
const HEALTH_INTERVAL = 30000; // 30 seconds
|
|
|
|
setInterval(async () => {
|
|
const result = await healthChecker.check();
|
|
|
|
if (result.isErr()) {
|
|
logger.warn("Health check failed", { checks: result.error });
|
|
}
|
|
}, HEALTH_INTERVAL);
|
|
```
|
|
|
|
## PID File Management
|
|
|
|
### XDG Paths
|
|
|
|
```typescript
|
|
import { getLockPath, getSocketPath, getLogPath } from "@outfitter/daemon";
|
|
|
|
// PID file: ~/.local/state/my-daemon/my-daemon.pid
|
|
const pidPath = getLockPath("my-daemon");
|
|
|
|
// Socket: ~/.local/state/my-daemon/my-daemon.sock
|
|
const socketPath = getSocketPath("my-daemon");
|
|
|
|
// Logs: ~/.local/state/my-daemon/logs/
|
|
const logDir = getLogPath("my-daemon");
|
|
```
|
|
|
|
### Checking if Running
|
|
|
|
```typescript
|
|
import { isDaemonRunning, getDaemonPid } from "@outfitter/daemon";
|
|
|
|
if (await isDaemonRunning("my-daemon")) {
|
|
const pid = await getDaemonPid("my-daemon");
|
|
console.log(`Daemon already running (PID: ${pid})`);
|
|
process.exit(1);
|
|
}
|
|
```
|
|
|
|
## CLI Integration
|
|
|
|
### Start Command
|
|
|
|
```typescript
|
|
export const startCommand = command("start")
|
|
.option("-d, --detach", "Run in background")
|
|
.action(async ({ flags }) => {
|
|
if (await isDaemonRunning("my-daemon")) {
|
|
console.log("Daemon already running");
|
|
return;
|
|
}
|
|
|
|
if (flags.detach) {
|
|
// Spawn detached process
|
|
spawn("bun", ["run", "src/daemon.ts"], {
|
|
detached: true,
|
|
stdio: "ignore",
|
|
}).unref();
|
|
console.log("Daemon started in background");
|
|
} else {
|
|
// Run in foreground
|
|
await runDaemon();
|
|
}
|
|
})
|
|
.build();
|
|
```
|
|
|
|
### Stop Command
|
|
|
|
```typescript
|
|
export const stopCommand = command("stop")
|
|
.action(async () => {
|
|
const client = createIpcClient(getSocketPath("my-daemon"));
|
|
|
|
try {
|
|
await client.connect();
|
|
await client.send({ type: "shutdown" });
|
|
console.log("Daemon stopped");
|
|
} catch {
|
|
console.log("Daemon not running");
|
|
} finally {
|
|
client.close();
|
|
}
|
|
})
|
|
.build();
|
|
```
|
|
|
|
### Status Command
|
|
|
|
```typescript
|
|
export const statusCommand = command("status")
|
|
.action(async () => {
|
|
const client = createIpcClient(getSocketPath("my-daemon"));
|
|
|
|
try {
|
|
await client.connect();
|
|
const status = await client.send<Status>({ type: "status" });
|
|
console.log("Status:", status);
|
|
} catch {
|
|
console.log("Daemon not running");
|
|
} finally {
|
|
client.close();
|
|
}
|
|
})
|
|
.build();
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
1. **Graceful shutdown** - Register cleanup handlers with `onShutdown`
|
|
2. **Health checks** - Monitor critical dependencies
|
|
3. **IPC protocol** - Use structured message types
|
|
4. **PID files** - Use XDG paths for consistency
|
|
5. **Logging** - Log lifecycle events for debugging
|
|
6. **CLI commands** - Provide start/stop/status commands
|