From 41b0cbcc9cc5472d64f9507dc95230abec9037a8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 3 Mar 2026 00:37:40 +0000 Subject: [PATCH] refactor(daemon): extract host support utilities --- src/daemon/config-layers.ts | 23 +++++ src/daemon/host.ts | 187 ++++-------------------------------- src/daemon/log-context.ts | 78 +++++++++++++++ src/daemon/request-utils.ts | 67 +++++++++++++ 4 files changed, 189 insertions(+), 166 deletions(-) create mode 100644 src/daemon/config-layers.ts create mode 100644 src/daemon/log-context.ts create mode 100644 src/daemon/request-utils.ts diff --git a/src/daemon/config-layers.ts b/src/daemon/config-layers.ts new file mode 100644 index 0000000..f2aefed --- /dev/null +++ b/src/daemon/config-layers.ts @@ -0,0 +1,23 @@ +import fs from 'node:fs/promises'; +import type { LoadConfigOptions } from '../config.js'; +import { listConfigLayerPaths } from '../config.js'; + +export async function statConfigMtime(configPath: string): Promise { + try { + const stats = await fs.stat(configPath); + return stats.mtimeMs; + } catch { + return null; + } +} + +export async function collectConfigLayers( + options: LoadConfigOptions +): Promise> { + const layerPaths = await listConfigLayerPaths(options, options.rootDir ?? process.cwd()); + const entries: Array<{ path: string; mtimeMs: number | null }> = []; + for (const layerPath of layerPaths) { + entries.push({ path: layerPath, mtimeMs: await statConfigMtime(layerPath) }); + } + return entries; +} diff --git a/src/daemon/host.ts b/src/daemon/host.ts index 07f2144..3b19c48 100644 --- a/src/daemon/host.ts +++ b/src/daemon/host.ts @@ -1,11 +1,18 @@ -import fsSync from 'node:fs'; import fs from 'node:fs/promises'; import net from 'node:net'; import path from 'node:path'; import type { ServerDefinition } from '../config.js'; -import { listConfigLayerPaths } from '../config.js'; -import { isKeepAliveServer, keepAliveIdleTimeout } from '../lifecycle.js'; +import { isKeepAliveServer } from '../lifecycle.js'; import { createRuntime, type Runtime } from '../runtime.js'; +import { collectConfigLayers, statConfigMtime } from './config-layers.js'; +import { + createLogContext, + disposeLogContext, + formatError, + type LogContext, + logEvent, + shouldLogServer, +} from './log-context.js'; import type { CallToolParams, CloseServerParams, @@ -15,6 +22,13 @@ import type { ListToolsParams, StatusResult, } from './protocol.js'; +import { + buildErrorResponse, + ensureManaged, + evictIdleServers, + markActivity, + type ServerActivity, +} from './request-utils.js'; interface DaemonHostOptions { readonly socketPath: string; @@ -27,13 +41,11 @@ interface DaemonHostOptions { readonly logAllServers?: boolean; } -interface ServerActivity { - connected: boolean; - lastUsedAt?: number; -} - export async function runDaemonHost(options: DaemonHostOptions): Promise { - const configLayers = await collectConfigLayers(options); + const configLayers = await collectConfigLayers({ + configPath: options.configExplicit ? options.configPath : undefined, + rootDir: options.rootDir, + }); const runtime = await createRuntime({ configPath: options.configExplicit ? options.configPath : undefined, rootDir: options.rootDir, @@ -210,29 +222,6 @@ async function cleanupArtifacts(options: DaemonHostOptions): Promise { } } -async function statConfigMtime(configPath: string): Promise { - try { - const stats = await fs.stat(configPath); - return stats.mtimeMs; - } catch { - return null; - } -} - -async function collectConfigLayers( - options: DaemonHostOptions -): Promise> { - const layerPaths = await listConfigLayerPaths( - options.configExplicit ? { configPath: options.configPath } : {}, - options.rootDir ?? process.cwd() - ); - const entries: Array<{ path: string; mtimeMs: number | null }> = []; - for (const layerPath of layerPaths) { - entries.push({ path: layerPath, mtimeMs: await statConfigMtime(layerPath) }); - } - return entries; -} - async function handleSocketRequest( rawPayload: string, socket: net.Socket, @@ -446,140 +435,6 @@ async function processRequest( } } -function ensureManaged(server: string, managedServers: Map): void { - if (!managedServers.has(server)) { - throw new Error(`Server '${server}' is not managed by the daemon.`); - } -} - -function markActivity(server: string, activity: Map): void { - const entry = activity.get(server); - if (entry) { - entry.connected = true; - entry.lastUsedAt = Date.now(); - } else { - activity.set(server, { connected: true, lastUsedAt: Date.now() }); - } -} - -async function evictIdleServers( - runtime: Runtime, - managedServers: Map, - activity: Map -): Promise { - const now = Date.now(); - await Promise.all( - Array.from(managedServers.entries()).map(async ([name, definition]) => { - const timeout = keepAliveIdleTimeout(definition); - if (!timeout) { - return; - } - const entry = activity.get(name); - if (!entry?.lastUsedAt) { - return; - } - if (now - entry.lastUsedAt < timeout) { - return; - } - await runtime.close(name).catch(() => {}); - activity.set(name, { connected: false }); - }) - ); -} - -function buildErrorResponse(id: string, code: string, error?: unknown): DaemonResponse { - let message = code; - if (error instanceof Error) { - message = error.message; - } else if (typeof error === 'string') { - message = error; - } - return { - id, - ok: false, - error: { - code, - message, - }, - }; -} - -interface LogContext { - enabled: boolean; - logAllServers: boolean; - servers: Set; - writer?: fsSync.WriteStream; -} - -function createLogContext(options: { - enabled: boolean; - logAllServers: boolean; - servers: Set; - logPath?: string; -}): LogContext { - const derivedEnabled = options.enabled || options.logAllServers || options.servers.size > 0; - const context: LogContext = { - enabled: derivedEnabled, - logAllServers: options.logAllServers, - servers: options.servers, - }; - if (derivedEnabled && options.logPath) { - try { - fsSync.mkdirSync(path.dirname(options.logPath), { recursive: true }); - context.writer = fsSync.createWriteStream(options.logPath, { - flags: 'a', - }); - } catch (error) { - console.warn(`[daemon] Failed to open log file ${options.logPath}: ${(error as Error).message}`); - } - } - return context; -} - -function logEvent(context: LogContext, message: string): void { - if (!context.enabled) { - return; - } - const line = `[daemon] ${new Date().toISOString()} ${message}`; - console.log(line); - try { - context.writer?.write(`${line}\n`); - } catch { - // ignore file write failures - } -} - -async function disposeLogContext(context: LogContext): Promise { - const writer = context.writer; - if (!writer) { - return; - } - await new Promise((resolve) => { - writer.end(() => resolve()); - writer.on('error', () => resolve()); - }); -} - -function shouldLogServer(context: LogContext, server: string): boolean { - if (!context.enabled) { - return false; - } - if (context.logAllServers) { - return true; - } - return context.servers.has(server); -} - -function formatError(error: unknown): string { - if (error instanceof Error) { - return error.message; - } - if (typeof error === 'string') { - return error; - } - return 'unknown'; -} - export async function __testProcessRequest( rawPayload: string, runtime: Runtime, diff --git a/src/daemon/log-context.ts b/src/daemon/log-context.ts new file mode 100644 index 0000000..8f4731c --- /dev/null +++ b/src/daemon/log-context.ts @@ -0,0 +1,78 @@ +import fsSync from 'node:fs'; +import path from 'node:path'; + +export interface LogContext { + enabled: boolean; + logAllServers: boolean; + servers: Set; + writer?: fsSync.WriteStream; +} + +export function createLogContext(options: { + enabled: boolean; + logAllServers: boolean; + servers: Set; + logPath?: string; +}): LogContext { + const derivedEnabled = options.enabled || options.logAllServers || options.servers.size > 0; + const context: LogContext = { + enabled: derivedEnabled, + logAllServers: options.logAllServers, + servers: options.servers, + }; + if (derivedEnabled && options.logPath) { + try { + fsSync.mkdirSync(path.dirname(options.logPath), { recursive: true }); + context.writer = fsSync.createWriteStream(options.logPath, { + flags: 'a', + }); + } catch (error) { + console.warn(`[daemon] Failed to open log file ${options.logPath}: ${(error as Error).message}`); + } + } + return context; +} + +export function logEvent(context: LogContext, message: string): void { + if (!context.enabled) { + return; + } + const line = `[daemon] ${new Date().toISOString()} ${message}`; + console.log(line); + try { + context.writer?.write(`${line}\n`); + } catch { + // ignore file write failures + } +} + +export async function disposeLogContext(context: LogContext): Promise { + const writer = context.writer; + if (!writer) { + return; + } + await new Promise((resolve) => { + writer.end(() => resolve()); + writer.on('error', () => resolve()); + }); +} + +export function shouldLogServer(context: LogContext, server: string): boolean { + if (!context.enabled) { + return false; + } + if (context.logAllServers) { + return true; + } + return context.servers.has(server); +} + +export function formatError(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + if (typeof error === 'string') { + return error; + } + return 'unknown'; +} diff --git a/src/daemon/request-utils.ts b/src/daemon/request-utils.ts new file mode 100644 index 0000000..552312a --- /dev/null +++ b/src/daemon/request-utils.ts @@ -0,0 +1,67 @@ +import type { ServerDefinition } from '../config.js'; +import { keepAliveIdleTimeout } from '../lifecycle.js'; +import type { Runtime } from '../runtime.js'; +import type { DaemonResponse } from './protocol.js'; + +export interface ServerActivity { + connected: boolean; + lastUsedAt?: number; +} + +export function ensureManaged(server: string, managedServers: Map): void { + if (!managedServers.has(server)) { + throw new Error(`Server '${server}' is not managed by the daemon.`); + } +} + +export function markActivity(server: string, activity: Map): void { + const entry = activity.get(server); + if (entry) { + entry.connected = true; + entry.lastUsedAt = Date.now(); + } else { + activity.set(server, { connected: true, lastUsedAt: Date.now() }); + } +} + +export async function evictIdleServers( + runtime: Runtime, + managedServers: Map, + activity: Map +): Promise { + const now = Date.now(); + await Promise.all( + Array.from(managedServers.entries()).map(async ([name, definition]) => { + const timeout = keepAliveIdleTimeout(definition); + if (!timeout) { + return; + } + const entry = activity.get(name); + if (!entry?.lastUsedAt) { + return; + } + if (now - entry.lastUsedAt < timeout) { + return; + } + await runtime.close(name).catch(() => {}); + activity.set(name, { connected: false }); + }) + ); +} + +export function buildErrorResponse(id: string, code: string, error?: unknown): DaemonResponse { + let message = code; + if (error instanceof Error) { + message = error.message; + } else if (typeof error === 'string') { + message = error; + } + return { + id, + ok: false, + error: { + code, + message, + }, + }; +}