querysub 0.451.0 → 0.453.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/.claude/settings.local.json +12 -1
- package/bin/join-public.js +1 -0
- package/package.json +1 -1
- package/src/-a-archives/archiveCache.ts +53 -597
- package/src/-g-core-values/NodeCapabilities.ts +29 -28
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +24 -0
- package/src/0-path-value-core/pathValueCore.ts +1 -1
- package/src/2-proxy/PathValueProxyWatcher.ts +6 -6
- package/src/4-querysub/Querysub.ts +15 -13
- package/src/archiveapps/archiveGCEntry.tsx +1 -0
- package/src/archiveapps/archiveJoinEntry.ts +8 -2
- package/src/deployManager/LaunchTrackingHeader.tsx +65 -0
- package/src/deployManager/machineApplyMainCode.ts +140 -15
- package/src/deployManager/machineSchema.ts +82 -1
- package/src/diagnostics/NodeConnectionsPage.tsx +1 -1
- package/src/diagnostics/NodeViewer.tsx +15 -25
- package/src/diagnostics/debugger/mcp-server.ts +327 -53
- package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +2 -2
- package/src/diagnostics/logs/IndexedLogs/MCPIndexedLogs.ts +64 -22
- package/src/diagnostics/logs/IndexedLogs/MCPIndexedLogsEntry.ts +32 -1
- package/src/diagnostics/managementPages.tsx +8 -0
- package/src/diagnostics/misc-pages/AuthoritySpecPage.tsx +113 -0
- package/src/diagnostics/pathAuditer.ts +0 -6
- package/test.ts +2 -1
- package/src/misc/getParentProcessId.cs +0 -53
- package/src/misc/getParentProcessId.ts +0 -53
|
@@ -79,10 +79,11 @@ export class NodeViewer extends qreact.Component {
|
|
|
79
79
|
let data: NodeData = { nodeId };
|
|
80
80
|
try {
|
|
81
81
|
data.table = await controller.getMiscInfo(nodeId);
|
|
82
|
-
|
|
83
|
-
data.
|
|
84
|
-
data.
|
|
85
|
-
data.
|
|
82
|
+
let metadata = await controller.live_getMetadata(nodeId);
|
|
83
|
+
data.live_isAlive = !!metadata;
|
|
84
|
+
data.live_exposedControllers = metadata?.exposedControllers;
|
|
85
|
+
data.live_entryPoint = metadata?.entryPoint;
|
|
86
|
+
data.live_trueTimeOffset = metadata?.trueTimeOffset;
|
|
86
87
|
data.live_authorityPaths = await controller.live_getAuthorityPaths(nodeId);
|
|
87
88
|
data.ip = await controller.getNodeIP(nodeId);
|
|
88
89
|
if (data.ip === ourExternalIP || data.ip === ourIP) {
|
|
@@ -484,27 +485,18 @@ class NodeViewerControllerBase {
|
|
|
484
485
|
await syncNodesNow();
|
|
485
486
|
}
|
|
486
487
|
|
|
487
|
-
public async
|
|
488
|
-
return await NodeCapabilitiesController.nodes[nodeId].
|
|
488
|
+
public async live_getMetadata(nodeId: string) {
|
|
489
|
+
return await errorToUndefinedSilent(NodeCapabilitiesController.nodes[nodeId].getMetadata());
|
|
489
490
|
}
|
|
490
491
|
|
|
491
492
|
public async getControllerNodeIdList(controller: SocketRegistered<{}>) {
|
|
492
493
|
return await getControllerNodeIdList(controller);
|
|
493
494
|
}
|
|
494
495
|
|
|
495
|
-
public async live_isAlive(nodeId: string) {
|
|
496
|
-
return !!await errorToUndefinedSilent(NodeCapabilitiesController.nodes[nodeId].getEntryPoint());
|
|
497
|
-
}
|
|
498
496
|
public async live_getAuthorityPaths(nodeId: string) {
|
|
499
497
|
let topo = await NodeMetadataController.nodes[nodeId].debugGetTopologyEntry(nodeId);
|
|
500
498
|
return topo?.authoritySpec;
|
|
501
499
|
}
|
|
502
|
-
public async live_getEntryPoint(nodeId: string) {
|
|
503
|
-
return NodeCapabilitiesController.nodes[nodeId].getEntryPoint();
|
|
504
|
-
}
|
|
505
|
-
public async live_getTrueTimeOffset(nodeId: string) {
|
|
506
|
-
return NodeCapabilitiesController.nodes[nodeId].getTrueTimeOffset();
|
|
507
|
-
}
|
|
508
500
|
|
|
509
501
|
public async getMiscInfo(nodeId: string): Promise<{
|
|
510
502
|
columns: ColumnsType;
|
|
@@ -521,18 +513,19 @@ class NodeViewerControllerBase {
|
|
|
521
513
|
row[columnName] = "Error: " + e.stack;
|
|
522
514
|
}
|
|
523
515
|
}
|
|
516
|
+
let metadataPromise = NodeCapabilitiesController.nodes[nodeId].getMetadata();
|
|
524
517
|
let promises = [
|
|
525
518
|
wrapAddTableValue("paths|paths", {}, async () => {
|
|
526
519
|
let live_authorityPaths = (await NodeMetadataController.nodes[nodeId].debugGetPathAuthorities(nodeId));
|
|
527
520
|
return JSON.stringify(live_authorityPaths);
|
|
528
521
|
}),
|
|
529
522
|
wrapAddTableValue("paths|functions", {}, async () => {
|
|
530
|
-
let
|
|
531
|
-
return
|
|
523
|
+
let metadata = await metadataPromise;
|
|
524
|
+
return metadata.functionRunnerShards.map(x => `${x.domainName}[${x.shardRange.startFraction}-${x.shardRange.endFraction}]${x.secondaryShardRange && `+[${x.secondaryShardRange?.startFraction || 0}-${x.secondaryShardRange?.endFraction || 0}]` || ""}`).join(" | ");
|
|
532
525
|
}),
|
|
533
526
|
wrapAddTableValue("uptime", { formatter: "timeSpan" }, async () => {
|
|
534
|
-
let
|
|
535
|
-
return Date.now() - startupTime;
|
|
527
|
+
let metadata = await metadataPromise;
|
|
528
|
+
return Date.now() - metadata.startupTime;
|
|
536
529
|
}),
|
|
537
530
|
wrapAddTableValue("port", {}, async () => {
|
|
538
531
|
return nodeId.split(":").at(-1);
|
|
@@ -557,8 +550,8 @@ class NodeViewerControllerBase {
|
|
|
557
550
|
].filter(x => x);
|
|
558
551
|
}),
|
|
559
552
|
wrapAddTableValue("capabilities|capabilities", {}, async () => {
|
|
560
|
-
let
|
|
561
|
-
return
|
|
553
|
+
let metadata = await metadataPromise;
|
|
554
|
+
return metadata.exposedControllers.map(x => x.split("-")[0]);
|
|
562
555
|
}),
|
|
563
556
|
];
|
|
564
557
|
await Promise.allSettled(promises);
|
|
@@ -590,12 +583,9 @@ export const NodeViewerController = SocketFunction.register(
|
|
|
590
583
|
getExternalInspectURL: {},
|
|
591
584
|
verifyAccess: {},
|
|
592
585
|
getAllNodeIds: {},
|
|
593
|
-
getExposedControllers: {},
|
|
594
586
|
getControllerNodeIdList: {},
|
|
595
|
-
|
|
587
|
+
live_getMetadata: {},
|
|
596
588
|
live_getAuthorityPaths: {},
|
|
597
|
-
live_getEntryPoint: {},
|
|
598
|
-
live_getTrueTimeOffset: {},
|
|
599
589
|
getMiscInfo: {},
|
|
600
590
|
|
|
601
591
|
forceRefreshNodes: {},
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Exposes the V8 inspector / Chrome DevTools Protocol as MCP tools:
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* attachNode - connect to a Querysub cluster node by nodeId (deciding
|
|
7
|
+
* local vs remote via getExternalIP), keep the connection
|
|
8
|
+
* open, and return a short sessionId used by every other tool.
|
|
8
9
|
* detach - close a session.
|
|
9
10
|
* listScripts - list scripts loaded in the target.
|
|
10
11
|
* getSource - get a script's source (optionally a line range).
|
|
@@ -23,7 +24,6 @@
|
|
|
23
24
|
module.hotreload = false;
|
|
24
25
|
|
|
25
26
|
import * as http from "http";
|
|
26
|
-
import * as path from "path";
|
|
27
27
|
import * as dns from "dns";
|
|
28
28
|
|
|
29
29
|
// Allow callers to point this process at a different project root (so we use
|
|
@@ -49,7 +49,6 @@ import { getExternalIP } from "../../misc/networking";
|
|
|
49
49
|
import { getNodeIdDomain } from "socket-function/src/nodeCache";
|
|
50
50
|
|
|
51
51
|
const DEFAULT_MCP_PORT = 3001;
|
|
52
|
-
const DEFAULT_PORT_FILE = path.join(process.cwd(), "debugger-port.json");
|
|
53
52
|
const PROTOCOL_VERSION = "2025-06-18";
|
|
54
53
|
// Per-node budget for the getEntryPoint / getInspectURL probes in listNodes;
|
|
55
54
|
// a node that doesn't answer in time is reported with those fields undefined.
|
|
@@ -78,10 +77,38 @@ interface ScriptParsed {
|
|
|
78
77
|
url: string;
|
|
79
78
|
}
|
|
80
79
|
|
|
80
|
+
interface PausedState {
|
|
81
|
+
/** Reason CDP reported for the pause (e.g. "other" for breakpoint, "step"). */
|
|
82
|
+
reason: string;
|
|
83
|
+
/** CDP breakpointIds that fired this pause, if any. */
|
|
84
|
+
hitBreakpoints: string[];
|
|
85
|
+
/** CDP call frames, top-most first. */
|
|
86
|
+
callFrames: any[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface BreakpointEntry {
|
|
90
|
+
/** CDP breakpointId (also used as the user-facing handle). */
|
|
91
|
+
breakpointId: string;
|
|
92
|
+
/** URL the breakpoint was placed on. */
|
|
93
|
+
url: string;
|
|
94
|
+
/** Requested line number, 1-based. */
|
|
95
|
+
requestedLine: number;
|
|
96
|
+
/** Actual resolved line number, 1-based; undefined if it didn't bind. */
|
|
97
|
+
actualLine?: number;
|
|
98
|
+
/** Optional condition expression that gates the pause. */
|
|
99
|
+
condition?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
81
102
|
interface Session {
|
|
82
103
|
id: string;
|
|
83
104
|
remote: DebuggerRemote;
|
|
84
105
|
scripts: ScriptParsed[];
|
|
106
|
+
/** Active pausing breakpoints, keyed by CDP breakpointId. */
|
|
107
|
+
breakpoints: Map<string, BreakpointEntry>;
|
|
108
|
+
/** Set while the target is paused; cleared on resume. */
|
|
109
|
+
paused?: PausedState;
|
|
110
|
+
/** Resolvers waiting for the next pause event. */
|
|
111
|
+
pauseWaiters: Array<() => void>;
|
|
85
112
|
}
|
|
86
113
|
|
|
87
114
|
const sessions = new Map<string, Session>();
|
|
@@ -129,17 +156,60 @@ function resolveScript(session: Session, query: string): ScriptParsed {
|
|
|
129
156
|
* other tools can find it by sessionId.
|
|
130
157
|
*/
|
|
131
158
|
async function openSession(remote: DebuggerRemote): Promise<Session> {
|
|
132
|
-
const session: Session = {
|
|
159
|
+
const session: Session = {
|
|
160
|
+
id: newSessionId(),
|
|
161
|
+
remote,
|
|
162
|
+
scripts: [],
|
|
163
|
+
breakpoints: new Map(),
|
|
164
|
+
pauseWaiters: [],
|
|
165
|
+
};
|
|
133
166
|
// Register before enabling so we capture the initial script dump.
|
|
134
167
|
remote.on("Debugger.scriptParsed", (p: ScriptParsed) =>
|
|
135
168
|
session.scripts.push({ scriptId: p.scriptId, url: p.url }),
|
|
136
169
|
);
|
|
170
|
+
// Track pause/resume so other tools (evaluate, waitForPause) can see
|
|
171
|
+
// whether the target is currently stopped at a breakpoint.
|
|
172
|
+
remote.on("Debugger.paused", (p: any) => {
|
|
173
|
+
session.paused = {
|
|
174
|
+
reason: p.reason ?? "other",
|
|
175
|
+
hitBreakpoints: p.hitBreakpoints ?? [],
|
|
176
|
+
callFrames: p.callFrames ?? [],
|
|
177
|
+
};
|
|
178
|
+
const waiters = session.pauseWaiters.splice(0);
|
|
179
|
+
for (const w of waiters) w();
|
|
180
|
+
});
|
|
181
|
+
remote.on("Debugger.resumed", () => {
|
|
182
|
+
session.paused = undefined;
|
|
183
|
+
});
|
|
137
184
|
await remote.send("Runtime.enable");
|
|
138
185
|
await remote.send("Debugger.enable");
|
|
139
186
|
sessions.set(session.id, session);
|
|
140
187
|
return session;
|
|
141
188
|
}
|
|
142
189
|
|
|
190
|
+
/** Short summary of a paused state, suitable for tool output. */
|
|
191
|
+
function describePaused(session: Session): string {
|
|
192
|
+
if (!session.paused) return "(not paused)";
|
|
193
|
+
const { reason, hitBreakpoints, callFrames } = session.paused;
|
|
194
|
+
const hit = hitBreakpoints.length
|
|
195
|
+
? ` (hit ${hitBreakpoints.join(", ")})`
|
|
196
|
+
: "";
|
|
197
|
+
const stack = callFrames
|
|
198
|
+
.slice(0, 8)
|
|
199
|
+
.map((f: any, i: number) => {
|
|
200
|
+
const url = f.url || "?";
|
|
201
|
+
const line = (f.location?.lineNumber ?? 0) + 1;
|
|
202
|
+
const col = (f.location?.columnNumber ?? 0) + 1;
|
|
203
|
+
return ` #${i} ${f.functionName || "<anon>"} at ${url}:${line}:${col}`;
|
|
204
|
+
})
|
|
205
|
+
.join("\n");
|
|
206
|
+
return (
|
|
207
|
+
`Paused (reason=${reason})${hit}\n` +
|
|
208
|
+
`Top call frame id: ${callFrames[0]?.callFrameId ?? "(none)"}\n` +
|
|
209
|
+
`Call stack:\n${stack || " (empty)"}`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
143
213
|
/** Human-readable "Attached. sessionId=… + user scripts" summary for a session. */
|
|
144
214
|
function describeSession(session: Session): string {
|
|
145
215
|
const userScripts = session.scripts.filter((s) => isUserScript(s.url));
|
|
@@ -168,17 +238,13 @@ async function getNodeInfos(): Promise<NodeInfo[]> {
|
|
|
168
238
|
const nodes = await getAllNodeIds();
|
|
169
239
|
return Promise.all(
|
|
170
240
|
nodes.map(async (nodeId): Promise<NodeInfo> => {
|
|
171
|
-
const [
|
|
172
|
-
timeoutToUndefinedSilent(
|
|
173
|
-
NODE_INFO_TIMEOUT_MS,
|
|
174
|
-
NodeCapabilitiesController.nodes[nodeId].getEntryPoint(),
|
|
175
|
-
),
|
|
241
|
+
const [metadata] = await Promise.all([
|
|
176
242
|
timeoutToUndefinedSilent(
|
|
177
243
|
NODE_INFO_TIMEOUT_MS,
|
|
178
|
-
NodeCapabilitiesController.nodes[nodeId].
|
|
179
|
-
)
|
|
244
|
+
NodeCapabilitiesController.nodes[nodeId].getMetadata(),
|
|
245
|
+
)
|
|
180
246
|
]);
|
|
181
|
-
return { nodeId, entryPoint
|
|
247
|
+
return { nodeId, entryPoint: metadata?.entryPoint };
|
|
182
248
|
}),
|
|
183
249
|
);
|
|
184
250
|
}
|
|
@@ -267,9 +333,8 @@ const tools: Record<string, ToolDef> = {
|
|
|
267
333
|
description:
|
|
268
334
|
"List every node in the Querysub cluster, with each node's process " +
|
|
269
335
|
"entry point and V8 inspector (devtools) URL. Returns an array of " +
|
|
270
|
-
"{ nodeId, entryPoint
|
|
271
|
-
"time has entryPoint
|
|
272
|
-
"inspectUrl to point the `attach` tool at it for debugging.",
|
|
336
|
+
"{ nodeId, entryPoint }. A node that does not answer in " +
|
|
337
|
+
"time has entryPoint left undefined",
|
|
273
338
|
inputSchema: {
|
|
274
339
|
type: "object",
|
|
275
340
|
properties: {},
|
|
@@ -286,8 +351,7 @@ const tools: Record<string, ToolDef> = {
|
|
|
286
351
|
"and return a sessionId usable by every other tool. Works out " +
|
|
287
352
|
"whether the node is on this machine or remote — a local node is " +
|
|
288
353
|
"debugged directly; a remote node is reached by asking it to open a " +
|
|
289
|
-
"one-time external debug port locked to this machine's IP.
|
|
290
|
-
"this over `attach` when working with cluster nodes.",
|
|
354
|
+
"one-time external debug port locked to this machine's IP.",
|
|
291
355
|
inputSchema: {
|
|
292
356
|
type: "object",
|
|
293
357
|
properties: {
|
|
@@ -308,39 +372,6 @@ const tools: Record<string, ToolDef> = {
|
|
|
308
372
|
},
|
|
309
373
|
},
|
|
310
374
|
|
|
311
|
-
attach: {
|
|
312
|
-
description:
|
|
313
|
-
"Attach to a running Node.js process over its V8 inspector (debugger) " +
|
|
314
|
-
"port. Keeps the connection open and returns a short sessionId for use " +
|
|
315
|
-
"by every other tool. Provide one of portFile / port / wsUrl; defaults " +
|
|
316
|
-
"to the ./debugger-port.json written by test.ts. Retries for ~10s if " +
|
|
317
|
-
"the target is not listening yet.",
|
|
318
|
-
inputSchema: {
|
|
319
|
-
type: "object",
|
|
320
|
-
properties: {
|
|
321
|
-
portFile: {
|
|
322
|
-
type: "string",
|
|
323
|
-
description: "Path to a debugger-port.json file. Default: ./debugger-port.json",
|
|
324
|
-
},
|
|
325
|
-
port: { type: "number", description: "Inspector HTTP port (alternative to portFile)." },
|
|
326
|
-
wsUrl: { type: "string", description: "Direct CDP WebSocket URL (alternative to portFile/port)." },
|
|
327
|
-
},
|
|
328
|
-
},
|
|
329
|
-
handler: async (args) => {
|
|
330
|
-
let remote: DebuggerRemote;
|
|
331
|
-
if (typeof args.wsUrl === "string") {
|
|
332
|
-
remote = new DebuggerRemote({ wsUrl: args.wsUrl });
|
|
333
|
-
} else if (typeof args.port === "number") {
|
|
334
|
-
remote = new DebuggerRemote({ port: args.port });
|
|
335
|
-
} else {
|
|
336
|
-
remote = new DebuggerRemote({ portFile: args.portFile ?? DEFAULT_PORT_FILE });
|
|
337
|
-
}
|
|
338
|
-
const session = await openSession(remote);
|
|
339
|
-
log(`attached session ${session.id} (${session.scripts.length} scripts)`);
|
|
340
|
-
return describeSession(session);
|
|
341
|
-
},
|
|
342
|
-
},
|
|
343
|
-
|
|
344
375
|
detach: {
|
|
345
376
|
description: "Close a debugger session and disconnect from the target.",
|
|
346
377
|
inputSchema: {
|
|
@@ -424,8 +455,10 @@ const tools: Record<string, ToolDef> = {
|
|
|
424
455
|
evaluate: {
|
|
425
456
|
description:
|
|
426
457
|
"Evaluate a JavaScript expression inside the target process and return " +
|
|
427
|
-
"the result.
|
|
428
|
-
"
|
|
458
|
+
"the result. When the target is paused at a breakpoint, the expression " +
|
|
459
|
+
"runs in the top call frame's scope so locals and parameters are visible; " +
|
|
460
|
+
"otherwise it runs in the global context. Awaits promises. Tip: " +
|
|
461
|
+
"`process.exit(0)` cleanly terminates the target (when not paused).",
|
|
429
462
|
inputSchema: {
|
|
430
463
|
type: "object",
|
|
431
464
|
properties: {
|
|
@@ -439,6 +472,21 @@ const tools: Record<string, ToolDef> = {
|
|
|
439
472
|
if (typeof args.expression !== "string") {
|
|
440
473
|
throw new Error("expression must be a string");
|
|
441
474
|
}
|
|
475
|
+
if (session.paused?.callFrames?.length) {
|
|
476
|
+
const topFrame = session.paused.callFrames[0];
|
|
477
|
+
const res = await session.remote.send("Debugger.evaluateOnCallFrame", {
|
|
478
|
+
callFrameId: topFrame.callFrameId,
|
|
479
|
+
expression: args.expression,
|
|
480
|
+
returnByValue: true,
|
|
481
|
+
});
|
|
482
|
+
if (res.exceptionDetails) {
|
|
483
|
+
throw new Error(`Remote threw: ${JSON.stringify(res.exceptionDetails)}`);
|
|
484
|
+
}
|
|
485
|
+
const value = res.result?.value;
|
|
486
|
+
return value === undefined
|
|
487
|
+
? "undefined (or non-serializable result)"
|
|
488
|
+
: JSON.stringify(value, null, 2);
|
|
489
|
+
}
|
|
442
490
|
const value = await session.remote.evaluate(args.expression);
|
|
443
491
|
return value === undefined
|
|
444
492
|
? "undefined (or non-serializable result)"
|
|
@@ -578,6 +626,232 @@ const tools: Record<string, ToolDef> = {
|
|
|
578
626
|
return `${header}\n${body}`;
|
|
579
627
|
},
|
|
580
628
|
},
|
|
629
|
+
|
|
630
|
+
setBreakpoint: {
|
|
631
|
+
description:
|
|
632
|
+
"Install a real breakpoint that PAUSES the target when hit. Unlike " +
|
|
633
|
+
"logpoint, this stops execution so you can inspect state (via evaluate, " +
|
|
634
|
+
"which uses the paused call frame's scope) and add more breakpoints " +
|
|
635
|
+
"before resuming. Returns a breakpointId. Use waitForPause to block " +
|
|
636
|
+
"until the breakpoint actually fires. IMPORTANT: while paused, the " +
|
|
637
|
+
"target's entire event loop is stopped (no heartbeats, no I/O) — " +
|
|
638
|
+
"external watchdogs may kill long-paused processes, so resume promptly.",
|
|
639
|
+
inputSchema: {
|
|
640
|
+
type: "object",
|
|
641
|
+
properties: {
|
|
642
|
+
sessionId: { type: "string" },
|
|
643
|
+
script: { type: "string", description: "scriptId or URL substring." },
|
|
644
|
+
lineNumber: {
|
|
645
|
+
type: "number",
|
|
646
|
+
description: "1-based line to set the breakpoint on (matches getSource output).",
|
|
647
|
+
},
|
|
648
|
+
condition: {
|
|
649
|
+
type: "string",
|
|
650
|
+
description:
|
|
651
|
+
"Optional JS boolean expression. The target only pauses when it is truthy.",
|
|
652
|
+
},
|
|
653
|
+
},
|
|
654
|
+
required: ["sessionId", "script", "lineNumber"],
|
|
655
|
+
},
|
|
656
|
+
handler: async (args) => {
|
|
657
|
+
const session = getSession(args.sessionId);
|
|
658
|
+
const script = resolveScript(session, String(args.script));
|
|
659
|
+
const lineNumber = Math.floor(args.lineNumber);
|
|
660
|
+
if (!(lineNumber >= 1)) {
|
|
661
|
+
throw new Error("lineNumber must be a positive integer (1-based).");
|
|
662
|
+
}
|
|
663
|
+
if (args.condition !== undefined && typeof args.condition !== "string") {
|
|
664
|
+
throw new Error("condition must be a string if provided.");
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const params: any = {
|
|
668
|
+
url: script.url,
|
|
669
|
+
lineNumber: lineNumber - 1,
|
|
670
|
+
};
|
|
671
|
+
if (typeof args.condition === "string" && args.condition.trim()) {
|
|
672
|
+
params.condition = args.condition;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const res = await session.remote.send("Debugger.setBreakpointByUrl", params);
|
|
676
|
+
const breakpointId: string = res.breakpointId;
|
|
677
|
+
const locations: any[] = res.locations ?? [];
|
|
678
|
+
const bound = locations.length > 0;
|
|
679
|
+
const actualLine = bound ? locations[0].lineNumber + 1 : undefined;
|
|
680
|
+
|
|
681
|
+
session.breakpoints.set(breakpointId, {
|
|
682
|
+
breakpointId,
|
|
683
|
+
url: script.url,
|
|
684
|
+
requestedLine: lineNumber,
|
|
685
|
+
actualLine,
|
|
686
|
+
condition: params.condition,
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
if (!bound) {
|
|
690
|
+
return (
|
|
691
|
+
`Breakpoint set but did NOT bind: line ${lineNumber} of ${script.url} ` +
|
|
692
|
+
`has no executable code right now. It will bind later if the source is ` +
|
|
693
|
+
`(re)loaded. breakpointId=${breakpointId}`
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
return (
|
|
697
|
+
`Breakpoint set. breakpointId=${breakpointId}\n` +
|
|
698
|
+
`Location: ${script.url}:${actualLine}` +
|
|
699
|
+
(actualLine !== lineNumber ? ` (requested ${lineNumber})` : "") +
|
|
700
|
+
(params.condition ? `\nCondition: ${params.condition}` : "")
|
|
701
|
+
);
|
|
702
|
+
},
|
|
703
|
+
},
|
|
704
|
+
|
|
705
|
+
removeBreakpoint: {
|
|
706
|
+
description: "Remove a pausing breakpoint by its breakpointId.",
|
|
707
|
+
inputSchema: {
|
|
708
|
+
type: "object",
|
|
709
|
+
properties: {
|
|
710
|
+
sessionId: { type: "string" },
|
|
711
|
+
breakpointId: { type: "string" },
|
|
712
|
+
},
|
|
713
|
+
required: ["sessionId", "breakpointId"],
|
|
714
|
+
},
|
|
715
|
+
handler: async (args) => {
|
|
716
|
+
const session = getSession(args.sessionId);
|
|
717
|
+
if (typeof args.breakpointId !== "string" || !args.breakpointId) {
|
|
718
|
+
throw new Error("breakpointId must be a non-empty string.");
|
|
719
|
+
}
|
|
720
|
+
await session.remote.send("Debugger.removeBreakpoint", {
|
|
721
|
+
breakpointId: args.breakpointId,
|
|
722
|
+
});
|
|
723
|
+
const existed = session.breakpoints.delete(args.breakpointId);
|
|
724
|
+
return existed
|
|
725
|
+
? `Removed breakpoint ${args.breakpointId}.`
|
|
726
|
+
: `Removed breakpoint ${args.breakpointId} (was not in this session's table).`;
|
|
727
|
+
},
|
|
728
|
+
},
|
|
729
|
+
|
|
730
|
+
listBreakpoints: {
|
|
731
|
+
description: "List the pausing breakpoints currently set on this session.",
|
|
732
|
+
inputSchema: {
|
|
733
|
+
type: "object",
|
|
734
|
+
properties: { sessionId: { type: "string" } },
|
|
735
|
+
required: ["sessionId"],
|
|
736
|
+
},
|
|
737
|
+
handler: async (args) => {
|
|
738
|
+
const session = getSession(args.sessionId);
|
|
739
|
+
if (session.breakpoints.size === 0) return "(no breakpoints set)";
|
|
740
|
+
const lines: string[] = [];
|
|
741
|
+
for (const bp of session.breakpoints.values()) {
|
|
742
|
+
const loc = bp.actualLine !== undefined
|
|
743
|
+
? `${bp.url}:${bp.actualLine}`
|
|
744
|
+
: `${bp.url}:${bp.requestedLine} (NOT BOUND)`;
|
|
745
|
+
const cond = bp.condition ? ` condition=${bp.condition}` : "";
|
|
746
|
+
lines.push(`${bp.breakpointId} ${loc}${cond}`);
|
|
747
|
+
}
|
|
748
|
+
return lines.join("\n");
|
|
749
|
+
},
|
|
750
|
+
},
|
|
751
|
+
|
|
752
|
+
waitForPause: {
|
|
753
|
+
description:
|
|
754
|
+
"Block until the target is paused at a breakpoint (or already is), and " +
|
|
755
|
+
"return a summary of the paused state including call stack and the top " +
|
|
756
|
+
"callFrameId. If the timeout elapses before a pause, returns a not-paused " +
|
|
757
|
+
"message and does NOT throw.",
|
|
758
|
+
inputSchema: {
|
|
759
|
+
type: "object",
|
|
760
|
+
properties: {
|
|
761
|
+
sessionId: { type: "string" },
|
|
762
|
+
timeoutMs: {
|
|
763
|
+
type: "number",
|
|
764
|
+
description: "How long to wait for a pause, in ms. Default 30000.",
|
|
765
|
+
},
|
|
766
|
+
},
|
|
767
|
+
required: ["sessionId"],
|
|
768
|
+
},
|
|
769
|
+
handler: async (args) => {
|
|
770
|
+
const session = getSession(args.sessionId);
|
|
771
|
+
if (session.paused) {
|
|
772
|
+
return describePaused(session);
|
|
773
|
+
}
|
|
774
|
+
const timeoutMs = Math.max(1, Math.floor(args.timeoutMs ?? 30_000));
|
|
775
|
+
let timedOut = false;
|
|
776
|
+
await new Promise<void>((resolve) => {
|
|
777
|
+
const timer = setTimeout(() => {
|
|
778
|
+
timedOut = true;
|
|
779
|
+
// Drop our waiter so a later pause doesn't double-resolve.
|
|
780
|
+
const idx = session.pauseWaiters.indexOf(resolve);
|
|
781
|
+
if (idx >= 0) session.pauseWaiters.splice(idx, 1);
|
|
782
|
+
resolve();
|
|
783
|
+
}, timeoutMs);
|
|
784
|
+
session.pauseWaiters.push(() => {
|
|
785
|
+
clearTimeout(timer);
|
|
786
|
+
resolve();
|
|
787
|
+
});
|
|
788
|
+
});
|
|
789
|
+
if (timedOut && !session.paused) {
|
|
790
|
+
return `Timed out after ${timeoutMs}ms — target is not paused.`;
|
|
791
|
+
}
|
|
792
|
+
return describePaused(session);
|
|
793
|
+
},
|
|
794
|
+
},
|
|
795
|
+
|
|
796
|
+
resume: {
|
|
797
|
+
description: "Resume the paused target (Debugger.resume).",
|
|
798
|
+
inputSchema: {
|
|
799
|
+
type: "object",
|
|
800
|
+
properties: { sessionId: { type: "string" } },
|
|
801
|
+
required: ["sessionId"],
|
|
802
|
+
},
|
|
803
|
+
handler: async (args) => {
|
|
804
|
+
const session = getSession(args.sessionId);
|
|
805
|
+
if (!session.paused) return "Target is not paused — nothing to resume.";
|
|
806
|
+
await session.remote.send("Debugger.resume");
|
|
807
|
+
return "Resumed.";
|
|
808
|
+
},
|
|
809
|
+
},
|
|
810
|
+
|
|
811
|
+
stepOver: {
|
|
812
|
+
description: "Step over the next statement (Debugger.stepOver). Target must be paused.",
|
|
813
|
+
inputSchema: {
|
|
814
|
+
type: "object",
|
|
815
|
+
properties: { sessionId: { type: "string" } },
|
|
816
|
+
required: ["sessionId"],
|
|
817
|
+
},
|
|
818
|
+
handler: async (args) => {
|
|
819
|
+
const session = getSession(args.sessionId);
|
|
820
|
+
if (!session.paused) throw new Error("Target is not paused.");
|
|
821
|
+
await session.remote.send("Debugger.stepOver");
|
|
822
|
+
return "Stepped over.";
|
|
823
|
+
},
|
|
824
|
+
},
|
|
825
|
+
|
|
826
|
+
stepInto: {
|
|
827
|
+
description: "Step into the next function call (Debugger.stepInto). Target must be paused.",
|
|
828
|
+
inputSchema: {
|
|
829
|
+
type: "object",
|
|
830
|
+
properties: { sessionId: { type: "string" } },
|
|
831
|
+
required: ["sessionId"],
|
|
832
|
+
},
|
|
833
|
+
handler: async (args) => {
|
|
834
|
+
const session = getSession(args.sessionId);
|
|
835
|
+
if (!session.paused) throw new Error("Target is not paused.");
|
|
836
|
+
await session.remote.send("Debugger.stepInto");
|
|
837
|
+
return "Stepped into.";
|
|
838
|
+
},
|
|
839
|
+
},
|
|
840
|
+
|
|
841
|
+
stepOut: {
|
|
842
|
+
description: "Step out of the current function (Debugger.stepOut). Target must be paused.",
|
|
843
|
+
inputSchema: {
|
|
844
|
+
type: "object",
|
|
845
|
+
properties: { sessionId: { type: "string" } },
|
|
846
|
+
required: ["sessionId"],
|
|
847
|
+
},
|
|
848
|
+
handler: async (args) => {
|
|
849
|
+
const session = getSession(args.sessionId);
|
|
850
|
+
if (!session.paused) throw new Error("Target is not paused.");
|
|
851
|
+
await session.remote.send("Debugger.stepOut");
|
|
852
|
+
return "Stepped out.";
|
|
853
|
+
},
|
|
854
|
+
},
|
|
581
855
|
};
|
|
582
856
|
|
|
583
857
|
// --------------------------------------------------------------------------
|
|
@@ -290,8 +290,8 @@ export class IndexedLogs<T> {
|
|
|
290
290
|
if (!hasLogger) return false;
|
|
291
291
|
// NOTE: Prefer to do the searching on the move logs service. However, if it's not available, any service can do searching. It just might lag that server...
|
|
292
292
|
if (preferredOnly) {
|
|
293
|
-
let
|
|
294
|
-
if (!entryPoint
|
|
293
|
+
let metadata = await timeoutToUndefinedSilent(2500, NodeCapabilitiesController.nodes[nodeId].getMetadata());
|
|
294
|
+
if (!metadata?.entryPoint.includes("movelogs")) return false;
|
|
295
295
|
}
|
|
296
296
|
added = true;
|
|
297
297
|
|