breakpoint-mcp 1.78.3 โ 1.80.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bridge.js +89 -5
- package/dist/config.js +13 -0
- package/dist/csdap.js +65 -22
- package/dist/cslsp.js +13 -8
- package/dist/dap.js +180 -21
- package/dist/exit-cause.js +13 -7
- package/dist/index.js +1 -1
- package/dist/lsp.js +28 -9
- package/dist/readiness.js +26 -7
- package/dist/schemas.js +10 -2
- package/dist/timeout-cause.js +66 -0
- package/dist/timeout-caveat.js +15 -4
- package/dist/tools/csdap.js +31 -9
- package/dist/tools/dap.js +40 -6
- package/dist/tools/editor/common.js +7 -2
- package/dist/tools/lsp-common.js +6 -3
- package/dist/tools/processes.js +6 -1
- package/package.json +3 -2
package/dist/bridge.js
CHANGED
|
@@ -65,6 +65,68 @@ export function remedyClause(err) {
|
|
|
65
65
|
const r = err?.remedy;
|
|
66
66
|
return typeof r === "string" && r !== "" ? ` โ ${r}` : "";
|
|
67
67
|
}
|
|
68
|
+
/** The code this class raises when a bridge request outlives its own deadline. */
|
|
69
|
+
export const BRIDGE_TIMEOUT_CODE = "timeout";
|
|
70
|
+
/**
|
|
71
|
+
* The label `tools/editor/common.ts`'s `fail()` puts in front of a bridge failure.
|
|
72
|
+
*
|
|
73
|
+
* ๐ด **ONE BUILDER, TWO READERS, AND THE SECOND ONE WAS A STRING LITERAL UNTIL 268.**
|
|
74
|
+
* `timeout-caveat.ts` decides whether a mutating tool gets its retry caveat by
|
|
75
|
+
* looking for `Bridge error [timeout]` inside the rendered envelope โ a substring it
|
|
76
|
+
* spelled out itself, one file away from the template that produces it. That is
|
|
77
|
+
* `dap-timeout-predicate-reads-prose` again with a far wider blast radius: a reword of
|
|
78
|
+
* `fail()` would silently stop every non-idempotent tool from warning that a timed-out
|
|
79
|
+
* mutation may already have landed, and nothing compares the two spellings. Derived here
|
|
80
|
+
* so the two move together, and check 32 refuses a fresh literal.
|
|
81
|
+
*/
|
|
82
|
+
export function bridgeErrorLabel(code) {
|
|
83
|
+
return `Bridge error [${code}]`;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The code raised when the request could not be handed to the socket at all.
|
|
87
|
+
*
|
|
88
|
+
* ๐ด **ONE WORD, TWO PRODUCERS, AND ONLY ONE OF THEM HAS A REMEDY THAT FITS (268).** The
|
|
89
|
+
* ADDON raises `write_failed` too, from `operations.gd`, for a FILE it could not open for
|
|
90
|
+
* writing โ and `error_remedies.gd` answers it with *"Check the target path is inside the
|
|
91
|
+
* project and not read-only; the number is a Godot Error code."* That sentence is correct
|
|
92
|
+
* for the addon and nonsense for this site, which has no path, no file and no Godot error
|
|
93
|
+
* code: it is a TCP socket that went away. The collision was invisible while this branch
|
|
94
|
+
* was believed unreachable; 268 measured it reachable 200/200 and it became live.
|
|
95
|
+
*
|
|
96
|
+
* The CODE is kept and the message and remedy are made to fit โ 262's disposition for an
|
|
97
|
+
* overloaded shipped code, for the same reason: fix what the reader acts on, and leave a
|
|
98
|
+
* wire vocabulary that callers may already branch on alone. Whether the two producers
|
|
99
|
+
* should share a word at all is a wire question, and it is enqueued as one rather than
|
|
100
|
+
* decided here.
|
|
101
|
+
*/
|
|
102
|
+
export const BRIDGE_WRITE_FAILED = "write_failed";
|
|
103
|
+
/**
|
|
104
|
+
* The next action for a request that never reached the socket.
|
|
105
|
+
*
|
|
106
|
+
* ๐ด IT IS THE ONE FAILURE IN THIS CLASS THAT CAN SAY *RETRY* WITHOUT QUALIFICATION, and
|
|
107
|
+
* that is the whole value of separating it. `timeout-caveat.ts` exists because a bridge
|
|
108
|
+
* TIMEOUT may already have been applied by the editor; `closeRemedy` cannot promise
|
|
109
|
+
* otherwise either, since a request in flight when the socket dropped may have landed.
|
|
110
|
+
* This one is different in a way the host actually knows: the frame was never handed to
|
|
111
|
+
* the kernel, so nothing can have been applied, so a retry is a first attempt and not a
|
|
112
|
+
* second. 264's rule โ measure N families, speak for N โ read in the direction that
|
|
113
|
+
* licenses a stronger sentence rather than a weaker one.
|
|
114
|
+
*/
|
|
115
|
+
export const WRITE_FAILED_REMEDY = "Retry the call โ the request was never sent, so nothing was applied and this is a " +
|
|
116
|
+
"first attempt rather than a second. A recurrence means the host is tearing the " +
|
|
117
|
+
"connection down mid-request.";
|
|
118
|
+
/**
|
|
119
|
+
* The sentence for a request that never reached the socket.
|
|
120
|
+
*
|
|
121
|
+
* `cause` is node's own error, kept in a parenthetical rather than used as the whole
|
|
122
|
+
* message: `Cannot call write after a stream was destroyed` is a true statement about a
|
|
123
|
+
* Node stream and tells a caller of `node_add` nothing about their call, which is what
|
|
124
|
+
* this site shipped until 268.
|
|
125
|
+
*/
|
|
126
|
+
export function writeFailedMessage(method, cause) {
|
|
127
|
+
return (`Bridge request '${method}' was never sent โ the connection was torn down between ` +
|
|
128
|
+
`preparing the request and writing it, so the editor did not receive it (${cause.message}).`);
|
|
129
|
+
}
|
|
68
130
|
/**
|
|
69
131
|
* TCP client for the in-editor Breakpoint MCP addon. Speaks newline-delimited
|
|
70
132
|
* JSON. Requests are correlated to responses by `id`. Connects lazily and
|
|
@@ -396,9 +458,15 @@ export class BridgeClient {
|
|
|
396
458
|
const timer = setTimeout(() => {
|
|
397
459
|
this.pending.delete(id);
|
|
398
460
|
// Remember the id BEFORE rejecting, so a reply already in flight is
|
|
399
|
-
// reconciled rather than dropped as anonymous.
|
|
400
|
-
//
|
|
401
|
-
//
|
|
461
|
+
// reconciled rather than dropped as anonymous.
|
|
462
|
+
//
|
|
463
|
+
// ๐ด THIS COMMENT USED TO SAY THE `timed out after <n>ms` PHRASING WAS
|
|
464
|
+
// LOAD-BEARING, NAMING `tools/dap.ts:29` AND `tools/csdap.ts:31`. Both line
|
|
465
|
+
// numbers had drifted by more than a hundred lines, and as of 268 the claim is
|
|
466
|
+
// false as well: those predicates read `DapError.code`, and this class's own
|
|
467
|
+
// caveat reader derives its marker from `bridgeErrorLabel`. A comment naming a
|
|
468
|
+
// line number is a claim that goes stale in silence โ which is the whole reason
|
|
469
|
+
// the join is structural now.
|
|
402
470
|
this.ledger.note(id, method, timeoutMs);
|
|
403
471
|
// ๐ด 262 ยง2 CALLED THIS "THE ONE HOST-RAISED FAILURE THAT HAS A KNOWABLE CAUSE",
|
|
404
472
|
// and said everything else this class raises โ "a closed socket, a peer that never
|
|
@@ -408,14 +476,30 @@ export class BridgeClient {
|
|
|
408
476
|
// line โ a peer that never answered really does leave nothing behind โ which is
|
|
409
477
|
// why the one cause the host can supply comes from outside the socket: its own
|
|
410
478
|
// debugger holding the game. See `setHoldProbe`, and 264's census for the rest.
|
|
411
|
-
reject(new BridgeError(
|
|
479
|
+
reject(new BridgeError(BRIDGE_TIMEOUT_CODE, `Bridge request '${method}' timed out after ${timeoutMs}ms`, this.holdProbe?.() ?? undefined));
|
|
412
480
|
}, timeoutMs);
|
|
413
481
|
this.pending.set(id, { resolve: resolve, reject, timer, method });
|
|
414
482
|
socket.write(payload, (err) => {
|
|
415
483
|
if (err) {
|
|
416
484
|
clearTimeout(timer);
|
|
417
485
|
this.pending.delete(id);
|
|
418
|
-
|
|
486
|
+
// ๐ด REACHABLE, AND 265 CONCLUDED THE OPPOSITE FROM THE SAME TECHNIQUE (268).
|
|
487
|
+
// 265 broke a live socket four ways and read `bridge_closed` every time, and
|
|
488
|
+
// reasoned that `onClose()` always rejects the pending map first. Measured here
|
|
489
|
+
// 300/300 at the node level, that ordering is BACKWARDS: on a destroyed socket
|
|
490
|
+
// the write callback fires with ERR_STREAM_DESTROYED **before** the `close`
|
|
491
|
+
// event, so this line wins whenever the socket dies inside the await gap
|
|
492
|
+
// `request()` leaves between `connect()` and here. Driven through the real
|
|
493
|
+
// client โ `request()` not awaited, `close()` on the next statement โ the caller
|
|
494
|
+
// received THIS rejection 200 times out of 200. 265's drives all broke the
|
|
495
|
+
// socket and then WAITED, which is the one window in which they could not win.
|
|
496
|
+
//
|
|
497
|
+
// The sentence is worth having because this case knows something the other two
|
|
498
|
+
// do not: the payload never left. A `timeout` may have been applied by the
|
|
499
|
+
// editor and `bridge_closed` may have been too, so neither can tell a caller to
|
|
500
|
+
// retry; this one can, and 267's rule is that a next action rides in the field
|
|
501
|
+
// rather than inside the message.
|
|
502
|
+
reject(new BridgeError(BRIDGE_WRITE_FAILED, writeFailedMessage(method, err), WRITE_FAILED_REMEDY));
|
|
419
503
|
}
|
|
420
504
|
});
|
|
421
505
|
});
|
package/dist/config.js
CHANGED
|
@@ -111,6 +111,18 @@ function positiveInt(raw, fallback) {
|
|
|
111
111
|
const n = Number(t);
|
|
112
112
|
return Number.isSafeInteger(n) && n > 0 && n <= 2_147_483_647 ? n : fallback;
|
|
113
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* An opt-OUT flag: true unless the operator spelled a recognised falsehood.
|
|
116
|
+
*
|
|
117
|
+
* ๐ด THE UNRECOGNISED VALUE KEEPS THE SAFE DEFAULT, AND THAT IS THE OPPOSITE OF
|
|
118
|
+
* `positiveInt`'s reasoning ON PURPOSE. A malformed deadline falls back because both
|
|
119
|
+
* outcomes are ordinary; a malformed *guard* setting must not disable the guard, or a
|
|
120
|
+
* typo โ `GODOT_DAP_REQUIRE_INITIALIZED=no thanks` โ turns a refusal off silently. Only
|
|
121
|
+
* `0`, `false`, `off` and `no` are read as intent, case-insensitively.
|
|
122
|
+
*/
|
|
123
|
+
function optOut(raw) {
|
|
124
|
+
return !/^(0|false|off|no)$/i.test((raw ?? "").trim());
|
|
125
|
+
}
|
|
114
126
|
export function loadConfig() {
|
|
115
127
|
const projectPath = process.env.GODOT_PROJECT ?? process.cwd();
|
|
116
128
|
// The C# project defaults to the main project, but is usually pointed at a
|
|
@@ -136,6 +148,7 @@ export function loadConfig() {
|
|
|
136
148
|
dapTimeoutMs: positiveInt(process.env.GODOT_DAP_TIMEOUT_MS, 20000),
|
|
137
149
|
dapSetVarTimeoutMs: positiveInt(process.env.GODOT_DAP_SETVAR_TIMEOUT_MS, 8000),
|
|
138
150
|
dapEvaluateTimeoutMs: positiveInt(process.env.GODOT_DAP_EVALUATE_TIMEOUT_MS, 8000),
|
|
151
|
+
dapRequireInitialized: optOut(process.env.GODOT_DAP_REQUIRE_INITIALIZED),
|
|
139
152
|
csDapCmd: process.env.GODOT_CSDAP_CMD ?? "netcoredbg",
|
|
140
153
|
csDapArgs: (process.env.GODOT_CSDAP_ARGS ?? "--interpreter=vscode").split(/\s+/).filter(Boolean),
|
|
141
154
|
// The default program cs_dbg_launch launches is the Mono/.NET Godot binary. GODOT_CSHARP_BIN
|
package/dist/csdap.js
CHANGED
|
@@ -1,16 +1,32 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
-
import { DapError } from "./dap.js";
|
|
2
|
+
import { DapError, DAP_TIMEOUT_CODE, DAP_UNANNOUNCED_CODE, INITIALIZED_WAIT_CEILING_MS, unannouncedMessage, } from "./dap.js";
|
|
3
3
|
import { OverdueLedger } from "./late-reply.js";
|
|
4
4
|
import { closeDetail, closeRemedy } from "./close-cause.js";
|
|
5
|
+
import { timeoutRemedy, unannouncedRemedy } from "./timeout-cause.js";
|
|
5
6
|
/**
|
|
6
7
|
* Who answers this plane, in the words its late-reply ledger already uses.
|
|
7
8
|
*
|
|
8
|
-
* ๐ด ONE CONSTANT,
|
|
9
|
-
* late
|
|
10
|
-
*
|
|
11
|
-
* per-instance deadline noun was
|
|
9
|
+
* ๐ด ONE CONSTANT, THREE READERS (264, 267). The ledger names this peer when a reply
|
|
10
|
+
* arrives late, `closeRemedy` names it when the connection drops, and `timeoutRemedy`
|
|
11
|
+
* names it when a deadline fires. Two literals would let the same peer acquire two names
|
|
12
|
+
* on the same run, which is the class of drift 257's per-instance deadline noun was
|
|
13
|
+
* written to stop.
|
|
12
14
|
*/
|
|
13
15
|
const CSDAP_PEER = "the debug adapter";
|
|
16
|
+
/** The environment variable this plane's deadline comes from โ see `dap.ts`'s twin. */
|
|
17
|
+
const CSDAP_TIMEOUT_KNOB = "GODOT_CSDAP_TIMEOUT_MS";
|
|
18
|
+
/**
|
|
19
|
+
* The next action when the adapter refuses to start a session.
|
|
20
|
+
*
|
|
21
|
+
* ๐ด THE LAST OF 264's SEVENTEEN, and the one that most looked like it already had an
|
|
22
|
+
* answer. The message names the mode and the arguments, which is a description of what
|
|
23
|
+
* was asked, not of what to do next โ measured against a real netcoredbg, the two live
|
|
24
|
+
* causes are a `program` that is not a built assembly and a project that has not been
|
|
25
|
+
* compiled, and neither is visible in the argument dump the message already prints.
|
|
26
|
+
*/
|
|
27
|
+
export const CS_START_REMEDY = "Check that the program names a built .NET assembly and that the project compiles โ the C# debug adapter refused the session, so nothing is under the debugger and no process was left running.";
|
|
28
|
+
/** The next action when `cs_dbg_restart` is called with no launch behind it. */
|
|
29
|
+
export const CS_RESTART_REMEDY = "Call `cs_dbg_launch` or `cs_dbg_attach` first โ a restart reuses the parameters of a launch that already happened, and none has.";
|
|
14
30
|
/**
|
|
15
31
|
* How long `start()` waits for the entry `stopped` event when `stopAtEntry` was
|
|
16
32
|
* requested. Bounded so an adapter that ignores stop-on-entry reports `running`
|
|
@@ -51,7 +67,7 @@ export class CsDapClient extends EventEmitter {
|
|
|
51
67
|
* dropped. `seq` is monotonic and is never reset, so a seq is never reused and
|
|
52
68
|
* a late response can only be its own request's.
|
|
53
69
|
*/
|
|
54
|
-
ledger = new OverdueLedger("C# DAP", CSDAP_PEER,
|
|
70
|
+
ledger = new OverdueLedger("C# DAP", CSDAP_PEER, CSDAP_TIMEOUT_KNOB);
|
|
55
71
|
configured = false;
|
|
56
72
|
/** Persistent watch expressions, re-evaluated at each stop (see evaluateWatches). */
|
|
57
73
|
watches = [];
|
|
@@ -128,15 +144,12 @@ export class CsDapClient extends EventEmitter {
|
|
|
128
144
|
this.configured = false;
|
|
129
145
|
this.sessionStarted = false;
|
|
130
146
|
const detail = closeDetail(cause);
|
|
131
|
-
// ๐ด THE
|
|
132
|
-
//
|
|
133
|
-
// `fail()` renders no `remedyClause`, so the next action goes where the caller will
|
|
134
|
-
// actually read it. 264's census records the asymmetry rather than hiding it: of 25
|
|
135
|
-
// host-raised failures about the world, 13 are on classes that cannot carry an answer.
|
|
147
|
+
// ๐ด ON THE FIELD, NOT IN THE MESSAGE (267) โ see `dap.ts`'s twin for the whole
|
|
148
|
+
// argument. What a caller receives is byte-identical; the channel is what changed.
|
|
136
149
|
const remedy = closeRemedy(cause, CSDAP_PEER);
|
|
137
150
|
for (const [, p] of this.pending) {
|
|
138
151
|
clearTimeout(p.timer);
|
|
139
|
-
p.reject(new DapError(p.command, `C# DAP connection closed${detail}
|
|
152
|
+
p.reject(new DapError(p.command, `C# DAP connection closed${detail}`, remedy));
|
|
140
153
|
}
|
|
141
154
|
this.pending.clear();
|
|
142
155
|
this.emit("closed");
|
|
@@ -217,7 +230,9 @@ export class CsDapClient extends EventEmitter {
|
|
|
217
230
|
// Remember the seq BEFORE rejecting, so a response already in flight is
|
|
218
231
|
// reconciled rather than dropped as anonymous.
|
|
219
232
|
this.ledger.note(seq, command, timeoutMs);
|
|
220
|
-
|
|
233
|
+
// ๐ด THE WORDING IS NO LONGER LOAD-BEARING (268) โ see `dap.ts`'s twin. The code
|
|
234
|
+
// is what `tools/csdap.ts`'s `isDapTimeout` reads; the sentence is free.
|
|
235
|
+
reject(new DapError(command, `C# DAP '${command}' timed out after ${timeoutMs}ms`, timeoutRemedy(CSDAP_PEER, CSDAP_TIMEOUT_KNOB), DAP_TIMEOUT_CODE));
|
|
221
236
|
}, timeoutMs);
|
|
222
237
|
this.pending.set(seq, { command, resolve: resolve, reject, timer });
|
|
223
238
|
this.channel.send({ seq, type: "request", command, arguments: args }).catch((err) => {
|
|
@@ -227,15 +242,23 @@ export class CsDapClient extends EventEmitter {
|
|
|
227
242
|
});
|
|
228
243
|
});
|
|
229
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Wait for `name`, bounded โ resolving **`true` when the event arrived and `false` when
|
|
247
|
+
* the timer fired**. The twin of `dap.ts`'s, changed for the same measured reason (267):
|
|
248
|
+
* a `Promise<void>` made a five-second silence and a prompt event the same observable,
|
|
249
|
+
* so `start()` sent `setBreakpoints` ahead of the event that licenses it and said
|
|
250
|
+
* nothing. Driven against a stub adapter that never emits `initialized`, `start()`
|
|
251
|
+
* returned after 5,003 ms having sent the handshake in the conformant order.
|
|
252
|
+
*/
|
|
230
253
|
waitEvent(name, timeoutMs) {
|
|
231
254
|
return new Promise((resolve) => {
|
|
232
255
|
const timer = setTimeout(() => {
|
|
233
256
|
this.removeListener(name, onEvent);
|
|
234
|
-
resolve();
|
|
257
|
+
resolve(false);
|
|
235
258
|
}, timeoutMs);
|
|
236
259
|
const onEvent = () => {
|
|
237
260
|
clearTimeout(timer);
|
|
238
|
-
resolve();
|
|
261
|
+
resolve(true);
|
|
239
262
|
};
|
|
240
263
|
this.once(name, onEvent);
|
|
241
264
|
});
|
|
@@ -327,12 +350,18 @@ export class CsDapClient extends EventEmitter {
|
|
|
327
350
|
* afterwards failed with a bare hex code against a phantom session. The same held
|
|
328
351
|
* for `program: ""` and for `cs_dbg_attach` on a process id that does not exist.
|
|
329
352
|
*/
|
|
330
|
-
async start(mode, args) {
|
|
353
|
+
async start(mode, args, requireInitialized = true) {
|
|
331
354
|
this.lastStartMode = mode;
|
|
332
355
|
this.lastStartArgs = args;
|
|
333
356
|
this.sessionStarted = false;
|
|
334
|
-
// Listen for `initialized` before we ask, so we cannot miss it.
|
|
335
|
-
|
|
357
|
+
// Listen for `initialized` before we ask, so we cannot miss it. Two windows since
|
|
358
|
+
// 268 โ see `dap.ts`'s twin for the argument. Refusing is the default, so the wait
|
|
359
|
+
// runs to the caller's own deadline; the five-second ceiling survives on the opt-out
|
|
360
|
+
// path alone, which exists to reproduce 267's behaviour including its numbers.
|
|
361
|
+
const initializedWaitMs = requireInitialized
|
|
362
|
+
? this.timeoutMs
|
|
363
|
+
: Math.min(this.timeoutMs, INITIALIZED_WAIT_CEILING_MS);
|
|
364
|
+
const onInit = this.waitEvent("initialized", initializedWaitMs);
|
|
336
365
|
this.capabilities = await this.request("initialize", {
|
|
337
366
|
clientID: "breakpoint-mcp",
|
|
338
367
|
clientName: "Godot Breakpoint MCP",
|
|
@@ -364,7 +393,16 @@ export class CsDapClient extends EventEmitter {
|
|
|
364
393
|
// EventEmitter and throws when unlistened. This one is safe to ignore.
|
|
365
394
|
this.emit("start_failed", err);
|
|
366
395
|
});
|
|
367
|
-
|
|
396
|
+
// ๐ด CAPTURED, NOT DISCARDED (267) โ see `dap.ts`'s twin.
|
|
397
|
+
const initializedSeen = await onInit;
|
|
398
|
+
// ๐ด REFUSED, NOT REPORTED (268) โ and BEFORE `applyAllBreakpoints()`, because that
|
|
399
|
+
// call is the defect itself. See `dap.ts`'s twin.
|
|
400
|
+
if (!initializedSeen && requireInitialized) {
|
|
401
|
+
this.state = "terminated";
|
|
402
|
+
this.sessionStarted = false;
|
|
403
|
+
this.configured = false;
|
|
404
|
+
throw new DapError(mode, unannouncedMessage("The C# debug adapter", mode, initializedWaitMs), unannouncedRemedy(CSDAP_PEER, CSDAP_TIMEOUT_KNOB), DAP_UNANNOUNCED_CODE);
|
|
405
|
+
}
|
|
368
406
|
await this.applyAllBreakpoints();
|
|
369
407
|
// Arm the entry-stop wait BEFORE configurationDone is sent: netcoredbg can emit
|
|
370
408
|
// `stopped` before its configurationDone response lands, and a listener attached
|
|
@@ -389,9 +427,13 @@ export class CsDapClient extends EventEmitter {
|
|
|
389
427
|
this.state = "terminated";
|
|
390
428
|
void startReq;
|
|
391
429
|
const detail = fatal?.message ?? String(fatal);
|
|
430
|
+
// ๐ด THE NEXT ACTION MOVED OUT OF THE MESSAGE (267). It read *check that the program
|
|
431
|
+
// exists and is a .NET assembly* mid-sentence, between a state report and an
|
|
432
|
+
// argument dump โ 264's census counted exactly this shape among the SEVEN that
|
|
433
|
+
// named an action nothing structural could read. The message now reports state; the
|
|
434
|
+
// field carries the instruction, and check 28's grammar reader can see it.
|
|
392
435
|
throw new DapError(mode, `the C# debug adapter did not start the session: ${detail}. No debug session is ` +
|
|
393
|
-
`running
|
|
394
|
-
`${JSON.stringify(args).slice(0, 200)}).`);
|
|
436
|
+
`running (${mode} args: ${JSON.stringify(args).slice(0, 200)}).`, CS_START_REMEDY);
|
|
395
437
|
}
|
|
396
438
|
this.configured = true;
|
|
397
439
|
this.sessionStarted = true;
|
|
@@ -411,6 +453,7 @@ export class CsDapClient extends EventEmitter {
|
|
|
411
453
|
// simply reports `running`, as before.
|
|
412
454
|
if (entryStop)
|
|
413
455
|
await entryStop;
|
|
456
|
+
return { initializedSeen, initializedWaitMs };
|
|
414
457
|
}
|
|
415
458
|
threadId() {
|
|
416
459
|
return this.lastStoppedThreadId ?? 1;
|
|
@@ -482,7 +525,7 @@ export class CsDapClient extends EventEmitter {
|
|
|
482
525
|
*/
|
|
483
526
|
async restart(overrideArgs = {}, waitMs = 15000) {
|
|
484
527
|
if (!this.lastStartMode || !this.lastStartArgs) {
|
|
485
|
-
throw new DapError("restart", "no C# debug session to restart
|
|
528
|
+
throw new DapError("restart", "no C# debug session to restart", CS_RESTART_REMEDY);
|
|
486
529
|
}
|
|
487
530
|
const args = { ...this.lastStartArgs, ...overrideArgs };
|
|
488
531
|
if (this.capabilities?.["supportsRestartRequest"] === true) {
|
package/dist/cslsp.js
CHANGED
|
@@ -2,6 +2,7 @@ import { fileURLToPath } from "node:url";
|
|
|
2
2
|
import { LspError } from "./lsp.js";
|
|
3
3
|
import { OverdueLedger } from "./late-reply.js";
|
|
4
4
|
import { closeDetail, closeRemedy } from "./close-cause.js";
|
|
5
|
+
import { timeoutRemedy } from "./timeout-cause.js";
|
|
5
6
|
import { packageVersion } from "./version.js";
|
|
6
7
|
/**
|
|
7
8
|
* Who answers this plane, in the words its late-reply ledger already uses.
|
|
@@ -12,6 +13,8 @@ import { packageVersion } from "./version.js";
|
|
|
12
13
|
* per-instance deadline noun was written to stop.
|
|
13
14
|
*/
|
|
14
15
|
const CSLSP_PEER = "the language server";
|
|
16
|
+
/** The environment variable this plane's deadline comes from โ see `dap.ts`'s twin. */
|
|
17
|
+
const CSLSP_TIMEOUT_KNOB = "GODOT_CSLSP_TIMEOUT_MS";
|
|
15
18
|
/**
|
|
16
19
|
* Minimal LSP client for the C#/.NET semantic plane (D4 C2). Transport-agnostic:
|
|
17
20
|
* it drives any `JsonRpcChannel`, so production spawns OmniSharp over stdio
|
|
@@ -41,7 +44,7 @@ export class CsLspClient {
|
|
|
41
44
|
* dropped. `nextId` is monotonic and is deliberately NOT reset by onClose(),
|
|
42
45
|
* so an id is never reused and a late reply can only be its own request's.
|
|
43
46
|
*/
|
|
44
|
-
ledger = new OverdueLedger("C# LSP", CSLSP_PEER,
|
|
47
|
+
ledger = new OverdueLedger("C# LSP", CSLSP_PEER, CSLSP_TIMEOUT_KNOB);
|
|
45
48
|
/** Absolute project root path (no trailing slash), used to canonicalize URIs. */
|
|
46
49
|
rootFsPath;
|
|
47
50
|
constructor(channel, rootUri, timeoutMs) {
|
|
@@ -139,15 +142,17 @@ export class CsLspClient {
|
|
|
139
142
|
}
|
|
140
143
|
onClose(cause) {
|
|
141
144
|
const detail = closeDetail(cause);
|
|
142
|
-
// ๐ด THE SAME ERRNO SPLIT `bridge.ts` GOT (264 ยง3),
|
|
143
|
-
//
|
|
144
|
-
// `fail()` renders no `remedyClause
|
|
145
|
-
//
|
|
146
|
-
//
|
|
145
|
+
// ๐ด THE SAME ERRNO SPLIT `bridge.ts` GOT (264 ยง3), ON THE FIELD RATHER THAN IN THE
|
|
146
|
+
// MESSAGE (267). The comment that stood here said `LspError` "carries no `remedy`
|
|
147
|
+
// field, and the plane's `fail()` renders no `remedyClause`" โ true when written, and
|
|
148
|
+
// the reason 264's census counted this site among the SEVEN that named an action
|
|
149
|
+
// nothing structural could read. Both halves are gone. **What a caller receives is
|
|
150
|
+
// byte-identical** and is asserted so: the message ended in ` โ ${remedy}` and the
|
|
151
|
+
// renderer appends exactly that. A change of channel, not of wording.
|
|
147
152
|
const remedy = closeRemedy(cause, CSLSP_PEER);
|
|
148
153
|
for (const [, p] of this.pending) {
|
|
149
154
|
clearTimeout(p.timer);
|
|
150
|
-
p.reject(new LspError("closed", `C# LSP connection closed${detail}
|
|
155
|
+
p.reject(new LspError("closed", `C# LSP connection closed${detail}`, remedy));
|
|
151
156
|
}
|
|
152
157
|
this.pending.clear();
|
|
153
158
|
this.initialized = null;
|
|
@@ -169,7 +174,7 @@ export class CsLspClient {
|
|
|
169
174
|
// Remember the id BEFORE rejecting, so a reply already in flight is
|
|
170
175
|
// reconciled rather than dropped as anonymous.
|
|
171
176
|
this.ledger.note(id, method, timeoutMs);
|
|
172
|
-
reject(new LspError("timeout", `C# LSP '${method}' timed out after ${timeoutMs}ms
|
|
177
|
+
reject(new LspError("timeout", `C# LSP '${method}' timed out after ${timeoutMs}ms`, timeoutRemedy(CSLSP_PEER, CSLSP_TIMEOUT_KNOB)));
|
|
173
178
|
}, timeoutMs);
|
|
174
179
|
this.pending.set(id, { resolve: resolve, reject, timer });
|
|
175
180
|
this.channel.send({ jsonrpc: "2.0", id, method, params }).catch((err) => {
|