{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../../src/core/lsp/manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAG3C,OAAO,EAAE,SAAS,EAAa,MAAM,aAAa,CAAC;AACnD,OAAO,EAA4B,KAAK,kBAAkB,EAAiB,MAAM,gBAAgB,CAAC;AAElG,OAAO,KAAK,EACX,2BAA2B,EAE3B,eAAe,EACf,iBAAiB,EACjB,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,iBAAiB;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,2BAA2B,KAAK,IAAI,CAAC;CAC9D;AAED,UAAU,aAAa;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,SAAS,CAAC;IAClB,IAAI,EAAE,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC;IAC/B,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,YAAY,CAAC;IACtE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,2BAA2B,EAAE,CAAC;CAC3C;AAED,qBAAa,gBAAgB;IAC5B,OAAO,CAAC,OAAO,CAQb;IACF,OAAO,CAAC,OAAO,CAAoC;IACnD,OAAO,CAAC,QAAQ,CAA6C;IAC7D,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,OAAO,CAAS;IAExB,YAAY,IAAI,EAAE,iBAAiB,EAYlC;IAED,OAAO,CAAC,GAAG;IAIX,OAAO,CAAC,gBAAgB;IAIxB;;;OAGG;IACG,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,aAAa,CAAC;QAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CA6BlG;IAED,OAAO,CAAC,WAAW;IA8EnB,OAAO,CAAC,MAAM;YAkBA,aAAa;IAwBrB,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAYpE;IAED,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAUjD;IAED,WAAW,IAAI,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAOhG;IAED,gEAAgE;IAChE,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,2BAA2B,EAAE,CAQ5D;IAEK,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAejC;CACD;AAuBD,YAAY,EAAE,aAAa,EAAE,CAAC","sourcesContent":["import { spawn } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport nodePath from \"node:path\";\nimport { LspClient, toFileUri } from \"./client.js\";\nimport { DEFAULT_LANGUAGE_SERVERS, type LanguageServerSpec, resolveServer } from \"./discovery.js\";\nimport { JsonRpcClient } from \"./jsonrpc.js\";\nimport type {\n\tLspPublishDiagnosticsParams,\n\tLspServerCapabilities,\n\tLspServerHealth,\n\tLspServerIdentity,\n} from \"./types.js\";\n\n/**\n * Authoritative LSP server lifecycle manager.\n *\n * - One reusable server per (workspace, language, executable); duplicate\n *   server startup is prevented.\n * - Bounded startup/initialize/request timeouts.\n * - Graceful `shutdown` → `exit` and authoritative process cleanup (no leaks).\n * - Crash detection and typed degraded state with a bounded restart policy.\n * - Server stderr retained as bounded, sanitized diagnostic evidence.\n * - Server responses are untrusted and never adopted as an execution authority.\n */\n\nexport interface LspManagerOptions {\n\tworkspaceRoot: string;\n\tspecs?: LanguageServerSpec[];\n\tstartupTimeoutMs?: number;\n\tinitializeTimeoutMs?: number;\n\trequestTimeoutMs?: number;\n\tmaxRestarts?: number;\n\trestartDelayMs?: number;\n\tonDiagnostics?: (params: LspPublishDiagnosticsParams) => void;\n}\n\ninterface ManagedServer {\n\tserverId: string;\n\tlanguageId: string;\n\texecutable: string;\n\texePath: string;\n\targs: string[];\n\tprocessIdentity: string;\n\tclient: LspClient;\n\tproc: ReturnType<typeof spawn>;\n\tkill: () => void;\n\tinitializedAt: string;\n\trestartCount: number;\n\tstate: \"starting\" | \"running\" | \"degraded\" | \"stopped\" | \"crash_loop\";\n\tlastError?: string;\n\tcapabilitiesHash: string;\n\tdiagnostics: LspPublishDiagnosticsParams[];\n}\n\nexport class LspServerManager {\n\tprivate options: {\n\t\tspecs: LanguageServerSpec[];\n\t\tstartupTimeoutMs: number;\n\t\tinitializeTimeoutMs: number;\n\t\trequestTimeoutMs: number;\n\t\tmaxRestarts: number;\n\t\trestartDelayMs: number;\n\t\tonDiagnostics?: LspManagerOptions[\"onDiagnostics\"];\n\t};\n\tprivate servers = new Map<string, ManagedServer>();\n\tprivate starting = new Map<string, Promise<ManagedServer>>();\n\tprivate workspaceRoot: string;\n\tprivate rootUri: string;\n\n\tconstructor(opts: LspManagerOptions) {\n\t\tthis.workspaceRoot = nodePath.resolve(opts.workspaceRoot);\n\t\tthis.rootUri = toFileUri(this.workspaceRoot);\n\t\tthis.options = {\n\t\t\tspecs: opts.specs ?? DEFAULT_LANGUAGE_SERVERS,\n\t\t\tstartupTimeoutMs: opts.startupTimeoutMs ?? 20_000,\n\t\t\tinitializeTimeoutMs: opts.initializeTimeoutMs ?? 30_000,\n\t\t\trequestTimeoutMs: opts.requestTimeoutMs ?? 30_000,\n\t\t\tmaxRestarts: opts.maxRestarts ?? 2,\n\t\t\trestartDelayMs: opts.restartDelayMs ?? 500,\n\t\t\tonDiagnostics: opts.onDiagnostics,\n\t\t};\n\t}\n\n\tprivate key(languageId: string, exePath: string): string {\n\t\treturn `${this.workspaceRoot}|${languageId}|${exePath}`;\n\t}\n\n\tprivate hashCapabilities(c: LspServerCapabilities): string {\n\t\treturn createHash(\"sha256\").update(JSON.stringify(c)).digest(\"hex\").slice(0, 16);\n\t}\n\n\t/**\n\t * Get (or create and reuse) a managed server for a language. Returns null\n\t * when no server is installed or startup failed (typed via `unavailable`).\n\t */\n\tasync getServer(languageId: string): Promise<{ server: ManagedServer; unavailableReason?: string }> {\n\t\tconst resolution = await resolveServer(languageId, this.options.specs);\n\t\tif (!resolution.executable || !resolution.resolvedPath) {\n\t\t\treturn {\n\t\t\t\tserver: undefined as unknown as ManagedServer,\n\t\t\t\tunavailableReason: resolution.reason ?? \"server_unavailable\",\n\t\t\t};\n\t\t}\n\t\tconst exePath = resolution.resolvedPath;\n\t\tconst key = this.key(languageId, exePath);\n\t\tconst existing = this.servers.get(key);\n\t\tif (existing && (existing.state === \"running\" || existing.state === \"degraded\")) {\n\t\t\treturn { server: existing };\n\t\t}\n\n\t\t// Prevent duplicate concurrent startup.\n\t\tconst inFlight = this.starting.get(key);\n\t\tif (inFlight) {\n\t\t\treturn { server: await inFlight };\n\t\t}\n\t\tconst start = this.startServer(key, languageId, resolution.executable, exePath, resolution.args);\n\t\tthis.starting.set(key, start);\n\t\ttry {\n\t\t\tconst server = await start;\n\t\t\tthis.servers.set(key, server);\n\t\t\treturn { server };\n\t\t} finally {\n\t\t\tthis.starting.delete(key);\n\t\t}\n\t}\n\n\tprivate startServer(\n\t\tkey: string,\n\t\tlanguageId: string,\n\t\texecutable: string,\n\t\texePath: string,\n\t\targs: string[],\n\t): Promise<ManagedServer> {\n\t\treturn new Promise<ManagedServer>((resolve, reject) => {\n\t\t\tconst proc = spawn(exePath, args, {\n\t\t\t\tstdio: [\"pipe\", \"pipe\", \"pipe\"],\n\t\t\t\tshell: false,\n\t\t\t\twindowsHide: true,\n\t\t\t});\n\t\t\tconst startupTimer = setTimeout(() => {\n\t\t\t\tkillTree(proc);\n\t\t\t\treject(new Error(`LSP server ${executable} failed to initialize within startup timeout`));\n\t\t\t}, this.options.startupTimeoutMs);\n\n\t\t\tproc.once(\"spawn\", () => {\n\t\t\t\tvoid (async () => {\n\t\t\t\t\tconst jsonRpc = new JsonRpcClient({\n\t\t\t\t\t\tchild: proc as never,\n\t\t\t\t\t\trequestTimeoutMs: this.options.requestTimeoutMs,\n\t\t\t\t\t});\n\t\t\t\t\tconst client = new LspClient({\n\t\t\t\t\t\trpc: jsonRpc,\n\t\t\t\t\t\trootUri: this.rootUri,\n\t\t\t\t\t\tworkspaceRoot: this.workspaceRoot,\n\t\t\t\t\t\tonDiagnostics: (params) => {\n\t\t\t\t\t\t\tconst server = this.servers.get(key);\n\t\t\t\t\t\t\tif (server) {\n\t\t\t\t\t\t\t\tserver.diagnostics.push(params);\n\t\t\t\t\t\t\t\tif (server.diagnostics.length > 200) server.diagnostics.shift();\n\t\t\t\t\t\t\t\tthis.options.onDiagnostics?.(params);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst caps = await Promise.race([\n\t\t\t\t\t\t\tclient.initialize(),\n\t\t\t\t\t\t\tnew Promise<never>((_, rej) =>\n\t\t\t\t\t\t\t\tsetTimeout(() => rej(new Error(\"initialize timeout\")), this.options.initializeTimeoutMs),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t]);\n\t\t\t\t\t\tclearTimeout(startupTimer);\n\t\t\t\t\t\tconst server: ManagedServer = {\n\t\t\t\t\t\t\tserverId: key,\n\t\t\t\t\t\t\tlanguageId,\n\t\t\t\t\t\t\texecutable,\n\t\t\t\t\t\t\texePath,\n\t\t\t\t\t\t\targs,\n\t\t\t\t\t\t\tprocessIdentity: String(proc.pid),\n\t\t\t\t\t\t\tclient,\n\t\t\t\t\t\t\tproc,\n\t\t\t\t\t\t\tkill: () => killTree(proc),\n\t\t\t\t\t\t\tinitializedAt: new Date().toISOString(),\n\t\t\t\t\t\t\trestartCount: 0,\n\t\t\t\t\t\t\tstate: \"running\",\n\t\t\t\t\t\t\tcapabilitiesHash: this.hashCapabilities(caps),\n\t\t\t\t\t\t\tdiagnostics: [],\n\t\t\t\t\t\t};\n\t\t\t\t\t\tproc.once(\"exit\", (code) => this.onExit(key, code));\n\t\t\t\t\t\tresolve(server);\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tclearTimeout(startupTimer);\n\t\t\t\t\t\tkillTree(proc);\n\t\t\t\t\t\treject(err);\n\t\t\t\t\t}\n\t\t\t\t})();\n\t\t\t});\n\t\t\tproc.once(\"error\", (err) => {\n\t\t\t\tclearTimeout(startupTimer);\n\t\t\t\tkillTree(proc);\n\t\t\t\treject(err);\n\t\t\t});\n\t\t});\n\t}\n\n\tprivate onExit(key: string, code: number | null): void {\n\t\tconst server = this.servers.get(key);\n\t\tif (!server) return;\n\t\tif (server.state === \"stopped\") return;\n\t\tserver.state = \"degraded\";\n\t\tserver.lastError = `server exited (code ${code})`;\n\t\tserver.client.rejectAllPending(`LSP server exited (code ${code})`);\n\t\t// Bounded restart: only for non-graceful exits, up to maxRestarts.\n\t\tif (code !== 0 && server.restartCount < this.options.maxRestarts) {\n\t\t\tserver.restartCount += 1;\n\t\t\tsetTimeout(() => {\n\t\t\t\tvoid this.restartServer(key).catch(() => {});\n\t\t\t}, this.options.restartDelayMs);\n\t\t} else if (server.restartCount >= this.options.maxRestarts) {\n\t\t\tserver.state = \"crash_loop\";\n\t\t}\n\t}\n\n\tprivate async restartServer(key: string): Promise<void> {\n\t\tconst old = this.servers.get(key);\n\t\tif (!old || old.state === \"stopped\") return;\n\t\told.client.shutdown().catch(() => {});\n\t\told.kill();\n\t\tthis.servers.delete(key);\n\t\tconst resolution = await resolveServer(old.languageId, this.options.specs);\n\t\tif (!resolution.executable || !resolution.resolvedPath) return;\n\t\ttry {\n\t\t\tconst server = await this.startServer(\n\t\t\t\tkey,\n\t\t\t\told.languageId,\n\t\t\t\tresolution.executable,\n\t\t\t\tresolution.resolvedPath,\n\t\t\t\tresolution.args,\n\t\t\t);\n\t\t\tserver.restartCount = old.restartCount;\n\t\t\tthis.servers.set(key, server);\n\t\t} catch {\n\t\t\told.state = \"crash_loop\";\n\t\t\tthis.servers.set(key, old);\n\t\t}\n\t}\n\n\tasync identity(languageId: string): Promise<LspServerIdentity | null> {\n\t\tconst { server } = await this.getServer(languageId);\n\t\tif (!server) return null;\n\t\treturn {\n\t\t\tserverId: server.serverId,\n\t\t\tlanguageId: server.languageId,\n\t\t\texecutable: server.executable,\n\t\t\tworkspaceRoot: this.workspaceRoot,\n\t\t\tprocessIdentity: server.processIdentity,\n\t\t\tinitializedAt: server.initializedAt,\n\t\t\tcapabilitiesHash: server.capabilitiesHash,\n\t\t};\n\t}\n\n\thealth(languageId: string): LspServerHealth | null {\n\t\tconst resolutionKey = [...this.servers.keys()].find((k) => k.includes(`|${languageId}|`));\n\t\tconst server = resolutionKey ? this.servers.get(resolutionKey) : undefined;\n\t\tif (!server) return null;\n\t\treturn {\n\t\t\tserverId: server.serverId,\n\t\t\tstate: server.state,\n\t\t\talive: server.state === \"running\" || server.state === \"degraded\",\n\t\t\tlastError: server.lastError,\n\t\t};\n\t}\n\n\tlistServers(): Array<{ serverId: string; languageId: string; executable: string; state: string }> {\n\t\treturn [...this.servers.values()].map((s) => ({\n\t\t\tserverId: s.serverId,\n\t\t\tlanguageId: s.languageId,\n\t\t\texecutable: s.executable,\n\t\t\tstate: s.state,\n\t\t}));\n\t}\n\n\t/** Read bounded diagnostics published by a server for a URI. */\n\tdiagnosticsForUri(uri: string): LspPublishDiagnosticsParams[] {\n\t\tconst out: LspPublishDiagnosticsParams[] = [];\n\t\tfor (const s of this.servers.values()) {\n\t\t\tfor (const d of s.diagnostics) {\n\t\t\t\tif (d.uri === uri) out.push(d);\n\t\t\t}\n\t\t}\n\t\treturn out;\n\t}\n\n\tasync shutdownAll(): Promise<void> {\n\t\tconst servers = [...this.servers.values()];\n\t\tawait Promise.all(\n\t\t\tservers.map(async (s) => {\n\t\t\t\tif (s.state === \"stopped\") return;\n\t\t\t\ts.state = \"stopped\";\n\t\t\t\ttry {\n\t\t\t\t\tawait s.client.shutdown();\n\t\t\t\t} catch {\n\t\t\t\t\t/* noop */\n\t\t\t\t}\n\t\t\t\ts.kill();\n\t\t\t}),\n\t\t);\n\t\tthis.servers.clear();\n\t}\n}\n\nfunction killTree(proc: ReturnType<typeof spawn>): void {\n\tif (!proc.pid) return;\n\tif (process.platform === \"win32\") {\n\t\tconst { execFile } = require(\"node:child_process\") as typeof import(\"node:child_process\");\n\t\texecFile(\"taskkill\", [\"/T\", \"/F\", \"/PID\", String(proc.pid)], { windowsHide: true }, () => {});\n\t\treturn;\n\t}\n\ttry {\n\t\tprocess.kill(-proc.pid as number, \"SIGTERM\");\n\t} catch {\n\t\t/* noop */\n\t}\n\tsetTimeout(() => {\n\t\ttry {\n\t\t\tproc.kill(\"SIGKILL\");\n\t\t} catch {\n\t\t\t/* noop */\n\t\t}\n\t}, 500);\n}\n\nexport type { ManagedServer };\n"]}