9.4 KiB
9.4 KiB
Daemon Service Template
Background service with lifecycle management, IPC, and health checks.
Template
import {
createDaemon,
createIpcServer,
createHealthChecker,
getSocketPath,
getLockPath,
getLogPath,
} from "@outfitter/daemon";
import { createLogger, createConsoleSink, createFileSink } from "@outfitter/logging";
import { Result } from "@outfitter/contracts";
// ============================================================================
// Configuration
// ============================================================================
const DAEMON_NAME = "my-daemon";
const SHUTDOWN_TIMEOUT = 10000; // 10 seconds
const HEALTH_CHECK_INTERVAL = 30000; // 30 seconds
// ============================================================================
// Logger Setup
// ============================================================================
const logger = createLogger({
name: DAEMON_NAME,
level: process.env.LOG_LEVEL || "info",
sinks: [
createConsoleSink({ colorize: true }),
createFileSink({
path: `${getLogPath(DAEMON_NAME)}/daemon.log`,
maxSize: 10 * 1024 * 1024, // 10MB
maxFiles: 5,
}),
],
redaction: { enabled: true },
});
// ============================================================================
// Daemon Setup
// ============================================================================
const daemon = createDaemon({
name: DAEMON_NAME,
pidFile: getLockPath(DAEMON_NAME),
logger,
shutdownTimeout: SHUTDOWN_TIMEOUT,
});
// ============================================================================
// Health Checks
// ============================================================================
const healthChecker = createHealthChecker([
{
name: "memory",
check: async () => {
const used = process.memoryUsage().heapUsed / 1024 / 1024;
const threshold = 500; // MB
return used < threshold
? Result.ok(undefined)
: Result.err(new Error(`High memory usage: ${used.toFixed(2)}MB`));
},
},
{
name: "uptime",
check: async () => {
// Always healthy, just reports uptime
return Result.ok(undefined);
},
},
// Add more checks as needed:
// - Database connectivity
// - External API availability
// - Disk space
// - Queue depth
]);
// ============================================================================
// IPC Server
// ============================================================================
const ipcServer = createIpcServer(getSocketPath(DAEMON_NAME));
interface IpcMessage {
type: string;
payload?: unknown;
}
interface StatusResponse {
status: "ok" | "degraded" | "error";
uptime: number;
version: string;
pid: number;
}
interface HealthResponse {
healthy: boolean;
checks: Record<string, { ok: boolean; error?: string }>;
}
ipcServer.onMessage(async (msg): Promise<unknown> => {
const message = msg as IpcMessage;
switch (message.type) {
case "status":
return {
status: "ok",
uptime: process.uptime(),
version: "1.0.0",
pid: process.pid,
} satisfies StatusResponse;
case "health": {
const result = await healthChecker.check();
return {
healthy: result.isOk(),
checks: result.isOk() ? result.value : result.error,
} satisfies HealthResponse;
}
case "reload":
logger.info("Reloading configuration");
await reloadConfiguration();
return { success: true };
case "shutdown":
logger.info("Shutdown requested via IPC");
process.kill(process.pid, "SIGTERM");
return { success: true };
default:
return { error: `Unknown command: ${message.type}` };
}
});
// ============================================================================
// Lifecycle Hooks
// ============================================================================
daemon.onBeforeStart(async () => {
logger.info("Preparing to start daemon");
await initializeResources();
});
daemon.onAfterStart(async () => {
logger.info("Daemon started successfully", {
pid: process.pid,
socket: getSocketPath(DAEMON_NAME),
});
// Start periodic health checks
setInterval(async () => {
const result = await healthChecker.check();
if (result.isErr()) {
logger.warn("Health check failed", { checks: result.error });
}
}, HEALTH_CHECK_INTERVAL);
});
daemon.onShutdown(async () => {
logger.info("Shutting down daemon");
// Close IPC server
await ipcServer.close();
// Cleanup resources
await cleanupResources();
logger.info("Daemon shutdown complete");
});
// ============================================================================
// Main Entry Point
// ============================================================================
async function main() {
// Start daemon (handles PID file, signals)
const startResult = await daemon.start();
if (startResult.isErr()) {
logger.error("Failed to start daemon", { error: startResult.error });
process.exit(1);
}
// Start IPC server
await ipcServer.listen();
logger.info("IPC server listening", { socket: getSocketPath(DAEMON_NAME) });
// Start main work loop
await runMainLoop();
}
// ============================================================================
// Application Logic
// ============================================================================
async function initializeResources(): Promise<void> {
// Initialize database connections, caches, etc.
}
async function cleanupResources(): Promise<void> {
// Close connections, flush buffers, etc.
}
async function reloadConfiguration(): Promise<void> {
// Reload configuration without restart
}
async function runMainLoop(): Promise<void> {
// Main daemon work loop
while (!daemon.isShuttingDown) {
// Do work
await processNextItem();
await Bun.sleep(1000);
}
}
async function processNextItem(): Promise<void> {
// Process one unit of work
}
// ============================================================================
// Start
// ============================================================================
main().catch((error) => {
logger.fatal("Unhandled error", { error });
process.exit(1);
});
CLI Commands
import { command } from "@outfitter/cli";
import {
createIpcClient,
getSocketPath,
isDaemonRunning,
} from "@outfitter/daemon";
import { spawn } from "child_process";
const DAEMON_NAME = "my-daemon";
// Start command
export const startCommand = command("start")
.description("Start the daemon")
.option("-d, --detach", "Run in background")
.action(async ({ flags }) => {
if (await isDaemonRunning(DAEMON_NAME)) {
console.log("Daemon is already running");
return;
}
if (flags.detach) {
spawn("bun", ["run", "src/daemon.ts"], {
detached: true,
stdio: "ignore",
}).unref();
console.log("Daemon started in background");
} else {
// Import and run directly
await import("./daemon.js");
}
})
.build();
// Stop command
export const stopCommand = command("stop")
.description("Stop the daemon")
.action(async () => {
const client = createIpcClient(getSocketPath(DAEMON_NAME));
try {
await client.connect();
await client.send({ type: "shutdown" });
console.log("Daemon stopping");
} catch {
console.log("Daemon is not running");
} finally {
client.close();
}
})
.build();
// Status command
export const statusCommand = command("status")
.description("Check daemon status")
.action(async () => {
const client = createIpcClient(getSocketPath(DAEMON_NAME));
try {
await client.connect();
const status = await client.send<{
status: string;
uptime: number;
pid: number;
}>({ type: "status" });
console.log(`Status: ${status.status}`);
console.log(`PID: ${status.pid}`);
console.log(`Uptime: ${Math.floor(status.uptime)}s`);
} catch {
console.log("Daemon is not running");
} finally {
client.close();
}
})
.build();
// Health command
export const healthCommand = command("health")
.description("Check daemon health")
.action(async () => {
const client = createIpcClient(getSocketPath(DAEMON_NAME));
try {
await client.connect();
const health = await client.send<{
healthy: boolean;
checks: Record<string, { ok: boolean; error?: string }>;
}>({ type: "health" });
console.log(`Healthy: ${health.healthy}`);
for (const [name, check] of Object.entries(health.checks)) {
const status = check.ok ? "✓" : "✗";
const message = check.error ? ` (${check.error})` : "";
console.log(` ${status} ${name}${message}`);
}
} catch {
console.log("Daemon is not running");
} finally {
client.close();
}
})
.build();
Checklist
- Graceful shutdown with
onShutdownhook - PID file in XDG state directory
- IPC socket for control commands
- Health checks for critical dependencies
- Structured logging with redaction
- CLI commands for start/stop/status/health
XDG Paths
| Function | Path | Example |
|---|---|---|
getLockPath(name) |
~/.local/state/{name}/{name}.pid |
~/.local/state/my-daemon/my-daemon.pid |
getSocketPath(name) |
~/.local/state/{name}/{name}.sock |
~/.local/state/my-daemon/my-daemon.sock |
getLogPath(name) |
~/.local/state/{name}/logs/ |
~/.local/state/my-daemon/logs/ |