



























@@ -3,11 +3,13 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core";
33import type { OpenClawConfig } from "../config/types.openclaw.js";
44import { logDebug, logWarn } from "../logger.js";
55import { setPluginToolMeta } from "../plugins/tools.js";
6+import { killProcessTree } from "../process/kill-tree.js";
67import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
78import { loadEmbeddedPiLspConfig } from "./embedded-pi-lsp.js";
89import {
910resolveStdioMcpServerLaunchConfig,
1011describeStdioMcpServerLaunchConfig,
12+type StdioMcpServerLaunchConfig,
1113} from "./mcp-stdio.js";
1214import type { AnyAgentTool } from "./tools/common.js";
1315@@ -17,10 +19,17 @@ type LspSession = {
1719serverName: string;
1820process: ChildProcess;
1921requestId: number;
20-pendingRequests: Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void }>;
22+pendingRequests: Map<number, PendingLspRequest>;
2123buffer: string;
2224initialized: boolean;
2325capabilities: LspServerCapabilities;
26+disposed: boolean;
27+};
28+29+type PendingLspRequest = {
30+resolve: (v: unknown) => void;
31+reject: (e: Error) => void;
32+timeout: ReturnType<typeof setTimeout>;
2433};
25342635type LspServerCapabilities = {
@@ -44,6 +53,55 @@ type LspPositionParams = {
4453character: number;
4554};
465556+const LSP_SHUTDOWN_GRACE_MS = 500;
57+const LSP_PROCESS_TREE_KILL_GRACE_MS = 1_000;
58+const activeBundleLspSessions = new Set<LspSession>();
59+60+function delay(ms: number): Promise<void> {
61+return new Promise((resolve) => {
62+const timeout = setTimeout(resolve, Math.max(1, ms));
63+timeout.unref?.();
64+});
65+}
66+67+function spawnLspServerProcess(config: StdioMcpServerLaunchConfig): ChildProcess {
68+return spawn(config.command, config.args ?? [], {
69+stdio: ["pipe", "pipe", "pipe"],
70+env: { ...process.env, ...config.env },
71+cwd: config.cwd,
72+detached: process.platform !== "win32",
73+windowsHide: process.platform === "win32",
74+});
75+}
76+77+function createLspSession(serverName: string, child: ChildProcess): LspSession {
78+return {
79+ serverName,
80+process: child,
81+requestId: 0,
82+pendingRequests: new Map(),
83+buffer: "",
84+initialized: false,
85+capabilities: {},
86+disposed: false,
87+};
88+}
89+90+function registerActiveLspSession(session: LspSession): void {
91+activeBundleLspSessions.add(session);
92+}
93+94+function attachLspProcessHandlers(session: LspSession): void {
95+session.process.stdout?.setEncoding("utf-8");
96+session.process.stdout?.on("data", (chunk: string) => handleIncomingData(session, chunk));
97+session.process.stderr?.setEncoding("utf-8");
98+session.process.stderr?.on("data", (chunk: string) => {
99+for (const line of chunk.split(/\r?\n/).filter(Boolean)) {
100+logDebug(`bundle-lsp:${session.serverName}: ${line.trim()}`);
101+}
102+});
103+}
104+47105function encodeLspMessage(body: unknown): string {
48106const json = JSON.stringify(body);
49107return `Content-Length: ${Buffer.byteLength(json, "utf-8")}\r\n\r\n${json}`;
@@ -89,18 +147,17 @@ function parseLspMessages(buffer: string): { messages: unknown[]; remaining: str
89147function sendRequest(session: LspSession, method: string, params?: unknown): Promise<unknown> {
90148const id = ++session.requestId;
91149return new Promise((resolve, reject) => {
92-session.pendingRequests.set(id, { resolve, reject });
93-const message = { jsonrpc: "2.0", id, method, params };
94-const encoded = encodeLspMessage(message);
95-session.process.stdin?.write(encoded, "utf-8");
96-97-// Timeout after 10 seconds
98-setTimeout(() => {
150+const timeout = setTimeout(() => {
99151if (session.pendingRequests.has(id)) {
100152session.pendingRequests.delete(id);
101153reject(new Error(`LSP request ${method} timed out`));
102154}
103155}, 10_000);
156+timeout.unref?.();
157+session.pendingRequests.set(id, { resolve, reject, timeout });
158+const message = { jsonrpc: "2.0", id, method, params };
159+const encoded = encodeLspMessage(message);
160+session.process.stdin?.write(encoded, "utf-8");
104161});
105162}
106163@@ -119,6 +176,7 @@ function handleIncomingData(session: LspSession, chunk: string) {
119176const pending = session.pendingRequests.get(record.id);
120177if (pending) {
121178session.pendingRequests.delete(record.id);
179+clearTimeout(pending.timeout);
122180if ("error" in record) {
123181pending.reject(new Error(JSON.stringify(record.error)));
124182} else {
@@ -157,10 +215,32 @@ async function initializeSession(session: LspSession): Promise<LspServerCapabili
157215return result?.capabilities ?? {};
158216}
159217218+function hasLspProcessExited(child: ChildProcess): boolean {
219+return child.exitCode !== null || child.signalCode !== null;
220+}
221+222+function terminateLspProcessTree(session: LspSession): void {
223+const pid = session.process.pid;
224+if (pid && !hasLspProcessExited(session.process)) {
225+killProcessTree(pid, { graceMs: LSP_PROCESS_TREE_KILL_GRACE_MS });
226+return;
227+}
228+if (!hasLspProcessExited(session.process)) {
229+session.process.kill("SIGTERM");
230+}
231+}
232+160233async function disposeSession(session: LspSession) {
234+if (session.disposed) {
235+return;
236+}
237+session.disposed = true;
238+activeBundleLspSessions.delete(session);
239+161240if (session.initialized) {
162241try {
163-await sendRequest(session, "shutdown").catch(() => {});
242+const shutdown = sendRequest(session, "shutdown").catch(() => undefined);
243+await Promise.race([shutdown, delay(LSP_SHUTDOWN_GRACE_MS)]);
164244session.process.stdin?.write(
165245encodeLspMessage({ jsonrpc: "2.0", method: "exit", params: null }),
166246"utf-8",
@@ -170,10 +250,15 @@ async function disposeSession(session: LspSession) {
170250}
171251}
172252for (const [, pending] of session.pendingRequests) {
253+clearTimeout(pending.timeout);
173254pending.reject(new Error("LSP session disposed"));
174255}
175256session.pendingRequests.clear();
176-session.process.kill();
257+terminateLspProcessTree(session);
258+}
259+260+async function disposeSessions(sessions: Iterable<LspSession>): Promise<void> {
261+await Promise.allSettled(Array.from(sessions, (session) => disposeSession(session)));
177262}
178263179264function createLspPositionTool(params: {
@@ -325,32 +410,12 @@ export async function createBundleLspToolRuntime(params: {
325410continue;
326411}
327412const launchConfig = launch.config;
413+let session: LspSession | undefined;
328414329415try {
330-const child = spawn(launchConfig.command, launchConfig.args ?? [], {
331-stdio: ["pipe", "pipe", "pipe"],
332-env: { ...process.env, ...launchConfig.env },
333-cwd: launchConfig.cwd,
334-});
335-336-const session: LspSession = {
337- serverName,
338-process: child,
339-requestId: 0,
340-pendingRequests: new Map(),
341-buffer: "",
342-initialized: false,
343-capabilities: {},
344-};
345-346-child.stdout?.setEncoding("utf-8");
347-child.stdout?.on("data", (chunk: string) => handleIncomingData(session, chunk));
348-child.stderr?.setEncoding("utf-8");
349-child.stderr?.on("data", (chunk: string) => {
350-for (const line of chunk.split(/\r?\n/).filter(Boolean)) {
351-logDebug(`bundle-lsp:${serverName}: ${line.trim()}`);
352-}
353-});
416+session = createLspSession(serverName, spawnLspServerProcess(launchConfig));
417+registerActiveLspSession(session);
418+attachLspProcessHandlers(session);
354419355420const capabilities = await initializeSession(session);
356421session.capabilities = capabilities;
@@ -380,6 +445,9 @@ export async function createBundleLspToolRuntime(params: {
380445`bundle-lsp: started "${serverName}" (${describeStdioMcpServerLaunchConfig(launchConfig)}) with ${serverTools.length} tools`,
381446);
382447} catch (error) {
448+if (session) {
449+await disposeSession(session);
450+}
383451logWarn(
384452`bundle-lsp: failed to start server "${serverName}" (${describeStdioMcpServerLaunchConfig(launchConfig)}): ${String(error)}`,
385453);
@@ -393,11 +461,15 @@ export async function createBundleLspToolRuntime(params: {
393461capabilities: s.capabilities,
394462})),
395463dispose: async () => {
396-await Promise.allSettled(sessions.map((session) => disposeSession(session)));
464+await disposeSessions(sessions);
397465},
398466};
399467} catch (error) {
400-await Promise.allSettled(sessions.map((session) => disposeSession(session)));
468+await disposeSessions(sessions);
401469throw error;
402470}
403471}
472+473+export async function disposeAllBundleLspRuntimes(): Promise<void> {
474+await disposeSessions(activeBundleLspSessions);
475+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。