# Daemon Service Template Background service with lifecycle management, IPC, and health checks. ## Template ```typescript 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; } ipcServer.onMessage(async (msg): Promise => { 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 { // Initialize database connections, caches, etc. } async function cleanupResources(): Promise { // Close connections, flush buffers, etc. } async function reloadConfiguration(): Promise { // Reload configuration without restart } async function runMainLoop(): Promise { // Main daemon work loop while (!daemon.isShuttingDown) { // Do work await processNextItem(); await Bun.sleep(1000); } } async function processNextItem(): Promise { // Process one unit of work } // ============================================================================ // Start // ============================================================================ main().catch((error) => { logger.fatal("Unhandled error", { error }); process.exit(1); }); ``` ## CLI Commands ```typescript 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; }>({ 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 `onShutdown` hook - [ ] 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/` |