Restart a crashed language server per --restart never|always|N
The process is supervised: on exit after a successful initialize it is spawned again with exponential backoff (500ms to 30s), open documents are re-sent as didOpen before the new session is announced, and requests caught by the crash wait for it and retry. A crash more than a minute after the previous one starts a fresh streak; reaching the streak limit (default 3) ends restarting for good, after which every request fails with an LspError saying so. Restarts and give-ups are logged and forwarded to the MCP client. Writes to the child's stdin go through a PassThrough so a broken pipe ends the session instead of surfacing as an unhandled rejection.
This commit is contained in:
6 files changed
+441
-146
No files matched your search
+37
-1
@@ -1,7 +1,7 @@
|
||||
import { NodeServices } from "@effect/platform-node";
|
||||
import { assert, describe, it } from "@effect/vitest";
|
||||
import { Effect, FileSystem, Layer } from "effect";
|
||||
import { RequestType } from "vscode-languageserver-protocol";
|
||||
import { NotificationType, RequestType } from "vscode-languageserver-protocol";
|
||||
import { Diagnostics } from "../src/lsp/Diagnostics.ts";
|
||||
import { Documents } from "../src/lsp/Documents.ts";
|
||||
import * as Editor from "../src/lsp/Editor.ts";
|
||||
@@ -12,6 +12,7 @@ const State = new RequestType<null, { open: number; closes: number }, void>(
|
||||
"fixture/state",
|
||||
);
|
||||
const Progress = new RequestType<number, string, void>("fixture/progress");
|
||||
const Crash = new NotificationType<null>("fixture/crash");
|
||||
|
||||
const TestLayer = Layer.mergeAll(
|
||||
Documents.layer({ capacity: 2, languages: {} }),
|
||||
@@ -168,4 +169,39 @@ describe("Editor", () => {
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.layer(
|
||||
Layer.mergeAll(
|
||||
Documents.layer({ capacity: 2, languages: {} }),
|
||||
Diagnostics.layer,
|
||||
).pipe(
|
||||
Layer.provideMerge(
|
||||
workspace({ "a.ts": "const alpha = 1\n" }, { restart: 1 }),
|
||||
),
|
||||
Layer.provideMerge(NodeServices.layer),
|
||||
),
|
||||
{ excludeTestServices: true },
|
||||
)("when the server crashes", (it) => {
|
||||
it.effect(
|
||||
"starts it again with the same documents open, until the policy gives up",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const lsp = yield* LanguageServer;
|
||||
const hover = Editor.hover({
|
||||
file: "a.ts",
|
||||
line: 1,
|
||||
symbol: "alpha",
|
||||
});
|
||||
yield* hover;
|
||||
|
||||
yield* lsp.notify(Crash, null);
|
||||
assert.strictEqual((yield* hover)?.contents, "word **alpha**");
|
||||
assert.strictEqual((yield* lsp.request(State, null)).open, 1);
|
||||
|
||||
yield* lsp.notify(Crash, null);
|
||||
const error = yield* Effect.flip(hover);
|
||||
assert.match(error.message, /1 crash in a row/);
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
Vendored
+3
-1
@@ -2,7 +2,8 @@
|
||||
* A miniature language server used by the tests. Words are symbols: hover
|
||||
* describes the word under the cursor, definition is its first occurrence,
|
||||
* references and rename cover every occurrence, and each line containing
|
||||
* "TODO" yields a warning. `fixture/state` reports open/close counts.
|
||||
* "TODO" yields a warning. `fixture/state` reports open/close counts and
|
||||
* `fixture/crash` kills the process.
|
||||
*/
|
||||
import {
|
||||
createConnection,
|
||||
@@ -136,6 +137,7 @@ connection.onDocumentSymbol(({ textDocument }) => {
|
||||
);
|
||||
});
|
||||
|
||||
connection.onNotification("fixture/crash", () => process.exit(1));
|
||||
connection.onRequest("fixture/state", () => ({
|
||||
open: documents.all().length,
|
||||
closes,
|
||||
|
||||
Vendored
+6
-2
@@ -1,11 +1,14 @@
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { Effect, FileSystem, Layer, Path } from "effect";
|
||||
import { LanguageServer } from "../../src/lsp/LanguageServer.ts";
|
||||
import { LanguageServer, type Options } from "../../src/lsp/LanguageServer.ts";
|
||||
|
||||
const server = fileURLToPath(new URL("./server.ts", import.meta.url));
|
||||
|
||||
/** A temporary workspace with the given files, served by the fixture language server. */
|
||||
export const workspace = (files: Record<string, string>) =>
|
||||
export const workspace = (
|
||||
files: Record<string, string>,
|
||||
options?: Pick<Options, "restart">,
|
||||
) =>
|
||||
Layer.unwrap(
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* FileSystem.FileSystem;
|
||||
@@ -18,6 +21,7 @@ export const workspace = (files: Record<string, string>) =>
|
||||
command: process.execPath,
|
||||
args: [server],
|
||||
root,
|
||||
...options,
|
||||
});
|
||||
}),
|
||||
);
|
||||
Reference in new issue
Block a user