gdharness 1.1.59 → 1.1.61

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/build/cli.js CHANGED
@@ -9183,11 +9183,175 @@ function enclosingFunction(source, line, code = codeOf(source)) {
9183
9183
  }
9184
9184
  return null;
9185
9185
  }
9186
- function callSitesOf(source, name) {
9186
+ function afterHeader(text, from) {
9187
+ let depth = 0;
9188
+ for (let at = text.indexOf("(", from);at !== -1 && at < text.length; at += 1) {
9189
+ if (text[at] === "(") {
9190
+ depth += 1;
9191
+ } else if (text[at] === ")") {
9192
+ depth -= 1;
9193
+ if (depth === 0) {
9194
+ const colon = text.indexOf(":", at);
9195
+ return colon === -1 ? -1 : colon + 1;
9196
+ }
9197
+ }
9198
+ }
9199
+ return -1;
9200
+ }
9201
+ function bodyCallsSuper(code, line, lambda) {
9202
+ const lines = code.split(`
9203
+ `);
9204
+ const first = lines[line - 1];
9205
+ if (first === undefined) {
9206
+ return false;
9207
+ }
9208
+ const header = lambda ? LAMBDA_HEADER.exec(first) : FUNC_LINE.exec(first);
9209
+ if (header !== null) {
9210
+ const body = afterHeader(first, header.index);
9211
+ if (body !== -1 && first.slice(body).trim() !== "") {
9212
+ return SUPER_CALL.test(first.slice(body));
9213
+ }
9214
+ }
9215
+ const indent = indentOf(first);
9216
+ let lambdaIndent = null;
9217
+ for (let index = line - 1;index < lines.length; index += 1) {
9218
+ const text = lines[index] ?? "";
9219
+ if (text.trim() === "") {
9220
+ continue;
9221
+ }
9222
+ const at = indentOf(text);
9223
+ if (at < indent) {
9224
+ break;
9225
+ }
9226
+ if (lambdaIndent !== null) {
9227
+ if (at > lambdaIndent) {
9228
+ continue;
9229
+ }
9230
+ lambdaIndent = null;
9231
+ }
9232
+ const inner = LAMBDA_HEADER.exec(text);
9233
+ if (SUPER_CALL.test(inner === null ? text : text.slice(0, inner.index))) {
9234
+ return true;
9235
+ }
9236
+ if (inner !== null) {
9237
+ const body = afterHeader(text, inner.index);
9238
+ if (body !== -1 && text.slice(body).trim() === "") {
9239
+ lambdaIndent = at;
9240
+ }
9241
+ }
9242
+ }
9243
+ return false;
9244
+ }
9245
+ function parametersOf(text, from) {
9246
+ const open = text.indexOf("(", from);
9247
+ if (open === -1) {
9248
+ return [];
9249
+ }
9250
+ const names = [];
9251
+ let depth = 0;
9252
+ let start = open + 1;
9253
+ for (let at = open;at < text.length; at += 1) {
9254
+ const here = text[at];
9255
+ if (here === "(" || here === "[" || here === "{") {
9256
+ depth += 1;
9257
+ } else if (here === ")" || here === "]" || here === "}") {
9258
+ depth -= 1;
9259
+ }
9260
+ if (here === "," && depth === 1 || depth === 0) {
9261
+ const named = new RegExp(`^\\s*(${NAME})`, "u").exec(text.slice(start, at));
9262
+ if (named?.[1] !== undefined) {
9263
+ names.push(named[1]);
9264
+ }
9265
+ start = at + 1;
9266
+ if (depth === 0) {
9267
+ break;
9268
+ }
9269
+ }
9270
+ }
9271
+ return names;
9272
+ }
9273
+ function isLocalAt(lines, index, name) {
9274
+ const word = escapedForPattern(name);
9275
+ const declares = new RegExp(`(?<![\\p{L}\\p{N}_.])(?:var|const)\\s+${word}(?![\\p{L}\\p{N}_])`, "u");
9276
+ const iterates = new RegExp(`^\\s*for\\s+${word}(?![\\p{L}\\p{N}_])`, "u");
9277
+ const own = lines[index] ?? "";
9278
+ const headerOnOwnLine = FUNC_LINE.exec(own) ?? LAMBDA_HEADER.exec(own);
9279
+ if (headerOnOwnLine !== null && parametersOf(own, headerOnOwnLine.index).includes(name)) {
9280
+ return true;
9281
+ }
9282
+ if (FUNC_LINE.test(own)) {
9283
+ return false;
9284
+ }
9285
+ let depth = indentOf(own);
9286
+ for (let at = index - 1;at >= 0; at -= 1) {
9287
+ const text = lines[at] ?? "";
9288
+ if (text.trim() === "") {
9289
+ continue;
9290
+ }
9291
+ const indent = indentOf(text);
9292
+ if (indent > depth) {
9293
+ continue;
9294
+ }
9295
+ const named = FUNC_LINE.exec(text);
9296
+ if (indent < depth) {
9297
+ depth = indent;
9298
+ if (named !== null) {
9299
+ return parametersOf(text, named.index).includes(name);
9300
+ }
9301
+ const lambda = LAMBDA_HEADER.exec(text);
9302
+ if (lambda !== null && parametersOf(text, lambda.index).includes(name) || iterates.test(text)) {
9303
+ return true;
9304
+ }
9305
+ }
9306
+ if (named !== null || depth === 0) {
9307
+ return false;
9308
+ }
9309
+ if (declares.test(text)) {
9310
+ return true;
9311
+ }
9312
+ }
9313
+ return false;
9314
+ }
9315
+ function receiverBefore(code, offset) {
9316
+ let at = offset - 1;
9317
+ while (at >= 0 && /\s/.test(code[at] ?? "")) {
9318
+ at -= 1;
9319
+ }
9320
+ if (code[at] !== ".") {
9321
+ return null;
9322
+ }
9323
+ at -= 1;
9324
+ while (at >= 0 && /\s/.test(code[at] ?? "")) {
9325
+ at -= 1;
9326
+ }
9327
+ let start = at + 1;
9328
+ while (start > 0 && isIdentifierPart(code[start - 1])) {
9329
+ start -= 1;
9330
+ }
9331
+ return start <= at ? code.slice(start, at + 1) : null;
9332
+ }
9333
+ function bodyHolding(body, offset) {
9334
+ const inner = body.inner.find((one) => one.start <= offset && offset <= one.end);
9335
+ return inner === undefined ? body : bodyHolding(inner, offset);
9336
+ }
9337
+ function callSitesOf(source, name, otherClasses = new Set) {
9187
9338
  const regions = regionsOf(source);
9188
9339
  const code = codeOf(source, regions);
9340
+ const codeLines = code.split(`
9341
+ `);
9189
9342
  const lines = new Lines(source);
9190
- return occurrencesOf(source, name, regions).filter((one) => one.kind === "code" && one.declaredAs === null).map((one) => {
9343
+ const shape = shapeOf(source);
9344
+ return occurrencesOf(source, name, regions).filter((one) => one.kind === "code" && one.declaredAs === null).filter((one) => {
9345
+ if (one.afterDot) {
9346
+ const receiver = receiverBefore(code, one.offset);
9347
+ return receiver === null || !otherClasses.has(receiver);
9348
+ }
9349
+ const index = lines.positionOf(one.offset).line;
9350
+ if (isLocalAt(codeLines, index, name)) {
9351
+ return false;
9352
+ }
9353
+ return !bodyHolding(shape.body, one.offset).declarations.some((declared) => declared.name === name && MEMBER_KINDS.has(declared.kind));
9354
+ }).map((one) => {
9191
9355
  const line = lines.positionOf(one.offset).line + 1;
9192
9356
  const after = code.slice(one.offset + name.length).trimStart();
9193
9357
  return { line, within: enclosingFunction(source, line, code), called: after.startsWith("(") };
@@ -9208,7 +9372,7 @@ function collectEnumValues(text, from, into) {
9208
9372
  function isIdentifier(name) {
9209
9373
  return new RegExp(`^${NAME}$`, "u").test(name) && !KEYWORDS.has(name);
9210
9374
  }
9211
- var IDENTIFIER_START, IDENTIFIER_PART, DECLARING, NAME = "[\\p{L}_][\\p{L}\\p{N}_]*", ANNOTATIONS = "(?:@[\\p{L}_][\\p{L}\\p{N}_]*(?:\\([^)]*\\))?\\s+)*", DECLARATION_LINE, CLASS_NAME_LINE, EXTENDS_LINE, EXTENDS_CLAUSE, FUNC_LINE, KEYWORDS;
9375
+ var IDENTIFIER_START, IDENTIFIER_PART, DECLARING, NAME = "[\\p{L}_][\\p{L}\\p{N}_]*", ANNOTATIONS = "(?:@[\\p{L}_][\\p{L}\\p{N}_]*(?:\\([^)]*\\))?\\s+)*", DECLARATION_LINE, CLASS_NAME_LINE, EXTENDS_LINE, EXTENDS_CLAUSE, FUNC_LINE, SUPER_CALL, LAMBDA_HEADER, MEMBER_KINDS, KEYWORDS;
9212
9376
  var init_gdscript_source = __esm(() => {
9213
9377
  IDENTIFIER_START = /[\p{L}_]/u;
9214
9378
  IDENTIFIER_PART = /[\p{L}\p{N}_]/u;
@@ -9218,6 +9382,9 @@ var init_gdscript_source = __esm(() => {
9218
9382
  EXTENDS_LINE = new RegExp(`^${ANNOTATIONS}(?:class_name\\s+${NAME}\\s+)?extends\\s`, "u");
9219
9383
  EXTENDS_CLAUSE = /\bextends(?=\s)/u;
9220
9384
  FUNC_LINE = new RegExp(`^[ \\t]*${ANNOTATIONS}(?:static\\s+)?func\\s+(${NAME})\\s*\\(`, "u");
9385
+ SUPER_CALL = /(?<![\p{L}\p{N}_.])super\s*[.(]/u;
9386
+ LAMBDA_HEADER = new RegExp(`(?<![\\p{L}\\p{N}_])func(?:\\s+${NAME})?\\s*\\(`, "u");
9387
+ MEMBER_KINDS = new Set(["var", "const", "signal", "enum", "enumValue"]);
9221
9388
  KEYWORDS = new Set([
9222
9389
  "and",
9223
9390
  "as",
@@ -18182,7 +18349,7 @@ var init_tool_definitions = __esm(() => {
18182
18349
  callersOf: {
18183
18350
  type: "string",
18184
18351
  ops: ["profile"],
18185
- description: "profile: also answer, under callers, where the project's scripts name this function in code: its name, or Class.name for the functions of one class. Each place has its script, line, the named function it is written in (within), whether the function is called there or passed on as a Callable (called), and the profile's calls, self and total milliseconds for the function it is in, those taking the most time first, forty at most. Found by name in the source, since the engine's profiler records nothing about who made a call, so a method of the same name on another class is listed too and a call made through a string is not."
18352
+ description: "profile: also answer, under callers, where the project's scripts name this function in code: its name, or Class.name for the functions of one class. Each place has its script, line, the named function it is written in (within), whether the function is called there or passed on as a Callable (called), and the profile's calls, self and total milliseconds for the function it is in, those taking the most time first, forty at most. Found by name in the source, since the engine's profiler records nothing about who made a call, so a method of the same name on another class is listed too, except one called through another class of the project by name when Class.name was asked, and a call made through a string is not. A parameter, local or class variable of the same name is not the function and is not listed."
18186
18353
  },
18187
18354
  limit: {
18188
18355
  type: "integer",
@@ -18218,7 +18385,7 @@ var init_tool_definitions = __esm(() => {
18218
18385
  },
18219
18386
  wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] },
18220
18387
  profile: {
18221
- summary: "where the run spends its time by GDScript function, while it runs or after it ended: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, sorted by self time, with functionsCounted, scriptMs (the self time of them all) and coveredMs. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. Self time counts a call into the engine's own methods as the caller's, and a function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. A run started with profile: true is profiled from its start. A game the editor plays that was not is profiled from the moment this is asked, and the answer says so; a run this server started without it is refused, since its debugger is fixed as it starts",
18388
+ summary: "where the run spends its time by GDScript function, while it runs or after it ended: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, sorted by self time, with functionsCounted, scriptMs (the self time of them all) and coveredMs. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. A run started with profile: true is profiled from its start. A game the editor plays that was not is profiled from the moment this is asked, and the answer says so; a run this server started without it is refused, since its debugger is fixed as it starts",
18222
18389
  requires: []
18223
18390
  }
18224
18391
  },
@@ -18274,7 +18441,7 @@ var init_tool_definitions = __esm(() => {
18274
18441
  },
18275
18442
  {
18276
18443
  name: "editor_status",
18277
- description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it; null when there is none. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
18444
+ description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it; null when there is none. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote, and editor_launch restart binds it again. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
18278
18445
  parameters: {},
18279
18446
  requires: []
18280
18447
  },
@@ -34131,7 +34298,7 @@ function runtimeRequest(endpoint, command, params, timeoutMs, keepListening = fa
34131
34298
  ok: false,
34132
34299
  reason: "busy",
34133
34300
  requestId: id,
34134
- message: `${still} Nothing was cancelled: a call that takes longer than the wait goes on and does everything it was asked, so read the state back rather than repeating it. runtime_invoke op result with requestId ${id} answers with its reply once it comes, and timeoutMs waits longer next time. A game held at a breakpoint instead is one debug_state stack answers about.`
34301
+ message: `${still} Nothing was cancelled: a call that takes longer than the wait goes on and does everything it was asked, so read the state back rather than repeating it. runtime_invoke op result with requestId ${id} answers with its reply once it comes. A game held at a breakpoint instead is one debug_state stack answers about.`
34135
34302
  });
34136
34303
  }, timeoutMs);
34137
34304
  socket.setEncoding("utf8");
@@ -40250,6 +40417,42 @@ var init_outside = __esm(() => {
40250
40417
  KEEPER_SCRIPT = fileURLToPath4(new URL("./keeper.js", import.meta.url));
40251
40418
  });
40252
40419
 
40420
+ // src/port-probe.ts
40421
+ import { connect as connect3 } from "node:net";
40422
+ function acceptsConnections(port, withinMs = 2000) {
40423
+ return new Promise((resolve) => {
40424
+ const socket = connect3({ host: "127.0.0.1", port });
40425
+ const settle = (answer) => {
40426
+ socket.destroy();
40427
+ resolve(answer);
40428
+ };
40429
+ socket.setTimeout(withinMs, () => {
40430
+ settle(null);
40431
+ });
40432
+ socket.once("connect", () => {
40433
+ settle(true);
40434
+ });
40435
+ socket.once("error", () => {
40436
+ settle(false);
40437
+ });
40438
+ });
40439
+ }
40440
+ function notListeningNote(servers, greetedAgoMs) {
40441
+ const down = [
40442
+ ...servers.lsp.listening === false ? [{ name: "language server", port: servers.lsp.port }] : [],
40443
+ ...servers.dap.listening === false ? [{ name: "debug adapter", port: servers.dap.port }] : []
40444
+ ];
40445
+ if (down.length === 0) {
40446
+ return;
40447
+ }
40448
+ const named = down.map((one) => `its ${one.name} on port ${one.port}`).join(" and ");
40449
+ const needs = down.map((one) => one.name === "debug adapter" ? "played runs and the debug tools need the debug adapter" : "diagnostics, symbols and renames need the language server").join(", and ");
40450
+ const starting = greetedAgoMs !== null && greetedAgoMs < STARTING_MS ? ` The editor greeted ${Math.round(greetedAgoMs / 1000)}s ago, so ${down.length === 1 ? "it" : "they"} may still be starting: ask again in a few seconds.` : "";
40451
+ return `The editor names ${named}, and nothing accepts connections there. Godot binds each once, as the editor starts, and reports a failure only in the editor's own log panel; ${needs}. editor_launch restart binds ${down.length === 1 ? "it" : "them"} again.${starting}`;
40452
+ }
40453
+ var STARTING_MS = 20000;
40454
+ var init_port_probe = () => {};
40455
+
40253
40456
  // src/process-time.ts
40254
40457
  import { execFile as execFile3 } from "node:child_process";
40255
40458
  import { promisify as promisify3 } from "node:util";
@@ -42325,25 +42528,36 @@ function signatureParts(signature) {
42325
42528
  return { script: parts.slice(0, -2).join("::"), line: Number.isFinite(line) ? line : 0, function: name };
42326
42529
  }
42327
42530
  function profiledFunctions(totals, limit, sourceOf = () => null) {
42328
- const rows = rowsOf(totals);
42531
+ const rows = rowsOf(totals, sourceOf);
42329
42532
  const scriptMs = ms(Object.values(totals.functions).reduce((sum, one) => sum + (one.calls > 0 ? one.selfSeconds : 0), 0));
42330
42533
  return {
42331
42534
  functions: rows.slice(0, limit).map((row) => placedLambda(row, sourceOf)),
42332
42535
  omitted: Math.max(0, rows.length - limit),
42333
42536
  scriptMs,
42334
- counted: rows.length
42537
+ counted: rows.length,
42538
+ superCallers: rows.filter((row) => row.selfIncludesSuper === true).length
42335
42539
  };
42336
42540
  }
42337
- function rowsOf(totals) {
42541
+ function rowsOf(totals, sourceOf) {
42542
+ const codes = new Map;
42543
+ const codeOfScript = (script) => {
42544
+ if (!codes.has(script)) {
42545
+ const source = sourceOf(script);
42546
+ codes.set(script, source === null ? null : codeOf(source));
42547
+ }
42548
+ return codes.get(script) ?? null;
42549
+ };
42338
42550
  return Object.entries(totals.functions).filter(([, sum]) => sum.calls > 0).map(([signature, sum]) => {
42339
42551
  const parts = signatureParts(signature);
42552
+ const code = codeOfScript(parts.script);
42340
42553
  return {
42341
42554
  script: parts.script,
42342
42555
  function: parts.function,
42343
42556
  line: parts.line,
42344
42557
  calls: sum.calls,
42345
42558
  selfMs: ms(sum.selfSeconds),
42346
- totalMs: ms(sum.totalSeconds)
42559
+ totalMs: ms(sum.totalSeconds),
42560
+ ...code !== null && bodyCallsSuper(code, parts.line, parts.function.endsWith("(lambda)")) ? { selfIncludesSuper: true } : {}
42347
42561
  };
42348
42562
  }).sort((a, b) => b.selfMs - a.selfMs || b.totalMs - a.totalMs || a.function.localeCompare(b.function));
42349
42563
  }
@@ -42358,18 +42572,41 @@ function placedLambda(row, sourceOf) {
42358
42572
  const within = source === null ? null : enclosingFunction(source, row.line);
42359
42573
  return within === null ? row : { ...row, within };
42360
42574
  }
42575
+ function classNamesOf(source) {
42576
+ const shape = shapeOf(source);
42577
+ const inner = (body) => body.inner.flatMap((one) => [...one.name === null ? [] : [one.name], ...inner(one)]);
42578
+ return [...shape.className === null ? [] : [shape.className.name], ...inner(shape.body)];
42579
+ }
42361
42580
  function callersOf(totals, name, scripts) {
42362
- const rows = rowsOf(totals);
42581
+ const listed = [...scripts];
42582
+ const sources = new Map(listed);
42583
+ const rows = rowsOf(totals, (script) => sources.get(script) ?? null);
42363
42584
  const bare = bareName(name);
42364
42585
  const target = rows.filter((row) => name.includes(".") ? row.function === name : bareName(row.function) === bare);
42586
+ const asked = name.includes(".") ? name.slice(0, name.lastIndexOf(".")) : null;
42587
+ const otherClasses = new Set;
42588
+ if (asked !== null) {
42589
+ for (const [, source] of listed) {
42590
+ for (const declared of classNamesOf(source)) {
42591
+ if (declared !== asked) {
42592
+ otherClasses.add(declared);
42593
+ }
42594
+ }
42595
+ }
42596
+ }
42365
42597
  const sites = [];
42366
- for (const [script, source] of scripts) {
42367
- for (const site of callSitesOf(source, bare)) {
42598
+ for (const [script, source] of listed) {
42599
+ for (const site of callSitesOf(source, bare, otherClasses)) {
42368
42600
  const within = site.within === null ? undefined : rows.find((row) => row.script === script && bareName(row.function) === site.within);
42369
42601
  sites.push({
42370
42602
  script,
42371
42603
  ...site,
42372
- ...within === undefined ? {} : { withinCalls: within.calls, withinSelfMs: within.selfMs, withinTotalMs: within.totalMs }
42604
+ ...within === undefined ? {} : {
42605
+ withinCalls: within.calls,
42606
+ withinSelfMs: within.selfMs,
42607
+ withinTotalMs: within.totalMs,
42608
+ ...within.selfIncludesSuper === true ? { withinSelfIncludesSuper: true } : {}
42609
+ }
42373
42610
  });
42374
42611
  }
42375
42612
  }
@@ -42406,8 +42643,14 @@ import { setTimeout as delay4 } from "node:timers/promises";
42406
42643
  import { fileURLToPath as fileURLToPath5 } from "node:url";
42407
42644
  import { promisify as promisify5 } from "node:util";
42408
42645
  function editorRestartTimeoutMs() {
42409
- const override = Number.parseInt(envValue("GDHARNESS_EDITOR_RESTART_TIMEOUT_MS") ?? "", 10);
42410
- return Number.isInteger(override) && override > 0 ? override : 90000;
42646
+ return waitFromEnv("GDHARNESS_EDITOR_RESTART_TIMEOUT_MS", 90000);
42647
+ }
42648
+ function waitFromEnv(variable, fallback) {
42649
+ const override = Number.parseInt(envValue(variable) ?? "", 10);
42650
+ return Number.isInteger(override) && override > 0 ? override : fallback;
42651
+ }
42652
+ function greetingWaitMs() {
42653
+ return waitFromEnv("GDHARNESS_GREETING_WAIT_MS", 1e4);
42411
42654
  }
42412
42655
  function restartUnderway(note, now = Date.now()) {
42413
42656
  return alive(note.byPid) && now - Date.parse(note.quitAt) < editorRestartTimeoutMs() + 30000;
@@ -42458,6 +42701,14 @@ function captureDetail(payload) {
42458
42701
  function patienceForFrames(frames, atLeast) {
42459
42702
  return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
42460
42703
  }
42704
+ function longerWaitNote(tool, op, waitedMs) {
42705
+ const spec = TOOL_SPECS.find((one) => one.name === tool);
42706
+ if (spec !== undefined && argumentsOf(spec, op).includes("timeoutMs")) {
42707
+ return "timeoutMs waits longer next time.";
42708
+ }
42709
+ const how = tool === "runtime_wait" && op === "frames" ? `the wait is counted from the number of frames, at ${SLOWEST_FRAME_RATE} a second on top of GDHARNESS_RUNTIME_TIMEOUT_MS: a game drawing frames more slowly than that is waited on in fewer frames at a time, and a game inside one long frame, such as a long _ready, answers no call however few frames are asked for` : "the wait is GDHARNESS_RUNTIME_TIMEOUT_MS, which is set in the environment the server is started with";
42710
+ return `${tool} ${op} takes no timeoutMs: it was given ${waitedMs}ms, and ${how}.`;
42711
+ }
42461
42712
  function projectSpelling(projectPath, absolutePath) {
42462
42713
  return `res://${relative5(realPathOr(projectPath), realPathOr(absolutePath)).replaceAll("\\", "/")}`;
42463
42714
  }
@@ -42537,7 +42788,10 @@ function runIsUp(run, editorSays, editorPlay = null, now = Date.now()) {
42537
42788
  function noCodeWillCome(run) {
42538
42789
  return !exited(run);
42539
42790
  }
42540
- async function untilGone(pids, withinMs = STOP_WAIT_MS) {
42791
+ function stopWaitMs() {
42792
+ return waitFromEnv("GDHARNESS_STOP_WAIT_MS", 1e4);
42793
+ }
42794
+ async function untilGone(pids, withinMs = stopWaitMs()) {
42541
42795
  const gone = () => pids.every((pid) => !alive(pid));
42542
42796
  const deadline = Date.now() + withinMs;
42543
42797
  while (!gone() && Date.now() < deadline) {
@@ -42592,7 +42846,7 @@ function stopVerdict(stop) {
42592
42846
  return {
42593
42847
  stopped: false,
42594
42848
  stillGoing: stop.endedPid,
42595
- note: `${told} ${STOP_WAIT_MS / 1000} seconds later, so it has not stopped. editor_status says when it has gone, and editor_run stop asks again.`,
42849
+ note: `${told} ${stopWaitMs() / 1000} seconds later, so it has not stopped. editor_status says when it has gone, and editor_run stop asks again.`,
42596
42850
  withChildren: true
42597
42851
  };
42598
42852
  }
@@ -42710,7 +42964,7 @@ function endedToStartThis(ended) {
42710
42964
  return ` ${which} was not ended here: the editor could not be asked to stop it. The editor stops a play before starting the next, so it is not running beside this one if this start reached the editor.`;
42711
42965
  }
42712
42966
  if (ended.ending === "lingering") {
42713
- return ` ${which} was told to end to start this one and was still running ${STOP_WAIT_MS / 1000} seconds later, so it is running beside this one: editor_status says when it has gone.`;
42967
+ return ` ${which} was told to end to start this one and was still running ${stopWaitMs() / 1000} seconds later, so it is running beside this one: editor_status says when it has gone.`;
42714
42968
  }
42715
42969
  return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.`;
42716
42970
  }
@@ -42827,6 +43081,9 @@ function outsideTheirRanges(spec, args) {
42827
43081
  }
42828
43082
  return complaints;
42829
43083
  }
43084
+ function scanWaitMs() {
43085
+ return waitFromEnv("GDHARNESS_SCAN_WAIT_MS", 30000);
43086
+ }
42830
43087
  function scanWaitIn(payload) {
42831
43088
  const waited = readNumber(payload, "waitedForEditorScanMs");
42832
43089
  const note = readString(payload, "scanNote");
@@ -42885,7 +43142,7 @@ function scanWaitAnswer(scanned, started = "game") {
42885
43142
  if (scanned.waitedMs === 0) {
42886
43143
  return {};
42887
43144
  }
42888
- const still = `The editor was still scanning the project after ${SCAN_WAIT_MS / 1000} seconds, so`;
43145
+ const still = `The editor was still scanning the project after ${scanWaitMs() / 1000} seconds, so`;
42889
43146
  return {
42890
43147
  waitedForEditorScanMs: scanned.waitedMs,
42891
43148
  scanNote: !scanned.stillScanning ? undefined : started === "game" ? `${still} this game started during the scan and may have read a class cache being rewritten. If ` + 'editor_output shows "Could not find type" for a class that is declared, editor_rescan ' + "waits for the scan to finish, and a start after it reads the finished cache." : `${still} the engine answering this started during the scan and may have read a class cache being ` + "rewritten, and a class cache it wrote can be written over when the scan finishes. If " + 'engine_messages shows "Could not find type" for a class that is declared, editor_rescan waits ' + "for the scan to finish, and the same call after it reads the finished cache."
@@ -43140,14 +43397,20 @@ function profileAnswer(reading, limit) {
43140
43397
  const table = profiledFunctions(totals, limit, reading.sourceOf);
43141
43398
  const notes = [];
43142
43399
  const addonNote = reading.runtimeAddon ? "" : " This project has no gdharness runtime addon, which switches the profiler off as the game quits so the engine sends its totals: setup installs it.";
43143
- if (totals.complete) {} else if (reading.running && totals.frames === 0) {
43400
+ const totalsWithheld = reading.runtimeAddon ? " The runtime addon switches the profiler off as the game quits, and did not here: either it was no longer running in this game by then, which a project can arrange for some scenes; or the game ended without taking its tree down, as a crash or a kill does; or the totals did not reach the debugger before the game exited, which the game then says in its output." : addonNote;
43401
+ if (totals.complete && reading.running) {
43402
+ notes.push(`The engine's totals arrived while the game is still running, which it sends only when the profiler is switched off, so ${reading.through === "editor" ? "the editor's Profiler tab or the game itself, through EngineDebugger," : "the game itself, through EngineDebugger,"} switched it off before the game ended. They cover the run up to then and nothing after.`);
43403
+ } else if (totals.complete) {} else if (reading.running && totals.frames === 0) {
43144
43404
  notes.push(`The profiler is on and the game has not finished its first frame yet: a frame is sent as it ends, so a scene doing its work in _ready shows nothing until that returns, and the engine's own totals arrive when the game quits.${addonNote}`);
43145
43405
  } else if (reading.running) {
43146
43406
  notes.push(`The run is still going, so these are the ${totals.frames === 1 ? "one frame" : `${totals.frames} frames`} summed so far${reading.through === "gdharness" ? ", written at most half a second behind" : ""}.`);
43147
43407
  } else if (totals.frames === 0) {
43148
- notes.push(`The game connected and ended before a frame of the profile reached ${reading.through === "editor" ? "the editor" : "this server"}: the engine sends its totals only when the profiler is switched off, and stops sending as it exits without emptying what it has queued.${addonNote}`);
43408
+ notes.push(`The game connected and ended before a frame of the profile reached ${reading.through === "editor" ? "the editor" : "this server"}: the engine sends its totals only when the profiler is switched off, and stops sending as it exits without emptying what it has queued.${totalsWithheld}`);
43149
43409
  } else {
43150
- notes.push(`These are the ${totals.frames === 1 ? "one frame" : `${totals.frames} frames`} that arrived, summed, without the engine's own totals, so what the game did after the last of them is not in it: the engine sends its totals only when the profiler is switched off, and stops sending as it exits without emptying what it has queued.${addonNote}`);
43410
+ notes.push(`These are the ${totals.frames === 1 ? "one frame" : `${totals.frames} frames`} that arrived, summed, without the engine's own totals, which it sends only when the profiler is switched off.${totalsWithheld} Not in them is any script time after the last frame sent: a frame still queued as the game exited, since the engine stops sending without emptying its queue, and what scripts did as it quit, such as in _exit_tree. That can be nothing: a scene that does its work in _ready and quits has all of it in its first frame.`);
43411
+ }
43412
+ if (table.superCallers > 0) {
43413
+ notes.push(`${table.superCallers === 1 ? "One function calls" : `${table.superCallers} functions call`} through super, marked selfIncludesSuper: the engine takes every other call out of the caller's self time, and not a super call, so ${table.superCallers === 1 ? "its" : "their"} selfMs holds the overridden function's time, which that function's own row counts again. Self time therefore cannot be added along an override chain, and scriptMs counts the time under a super call once more for each level above it.`);
43151
43414
  }
43152
43415
  notes.push(...cappedNotes(totals));
43153
43416
  if (totals.unreadable > 0) {
@@ -43884,7 +44147,7 @@ class GodotServer {
43884
44147
  }
43885
44148
  async dispatch(tool, op, args) {
43886
44149
  if (tool !== "editor_status" && this.godotBridge.isArriving()) {
43887
- await this.waitForBridge(() => !this.godotBridge.isArriving(), Date.now() + GREETING_WAIT_MS);
44150
+ await this.waitForBridge(() => !this.godotBridge.isArriving(), Date.now() + greetingWaitMs());
43888
44151
  }
43889
44152
  if (ENGINE_PASSES[tool]?.[op] !== undefined) {
43890
44153
  return op === "reimport" ? await this.handleReimport(args) : await this.handleRefreshUids(args);
@@ -44044,7 +44307,7 @@ class GodotServer {
44044
44307
  depth: depth ?? 3,
44045
44308
  include_properties: readBoolean(args, "includeProperties") ?? false,
44046
44309
  properties: readArray(args, "properties") ?? []
44047
- }, patience);
44310
+ }, { tool: "runtime_inspect", op, patience });
44048
44311
  }
44049
44312
  case "find":
44050
44313
  return await this.handleFindRuntimeNodes(args, patience);
@@ -44054,17 +44317,17 @@ class GodotServer {
44054
44317
  root: readNonEmptyString(args, "nodePath") ?? "/root",
44055
44318
  include_hidden: readBoolean(args, "includeHidden") ?? false,
44056
44319
  limit: readPositiveNumber(args, "limit") ?? 500
44057
- }, patience);
44320
+ }, { tool: "runtime_inspect", op, patience });
44058
44321
  case "rect":
44059
- return await this.handleRuntimeCommand("get_rect", { ...whichGame(args), path: readNonEmptyString(args, "nodePath") ?? "" }, patience);
44322
+ return await this.handleRuntimeCommand("get_rect", { ...whichGame(args), path: readNonEmptyString(args, "nodePath") ?? "" }, { tool: "runtime_inspect", op, patience });
44060
44323
  case "property":
44061
44324
  return await this.handleRuntimeCommand("get_property", {
44062
44325
  ...whichGame(args),
44063
44326
  path: readNonEmptyString(args, "nodePath") ?? "",
44064
44327
  property: readNonEmptyString(args, "property") ?? ""
44065
- }, patience);
44328
+ }, { tool: "runtime_inspect", op, patience });
44066
44329
  default:
44067
- return await this.handleRuntimeCommand("get_metrics", { ...whichGame(args), metrics: readArray(args, "metrics") ?? [] }, patience);
44330
+ return await this.handleRuntimeCommand("get_metrics", { ...whichGame(args), metrics: readArray(args, "metrics") ?? [] }, { tool: "runtime_inspect", op, patience });
44068
44331
  }
44069
44332
  }
44070
44333
  case "runtime_invoke": {
@@ -44077,16 +44340,16 @@ class GodotServer {
44077
44340
  path: readNonEmptyString(args, "nodePath") ?? "",
44078
44341
  property: readString(args, "property") ?? "",
44079
44342
  value: args["value"]
44080
- }, patience) : await this.handleRuntimeCommand("call_method", {
44343
+ }, { tool: "runtime_invoke", op, patience }) : await this.handleRuntimeCommand("call_method", {
44081
44344
  ...whichGame(args),
44082
44345
  path: readNonEmptyString(args, "nodePath") ?? "",
44083
44346
  method: readString(args, "method") ?? "",
44084
44347
  args: readArray(args, "args") ?? [],
44085
44348
  ...args["properties"] === undefined ? {} : { properties: args["properties"] }
44086
- }, patience);
44349
+ }, { tool: "runtime_invoke", op, patience });
44087
44350
  }
44088
44351
  case "runtime_capture":
44089
- return await this.handleRuntimeCommand(op === "screenshot" ? "capture_screenshot" : "capture_viewport", args);
44352
+ return await this.handleRuntimeCommand(op === "screenshot" ? "capture_screenshot" : "capture_viewport", args, { tool: "runtime_capture", op });
44090
44353
  case "runtime_input":
44091
44354
  if (op === "click") {
44092
44355
  if (readNonEmptyString(args, "nodePath") === undefined && readNonEmptyString(args, "says") === undefined) {
@@ -44099,7 +44362,7 @@ class GodotServer {
44099
44362
  double: readBoolean(args, "doubleClick") ?? false,
44100
44363
  ...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
44101
44364
  ...args["index"] === undefined ? {} : { index: args["index"] }
44102
- });
44365
+ }, { tool: "runtime_input", op });
44103
44366
  }
44104
44367
  if (op === "choose") {
44105
44368
  return await this.handleRuntimeCommand("choose", {
@@ -44107,9 +44370,9 @@ class GodotServer {
44107
44370
  path: readNonEmptyString(args, "nodePath") ?? "",
44108
44371
  text: readString(args, "text") ?? "",
44109
44372
  ...args["index"] === undefined ? {} : { index: args["index"] }
44110
- });
44373
+ }, { tool: "runtime_input", op });
44111
44374
  }
44112
- return await this.handleRuntimeCommand(`inject_${op}`, args);
44375
+ return await this.handleRuntimeCommand(`inject_${op}`, args, { tool: "runtime_input", op });
44113
44376
  case "runtime_wait":
44114
44377
  return await this.handleRuntimeWait(op, args);
44115
44378
  case "debug_breakpoint": {
@@ -44338,7 +44601,7 @@ class GodotServer {
44338
44601
  const answered = Date.now();
44339
44602
  if (status["scanning"] === true || status["importing"] === true || status["pending"] === true) {
44340
44603
  sawAScan = true;
44341
- if (Date.now() - started >= SCAN_WAIT_MS) {
44604
+ if (Date.now() - started >= scanWaitMs()) {
44342
44605
  return { waitedMs: Date.now() - started, stillScanning: true };
44343
44606
  }
44344
44607
  await new Promise((settle) => setTimeout(settle, 100));
@@ -45632,15 +45895,17 @@ class GodotServer {
45632
45895
  hold: current === null ? {} : await this.holdOf(current, active)
45633
45896
  };
45634
45897
  })();
45635
- const [games, { playing, active, run, hold }, scan, editorStatus] = await Promise.all([
45898
+ const [games, { playing, active, run, hold }, scan, editorStatus, servers] = await Promise.all([
45636
45899
  pinged,
45637
45900
  asked,
45638
45901
  this.editorScanState(),
45639
- this.getEditorStatusPayload()
45902
+ this.getEditorStatusPayload(),
45903
+ this.editorServersListening()
45640
45904
  ]);
45641
45905
  return this.jsonTextResponse({
45642
45906
  editor: {
45643
45907
  ...editorStatus,
45908
+ ...servers,
45644
45909
  debugPort: playing?.debugPort ?? this.godotBridge.getStatus().debugPort,
45645
45910
  scan
45646
45911
  },
@@ -45666,6 +45931,30 @@ class GodotServer {
45666
45931
  }
45667
45932
  });
45668
45933
  }
45934
+ async editorServersListening() {
45935
+ const status = this.godotBridge.getStatus();
45936
+ const { lspPort, dapPort } = status;
45937
+ if (!status.connected || lspPort === undefined || dapPort === undefined) {
45938
+ return {};
45939
+ }
45940
+ const [lsp, dap] = await Promise.all([acceptsConnections(lspPort), acceptsConnections(dapPort)]);
45941
+ const greetedAgoMs = status.connectedAt === undefined ? null : Date.now() - status.connectedAt.getTime();
45942
+ return {
45943
+ ...lsp === null ? {} : { lspListening: lsp },
45944
+ ...dap === null ? {} : { dapListening: dap },
45945
+ serversNote: notListeningNote({ lsp: { port: lspPort, listening: lsp }, dap: { port: dapPort, listening: dap } }, greetedAgoMs)
45946
+ };
45947
+ }
45948
+ async editorServersOnceStarted() {
45949
+ const deadline = Date.now() + EDITOR_SERVERS_START_MS;
45950
+ for (;; ) {
45951
+ const answer = await this.editorServersListening();
45952
+ if (answer.serversNote === undefined || Date.now() >= deadline) {
45953
+ return answer;
45954
+ }
45955
+ await delay4(500);
45956
+ }
45957
+ }
45669
45958
  settingKeysOf(projectPath) {
45670
45959
  if (projectPath === undefined) {
45671
45960
  return null;
@@ -45685,7 +45974,7 @@ class GodotServer {
45685
45974
  ]);
45686
45975
  }
45687
45976
  if (!hasSaidWhoItIs(before)) {
45688
- return this.createErrorResponse(`The editor has connected and has not said who it is within ${GREETING_WAIT_MS / 1000}s, so whether this server or the editor itself restarts it cannot be told.`, ["editor_status says when it has, under greeting while it has not", "Then ask for the restart again"]);
45977
+ return this.createErrorResponse(`The editor has connected and has not said who it is within ${greetingWaitMs() / 1000}s, so whether this server or the editor itself restarts it cannot be told.`, ["editor_status says when it has, under greeting while it has not", "Then ask for the restart again"]);
45689
45978
  }
45690
45979
  const mine = before.openedByAServer === true && before.projectPath !== undefined;
45691
45980
  if (!mine && args["hidden"] === true) {
@@ -45709,6 +45998,7 @@ class GodotServer {
45709
45998
  "editor_status says whether anything has reached the bridge since"
45710
45999
  ]);
45711
46000
  }
46001
+ const servers = await this.editorServersOnceStarted();
45712
46002
  const now = this.godotBridge.getStatus();
45713
46003
  const after = this.settingKeysOf(before.projectPath);
45714
46004
  return this.jsonTextResponse({
@@ -45719,6 +46009,7 @@ class GodotServer {
45719
46009
  addonIsStale: editorIsStale(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest()),
45720
46010
  staleNote: addonMismatch(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest(), this.editorCodeOnDisk()),
45721
46011
  ...this.whatTheEditorDropped(settingsBefore, after),
46012
+ ...servers,
45722
46013
  tookMs: Date.now() - began
45723
46014
  });
45724
46015
  }
@@ -46115,6 +46406,7 @@ class GodotServer {
46115
46406
  } catch (error) {
46116
46407
  return this.createErrorResponse(`The editor is connected but its debug adapter is not: ${errorMessage(error)}`, [
46117
46408
  `This asked on port ${this.dap().port}, which is where the connected editor says it serves`,
46409
+ "Godot binds its debug adapter once, as the editor starts, and reports a failure only in the editor's own log panel: editor_launch restart binds it again",
46118
46410
  "GDHARNESS_DAP_PORT points this server at another one"
46119
46411
  ]);
46120
46412
  }
@@ -46300,7 +46592,7 @@ class GodotServer {
46300
46592
  game.brokeOn = halt.text;
46301
46593
  game.log.record("error", halt.text);
46302
46594
  }
46303
- async untilTheEditorStopsPlaying(withinMs = STOP_WAIT_MS) {
46595
+ async untilTheEditorStopsPlaying(withinMs = stopWaitMs()) {
46304
46596
  const deadline = Date.now() + withinMs;
46305
46597
  for (;; ) {
46306
46598
  if ((await this.editorPlayingState())?.playing === false) {
@@ -46394,7 +46686,7 @@ class GodotServer {
46394
46686
  running.endedThrough = "signal";
46395
46687
  }
46396
46688
  clearTheRecord();
46397
- running.log.record(gone ? "info" : "warning", declined !== null ? `gdharness could not signal pid ${running.pid}: the operating system refused it (${declined}). It had described that process as ${described}.` : !gone ? `gdharness signalled pid ${running.pid}, which the operating system described as ${described}, and it was still running ${STOP_WAIT_MS / 1000} seconds later.` : landed ? `gdharness ended pid ${running.pid}, which the operating system described as ${described}.` : `gdharness signalled pid ${running.pid} and it was already gone; the operating system had described it as ${described}.`);
46689
+ running.log.record(gone ? "info" : "warning", declined !== null ? `gdharness could not signal pid ${running.pid}: the operating system refused it (${declined}). It had described that process as ${described}.` : !gone ? `gdharness signalled pid ${running.pid}, which the operating system described as ${described}, and it was still running ${stopWaitMs() / 1000} seconds later.` : landed ? `gdharness ended pid ${running.pid}, which the operating system described as ${described}.` : `gdharness signalled pid ${running.pid} and it was already gone; the operating system had described it as ${described}.`);
46398
46690
  return gone ? "gone" : "lingering";
46399
46691
  }
46400
46692
  async endThroughTheKeeper(run, pid, keeper) {
@@ -46414,7 +46706,7 @@ class GodotServer {
46414
46706
  run.exitCode = null;
46415
46707
  run.exitSignal = "SIGTERM";
46416
46708
  }
46417
- run.log.record(gone ? "info" : "warning", !gone ? `gdharness had the keeper holding pid ${pid} end it, and it was still running ${STOP_WAIT_MS / 1000} seconds later.` : asked === "signalled" ? `gdharness ended pid ${pid} through the keeper holding it.` : `gdharness asked the keeper holding pid ${pid} to end it, and it had already gone.`);
46709
+ run.log.record(gone ? "info" : "warning", !gone ? `gdharness had the keeper holding pid ${pid} end it, and it was still running ${stopWaitMs() / 1000} seconds later.` : asked === "signalled" ? `gdharness ended pid ${pid} through the keeper holding it.` : `gdharness asked the keeper holding pid ${pid} to end it, and it had already gone.`);
46418
46710
  return gone ? "gone" : "lingering";
46419
46711
  }
46420
46712
  spawnGame(godotPath, cmdArgs, env) {
@@ -47339,7 +47631,7 @@ class GodotServer {
47339
47631
  ...landed ? { endedThrough: "signal" } : {},
47340
47632
  ...childrenFields(ended),
47341
47633
  exitedBeforeStop: !landed,
47342
- note: `${!gone ? `pid ${pid} was told to end and was still running ${STOP_WAIT_MS / 1000} seconds later, so it has not stopped: a game of this project that no server here was holding,` : landed ? `pid ${pid} was ended: a game of this project that no server here was holding,` : `pid ${pid} had already gone when it was signalled: a game of this project that no server here was holding,`} identified by its announcement and by its command line, ${read.executable} run with --path ${read.projectPath ?? ""}. Nothing here read what it printed or holds its exit code, so neither is in this answer.${gone ? "" : " editor_status says when it has gone."} ${aboutTheChildren(ended, false)}`
47634
+ note: `${!gone ? `pid ${pid} was told to end and was still running ${stopWaitMs() / 1000} seconds later, so it has not stopped: a game of this project that no server here was holding,` : landed ? `pid ${pid} was ended: a game of this project that no server here was holding,` : `pid ${pid} had already gone when it was signalled: a game of this project that no server here was holding,`} identified by its announcement and by its command line, ${read.executable} run with --path ${read.projectPath ?? ""}. Nothing here read what it printed or holds its exit code, so neither is in this answer.${gone ? "" : " editor_status says when it has gone."} ${aboutTheChildren(ended, false)}`
47343
47635
  });
47344
47636
  }
47345
47637
  async whatTheRunStarted(run) {
@@ -48075,7 +48367,8 @@ class GodotServer {
48075
48367
  }
48076
48368
  return `The game this server started (${which}) is still coming up and has not announced its runtime after a wait of ${Math.min(budgetMs, remaining)}ms, so there is nothing to talk to yet. It is still running: ask again in a moment rather than starting another.`;
48077
48369
  }
48078
- async handleRuntimeCommand(command, args, timeoutMs = this.runtimeTimeoutMs()) {
48370
+ async handleRuntimeCommand(command, args, asked) {
48371
+ const timeoutMs = asked.patience ?? this.runtimeTimeoutMs();
48079
48372
  const { op: _op, projectPath, pid, outputPath, ...params } = asParams(args);
48080
48373
  const waited = typeof pid === "number" ? null : await this.ownRunOnItsWay(typeof projectPath === "string" ? projectPath : this.ownProject, timeoutMs);
48081
48374
  if (waited !== null) {
@@ -48128,7 +48421,7 @@ ${staleNote}`);
48128
48421
  requestId: reply.requestId,
48129
48422
  command,
48130
48423
  waitedMs: timeoutMs,
48131
- note: reply.message,
48424
+ note: `${reply.message} ${longerWaitNote(asked.tool, asked.op, timeoutMs)}`,
48132
48425
  ...answeredBy,
48133
48426
  ...stale
48134
48427
  });
@@ -48228,13 +48521,13 @@ ${staleNote}`}`
48228
48521
  limit: readPositiveNumber(args, "limit") ?? 100,
48229
48522
  ...property === undefined ? {} : { property },
48230
48523
  ...includeHidden === undefined ? {} : { include_hidden: includeHidden }
48231
- }, patience);
48524
+ }, { tool: "runtime_inspect", op: "find", patience });
48232
48525
  }
48233
48526
  async handleRuntimeWait(op, args) {
48234
48527
  if (op === "frames") {
48235
48528
  const frames = readPositiveNumber(args, "frames") ?? 1;
48236
48529
  const waited = patienceForFrames(frames, this.runtimeTimeoutMs());
48237
- return await this.handleRuntimeCommand("wait_frames", { ...whichGame(args), frames }, waited);
48530
+ return await this.handleRuntimeCommand("wait_frames", { ...whichGame(args), frames }, { tool: "runtime_wait", op, patience: waited });
48238
48531
  }
48239
48532
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 5000;
48240
48533
  const asked = readNonEmptyString(args, "nodePath");
@@ -48248,7 +48541,7 @@ ${staleNote}`}`
48248
48541
  path: nodePath,
48249
48542
  signal: readString(args, "signal") ?? "",
48250
48543
  timeout_ms: timeoutMs
48251
- }, patience) : await this.handleRuntimeCommand("wait_until", {
48544
+ }, { tool: "runtime_wait", op, patience }) : await this.handleRuntimeCommand("wait_until", {
48252
48545
  ...whichGame(args),
48253
48546
  path: nodePath,
48254
48547
  property: readString(args, "property") ?? "",
@@ -48256,7 +48549,7 @@ ${staleNote}`}`
48256
48549
  ...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
48257
48550
  ...readBoolean(args, "includeHidden") === undefined ? {} : { include_hidden: readBoolean(args, "includeHidden") },
48258
48551
  timeout_ms: timeoutMs
48259
- }, patience);
48552
+ }, { tool: "runtime_wait", op, patience });
48260
48553
  }
48261
48554
  }
48262
48555
  async function runGodotServer() {
@@ -48264,7 +48557,7 @@ async function runGodotServer() {
48264
48557
  const server = new GodotServer;
48265
48558
  await server.run();
48266
48559
  }
48267
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, GREETING_WAIT_MS = 1e4, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, SCAN_WAIT_MS = 30000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "';
48560
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless it is called through that class by name and one class was asked about. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "';
48268
48561
  var init_server3 = __esm(() => {
48269
48562
  init_mcp();
48270
48563
  init_stdio2();
@@ -48296,6 +48589,7 @@ var init_server3 = __esm(() => {
48296
48589
  init_lsp_client();
48297
48590
  init_outside();
48298
48591
  init_paths();
48592
+ init_port_probe();
48299
48593
  init_ports();
48300
48594
  init_process_children();
48301
48595
  init_process_time();