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 +345 -51
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +9 -0
- package/build/godot/addons/gdharness_editor/tools/profile_tools.gd +16 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +60 -10
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +8 -1
- package/build/index.js +343 -53
- package/build/keeper.js +7 -0
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 ? {} : {
|
|
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
|
-
|
|
42410
|
-
|
|
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
|
-
|
|
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} ${
|
|
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 ${
|
|
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 ${
|
|
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
|
-
|
|
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.${
|
|
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,
|
|
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() +
|
|
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 >=
|
|
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 ${
|
|
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 =
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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,
|
|
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,
|
|
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();
|