import { spawn, type ChildProcess } from 'node:child_process'; import { join } from 'node:path'; import { existsSync } from 'node:fs'; import { LspClient } from './client.js'; import { getServerConfig } from './config.js'; const IDLE_TIMEOUT_MS = 5 * 60 * 1000; const SWEEP_INTERVAL_MS = 30_000; interface LspInstance { client: LspClient; proc: ChildProcess; lastUsed: number; timer: ReturnType; } export class LspServerManager { private instances = new Map(); private sweepTimer: ReturnType | null = null; constructor() { this.startSweeper(); } private startSweeper(): void { this.sweepTimer = setInterval(() => this.sweep(), SWEEP_INTERVAL_MS); this.sweepTimer.unref?.(); } private findProjectRoot(filePath: string): string | null { let dir = filePath; const config = getServerConfig(filePath); if (!config) return null; while (true) { for (const pattern of config.rootPatterns) { if (existsSync(join(dir, pattern))) return dir; } const parent = join(dir, '..'); if (parent === dir) return dir; dir = parent; } } async getClient(filePath: string): Promise { const config = getServerConfig(filePath); if (!config) return null; const projectRoot = this.findProjectRoot(filePath); if (!projectRoot) return null; const existing = this.instances.get(projectRoot); if (existing) { existing.lastUsed = Date.now(); clearTimeout(existing.timer); existing.timer = setTimeout(() => this.kill(projectRoot), IDLE_TIMEOUT_MS); existing.timer.unref?.(); return existing.client; } return this.spawn(projectRoot, config.command, config.args); } private async spawn(projectRoot: string, command: string, args: string[]): Promise { const proc = spawn(command, args, { stdio: ['pipe', 'pipe', 'pipe'], cwd: projectRoot }); const client = new LspClient(proc.stdin!, proc.stdout!); await client.request('initialize', { processId: process.pid, rootUri: `file://${projectRoot}`, capabilities: { textDocument: { diagnostic: { dynamicRegistration: false }, definition: { dynamicRegistration: false }, references: { dynamicRegistration: false }, }, }, }); await client.notify('initialized', {}); const timer = setTimeout(() => this.kill(projectRoot), IDLE_TIMEOUT_MS); timer.unref?.(); this.instances.set(projectRoot, { client, proc, lastUsed: Date.now(), timer }); proc.on('exit', () => this.instances.delete(projectRoot)); return client; } private kill(projectRoot: string): void { const inst = this.instances.get(projectRoot); if (!inst) return; this.instances.delete(projectRoot); inst.proc.kill('SIGTERM'); setTimeout(() => { if (inst.proc.exitCode === null) inst.proc.kill('SIGKILL'); }, 5000); } private sweep(): void { const now = Date.now(); for (const [root, inst] of this.instances) { if (now - inst.lastUsed > IDLE_TIMEOUT_MS) { this.kill(root); } } } shutdown(): void { if (this.sweepTimer) clearInterval(this.sweepTimer); for (const root of [...this.instances.keys()]) { this.kill(root); } } getActiveCount(): number { return this.instances.size; } } export const lspManager = new LspServerManager();