gdharness 1.0.1 → 1.0.3
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 +276 -63
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +44 -9
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +123 -21
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +81 -46
- package/build/godot/operations/classdb_queries.gd +75 -0
- package/build/index.js +275 -61
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -7673,6 +7673,9 @@ function cacheWrittenAt(projectPath) {
|
|
|
7673
7673
|
return null;
|
|
7674
7674
|
}
|
|
7675
7675
|
}
|
|
7676
|
+
function cachedAtMissingPaths(cached, projectPath) {
|
|
7677
|
+
return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync6(join6(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
|
|
7678
|
+
}
|
|
7676
7679
|
function staleAgainst(cached, projectPath) {
|
|
7677
7680
|
return [...declaredClasses(projectPath)].filter(([name, path]) => cached.get(name) !== path).map(([name]) => name);
|
|
7678
7681
|
}
|
|
@@ -16084,7 +16087,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16084
16087
|
},
|
|
16085
16088
|
{
|
|
16086
16089
|
name: "editor_rescan",
|
|
16087
|
-
description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. The scan is a change-detecting walk rather than an unconditional reparse, so a file another engine has already imported reads as settled and the walk does not look inside it: its class_name then stays out of the list the editor resolves against, however many times you scan. Any class in that state is named under unseenByEditor, which is what no check on disk can see, since the declaration and the cache are both correct there and only the editor disagrees. The cure is this call on its own, with no change to the declaring script: measured against a real editor on three platforms, with idle waits of the same length ruled out so the scan is credited rather than the time it takes, and reproduced in a second project against its own reproduction in 203ms. editor_launch restart also does it and costs a window, and project_import refresh_classes does not: it rewrites the cache and does not touch what the editor is holding. A scan writes .godot/global_script_class_cache.cfg from the list the editor is holding, so a class the editor cannot resolve goes out of that file with it. That is not a reason to refuse the scan, because on a class the editor has merely not walked yet the scan is the cure; what the answer does instead is name the loss under cacheLost and rebuild the cache from the files, naming what came back under cacheRestored, so no fresh engine, CI run or clone inherits the short file. An editor that lost classes is still holding the short list, so the note says to restart it before scanning again. Needs the editor connected.",
|
|
16090
|
+
description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. The scan is a change-detecting walk rather than an unconditional reparse, so a file another engine has already imported reads as settled and the walk does not look inside it: its class_name then stays out of the list the editor resolves against, however many times you scan. Any class in that state is named under unseenByEditor, which is what no check on disk can see, since the declaration and the cache are both correct there and only the editor disagrees. The cure is this call on its own, with no change to the declaring script: measured against a real editor on three platforms, with idle waits of the same length ruled out so the scan is credited rather than the time it takes, and reproduced in a second project against its own reproduction in 203ms. editor_launch restart also does it and costs a window, and project_import refresh_classes does not: it rewrites the cache and does not touch what the editor is holding. A scan writes .godot/global_script_class_cache.cfg from the list the editor is holding, so a class the editor cannot resolve goes out of that file with it. That is not a reason to refuse the scan, because on a class the editor has merely not walked yet the scan is the cure; what the answer does instead is name the loss under cacheLost and rebuild the cache from the files, naming what came back under cacheRestored, so no fresh engine, CI run or clone inherits the short file. An editor that lost classes is still holding the short list, so the note says to restart it before scanning again. The other direction is guarded the same way: a class the editor still holds for a script that was renamed or deleted comes back into the cache at a path that is not there, and the next engine to read it fails on \"Could not parse global class\" in whichever correct script shares the bare name, so the cache is rebuilt without it and the answer names it under cacheDropped; the editor goes on holding it until it is restarted, and every scan until then ends in that rebuild. A script or shader written outside the editor has no .uid sidecar until something imports it, and the scan writes one beside it, which a project that commits sidecars needs before the commit. Needs the editor connected.",
|
|
16088
16091
|
parameters: {
|
|
16089
16092
|
projectPath: PROJECT_PATH,
|
|
16090
16093
|
timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." },
|
|
@@ -16120,12 +16123,20 @@ var init_tool_definitions = __esm(() => {
|
|
|
16120
16123
|
},
|
|
16121
16124
|
instantiableOnly: { type: "boolean", description: "query: leave out abstract classes. Default false." },
|
|
16122
16125
|
className: { type: "string", description: "info, inheritance: the class." },
|
|
16123
|
-
includeInherited: { type: "boolean", description: "info: include inherited members. Default false." }
|
|
16126
|
+
includeInherited: { type: "boolean", description: "info: include inherited members. Default false." },
|
|
16127
|
+
member: {
|
|
16128
|
+
type: "string",
|
|
16129
|
+
ops: ["info"],
|
|
16130
|
+
description: 'info: one member by name, a method, property, signal, enum or constant, with its signature and which class declares it, looked for on the class and every ancestor. The whole of Control is seventy thousand characters; the question "does this engine have it, and what does it take" is this. A name nothing declares is refused with the members whose names contain it.'
|
|
16131
|
+
}
|
|
16124
16132
|
},
|
|
16125
16133
|
requires: ["projectPath"],
|
|
16126
16134
|
operations: {
|
|
16127
16135
|
query: { summary: "classes matching a filter or category", requires: [] },
|
|
16128
|
-
info: {
|
|
16136
|
+
info: {
|
|
16137
|
+
summary: "methods, properties, signals and enums of one class, or one member of it by name",
|
|
16138
|
+
requires: ["className"]
|
|
16139
|
+
},
|
|
16129
16140
|
inheritance: { summary: "ancestors and descendants of one class", requires: ["className"] }
|
|
16130
16141
|
},
|
|
16131
16142
|
defaultOperation: "query"
|
|
@@ -16144,13 +16155,19 @@ var init_tool_definitions = __esm(() => {
|
|
|
16144
16155
|
property: {
|
|
16145
16156
|
type: "string",
|
|
16146
16157
|
ops: ["property", "find"],
|
|
16147
|
-
description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A negative number counts from the end. A step that is not there is named, and says what was there instead.'
|
|
16158
|
+
description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that takes no arguments and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. A step that is not there is named, and says what was there instead.'
|
|
16148
16159
|
},
|
|
16149
16160
|
depth: { type: "number", ops: ["tree"], description: "tree: levels to descend. Default 3." },
|
|
16150
16161
|
includeProperties: {
|
|
16151
16162
|
type: "boolean",
|
|
16152
16163
|
ops: ["tree"],
|
|
16153
|
-
description: "tree: include each node
|
|
16164
|
+
description: "tree: include every stored property of each node. Default false, and one row of a form answered seventy-six thousand characters with it; properties names the few that are wanted."
|
|
16165
|
+
},
|
|
16166
|
+
properties: {
|
|
16167
|
+
type: "array",
|
|
16168
|
+
items: { type: "string" },
|
|
16169
|
+
ops: ["tree"],
|
|
16170
|
+
description: "tree: the properties to read off each node, by name, so the text of every label and button under a panel is one call: a node that has not got one leaves it out. Colons read through what a node holds, as property does for find."
|
|
16154
16171
|
},
|
|
16155
16172
|
className: {
|
|
16156
16173
|
type: "string",
|
|
@@ -16219,7 +16236,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16219
16236
|
property: {
|
|
16220
16237
|
type: "string",
|
|
16221
16238
|
ops: ["set"],
|
|
16222
|
-
description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end. The answer reads back off the same holder, so a write a typed container refused shows as an unchanged value.'
|
|
16239
|
+
description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end, and a step written as a call, "get_viewport():gui_embed_subwindows", walks through what a method taking no arguments returned. The answer reads back off the same holder, so a write a typed container refused shows as an unchanged value.'
|
|
16223
16240
|
},
|
|
16224
16241
|
value: {
|
|
16225
16242
|
ops: ["set"],
|
|
@@ -16229,7 +16246,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16229
16246
|
method: {
|
|
16230
16247
|
type: "string",
|
|
16231
16248
|
ops: ["call"],
|
|
16232
|
-
description: 'call: which one to call. Colons call through what a node holds, "_game:run:advance", the same way a property is written through them,
|
|
16249
|
+
description: 'call: which one to call. Colons call through what a node holds, "_game:run:advance", the same way a property is written through them, a list or a map is stepped into by index or key, "_game:run:roster:0:retire", and a step written as a call walks through what a method taking no arguments returned, "get_viewport():gui_get_focus_owner".'
|
|
16233
16250
|
},
|
|
16234
16251
|
args: {
|
|
16235
16252
|
type: "array",
|
|
@@ -16289,7 +16306,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16289
16306
|
pressed: {
|
|
16290
16307
|
type: "boolean",
|
|
16291
16308
|
ops: ["action", "key", "mouse_click"],
|
|
16292
|
-
description: "action, key: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. mouse_click is one raw event, so it is down unless you say false."
|
|
16309
|
+
description: "action, key: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. mouse_click is one raw event, so it is down unless you say false; a wheel button down is a whole step, since a wheel is never held."
|
|
16293
16310
|
},
|
|
16294
16311
|
strength: { type: "number", ops: ["action"], description: "action: 0 to 1. Default 1." },
|
|
16295
16312
|
keycode: {
|
|
@@ -16339,7 +16356,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16339
16356
|
relativeX: {
|
|
16340
16357
|
type: "number",
|
|
16341
16358
|
ops: ["mouse_motion"],
|
|
16342
|
-
description: "mouse_motion: the movement the event carries. Left out, it is the distance from where the pointer
|
|
16359
|
+
description: "mouse_motion: the movement the event carries. Left out, it is the distance from where the last injected pointer event put the pointer, and none for the first; that is what a control that drags reads. Give it to send a motion the position does not show."
|
|
16343
16360
|
},
|
|
16344
16361
|
relativeY: {
|
|
16345
16362
|
type: "number",
|
|
@@ -16359,7 +16376,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16359
16376
|
},
|
|
16360
16377
|
action: { summary: "press an action, or hold it", requires: ["action"] },
|
|
16361
16378
|
key: {
|
|
16362
|
-
summary: "press a key, or hold it. Not for a menu a click has opened: an OptionButton or PopupMenu pops up as a window of its own that a key sent to the game never reaches, so an arrow moves nothing in it and Enter closes it with nothing chosen. choose is what selects there",
|
|
16379
|
+
summary: "press a key, or hold it. Not for a menu a click has opened: an OptionButton or PopupMenu pops up as a window of its own that a key sent to the game never reaches, so an arrow moves nothing in it and Enter closes it with nothing chosen. choose is what selects there. The limit is that menu's own navigation, not windows in general: an embedded dialog whose owner reads keys in _input gets them, and a binding catcher up in one takes the key first time",
|
|
16363
16380
|
requires: ["keycode"]
|
|
16364
16381
|
},
|
|
16365
16382
|
text: {
|
|
@@ -16367,11 +16384,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
16367
16384
|
requires: ["text"]
|
|
16368
16385
|
},
|
|
16369
16386
|
mouse_click: {
|
|
16370
|
-
summary: "one mouse button event at a position, carrying the buttons held once it has happened. A drag is a press held with pressed true, motions, and a release with pressed false",
|
|
16387
|
+
summary: "one mouse button event at a position, carrying the buttons held once it has happened. A drag is a press held with pressed true, motions, and a release with pressed false. A wheel button is a whole step, press and release together as a mouse sends one, and the answer says released: a lone wheel press holds the viewport on the control that took it, so every click after it lands there",
|
|
16371
16388
|
requires: ["x", "y"]
|
|
16372
16389
|
},
|
|
16373
16390
|
mouse_motion: {
|
|
16374
|
-
summary: "move the mouse to a position, carrying the distance from where
|
|
16391
|
+
summary: "move the mouse to a position, carrying the distance from where the last injected event put it unless relativeX and relativeY say otherwise, and the buttons held, so a control that drags moves under a run of these between a held mouse_click and its release",
|
|
16375
16392
|
requires: ["x", "y"]
|
|
16376
16393
|
}
|
|
16377
16394
|
}
|
|
@@ -16389,12 +16406,21 @@ var init_tool_definitions = __esm(() => {
|
|
|
16389
16406
|
},
|
|
16390
16407
|
nodePath: { type: "string", ops: ["signal", "until"], description: "signal, until: the node." },
|
|
16391
16408
|
signal: { type: "string", ops: ["signal"], description: "signal: the signal name." },
|
|
16392
|
-
property: {
|
|
16409
|
+
property: {
|
|
16410
|
+
type: "string",
|
|
16411
|
+
ops: ["until"],
|
|
16412
|
+
description: 'until: the property name, or a colon path through what the node holds, "_game:run:day", read the way runtime_inspect property reads one, a step written as a call included. Walked again on every frame, so a holder the game replaces while the wait is on is followed.'
|
|
16413
|
+
},
|
|
16393
16414
|
value: { ops: ["until"], description: "until: the value to wait for, fitted to the property's type." },
|
|
16394
16415
|
says: {
|
|
16395
16416
|
type: "string",
|
|
16396
16417
|
ops: ["until"],
|
|
16397
|
-
description: "until: wait for these words to appear anywhere under nodePath instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put. Case-insensitive, part of a line,
|
|
16418
|
+
description: "until: wait for these words to appear anywhere under nodePath instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put. Case-insensitive, part of a line, on a node the player can see unless includeHidden says otherwise; a label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Instead of, not as well as: a call carrying this and a property is refused, because they ask about different things and answering one of them silently is how a caller watches a screen believing they are watching a property."
|
|
16419
|
+
},
|
|
16420
|
+
includeHidden: {
|
|
16421
|
+
type: "boolean",
|
|
16422
|
+
ops: ["until"],
|
|
16423
|
+
description: "until, with says: count words on a hidden node too. Default false, since a wait for words is a wait for them to be shown: a button that exists hidden through a whole animation carries its words the whole time and satisfied the wait at once, and every wait on that screen fell back to counting frames."
|
|
16398
16424
|
},
|
|
16399
16425
|
timeoutMs: {
|
|
16400
16426
|
type: "number",
|
|
@@ -36440,18 +36466,59 @@ var init_lsp_client = __esm(() => {
|
|
|
36440
36466
|
});
|
|
36441
36467
|
|
|
36442
36468
|
// src/process-children.ts
|
|
36443
|
-
import { execFile as execFile5 } from "node:child_process";
|
|
36469
|
+
import { execFile as execFile5, execFileSync, spawnSync as spawnSync2 } from "node:child_process";
|
|
36444
36470
|
import { basename as basename2 } from "node:path";
|
|
36445
36471
|
import { promisify as promisify5 } from "node:util";
|
|
36472
|
+
function startTimesOf(pids) {
|
|
36473
|
+
const began = new Map;
|
|
36474
|
+
if (pids.length === 0) {
|
|
36475
|
+
return began;
|
|
36476
|
+
}
|
|
36477
|
+
try {
|
|
36478
|
+
if (process.platform === "win32") {
|
|
36479
|
+
const filter = pids.map((pid) => `ProcessId=${pid}`).join(" OR ");
|
|
36480
|
+
const said = execFileSync("powershell", [
|
|
36481
|
+
"-NoProfile",
|
|
36482
|
+
"-NonInteractive",
|
|
36483
|
+
"-Command",
|
|
36484
|
+
`Get-CimInstance Win32_Process -Filter "${filter}" | ForEach-Object { "$($_.ProcessId) $(([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds())" }`
|
|
36485
|
+
], { encoding: "utf8", timeout: ASK_TIMEOUT_MS, windowsHide: true });
|
|
36486
|
+
for (const line of said.split(`
|
|
36487
|
+
`)) {
|
|
36488
|
+
const found = /^\s*(\d+)\s+(\d+)\s*$/.exec(line);
|
|
36489
|
+
if (found?.[1] !== undefined && found[2] !== undefined) {
|
|
36490
|
+
began.set(Number(found[1]), Number(found[2]));
|
|
36491
|
+
}
|
|
36492
|
+
}
|
|
36493
|
+
return began;
|
|
36494
|
+
}
|
|
36495
|
+
const said = spawnSync2("ps", ["-p", pids.join(","), "-o", "pid=,lstart="], {
|
|
36496
|
+
encoding: "utf8",
|
|
36497
|
+
timeout: ASK_TIMEOUT_MS,
|
|
36498
|
+
env: { ...process.env, LC_ALL: "C" }
|
|
36499
|
+
}).stdout;
|
|
36500
|
+
for (const line of said.split(`
|
|
36501
|
+
`)) {
|
|
36502
|
+
const found = /^\s*(\d+)\s+(.+)$/.exec(line);
|
|
36503
|
+
const at = Date.parse(found?.[2]?.trim() ?? "");
|
|
36504
|
+
if (found?.[1] !== undefined && !Number.isNaN(at)) {
|
|
36505
|
+
began.set(Number(found[1]), at);
|
|
36506
|
+
}
|
|
36507
|
+
}
|
|
36508
|
+
} catch {}
|
|
36509
|
+
return began;
|
|
36510
|
+
}
|
|
36446
36511
|
async function processTree() {
|
|
36447
36512
|
try {
|
|
36448
|
-
|
|
36449
|
-
|
|
36513
|
+
if (process.platform === "win32") {
|
|
36514
|
+
return parseProcessTable(await askWindows(), true);
|
|
36515
|
+
}
|
|
36516
|
+
return parseProcessTable(await askPosix());
|
|
36450
36517
|
} catch {
|
|
36451
36518
|
return;
|
|
36452
36519
|
}
|
|
36453
36520
|
}
|
|
36454
|
-
function parseProcessTable(printed) {
|
|
36521
|
+
function parseProcessTable(printed, started = false) {
|
|
36455
36522
|
const tree = new Map;
|
|
36456
36523
|
for (const line of printed.split(/\r?\n/)) {
|
|
36457
36524
|
const fields = /^\s*(\d+)\s+(\d+)(?:\s+(.*))?$/.exec(line);
|
|
@@ -36460,16 +36527,38 @@ function parseProcessTable(printed) {
|
|
|
36460
36527
|
}
|
|
36461
36528
|
const pid = Number(fields[1]);
|
|
36462
36529
|
const parent = Number(fields[2]);
|
|
36463
|
-
|
|
36464
|
-
|
|
36530
|
+
const rest = (fields[3] ?? "").trim();
|
|
36531
|
+
if (pid <= 0) {
|
|
36532
|
+
continue;
|
|
36533
|
+
}
|
|
36534
|
+
if (!started) {
|
|
36535
|
+
tree.set(pid, { parent, command: rest });
|
|
36536
|
+
continue;
|
|
36465
36537
|
}
|
|
36538
|
+
const timed = /^(\d+)(?:\s+(.*))?$/.exec(rest);
|
|
36539
|
+
if (timed === null) {
|
|
36540
|
+
continue;
|
|
36541
|
+
}
|
|
36542
|
+
const began = Number(timed[1]);
|
|
36543
|
+
tree.set(pid, { parent, command: (timed[2] ?? "").trim(), ...began > 0 ? { startedAt: began } : {} });
|
|
36466
36544
|
}
|
|
36467
36545
|
return tree;
|
|
36468
36546
|
}
|
|
36547
|
+
function linked(tree, child, parent) {
|
|
36548
|
+
const from = tree.get(child);
|
|
36549
|
+
const to = tree.get(parent);
|
|
36550
|
+
if (from === undefined || from.parent !== parent) {
|
|
36551
|
+
return false;
|
|
36552
|
+
}
|
|
36553
|
+
if (from.startedAt === undefined || to?.startedAt === undefined) {
|
|
36554
|
+
return true;
|
|
36555
|
+
}
|
|
36556
|
+
return from.startedAt >= to.startedAt;
|
|
36557
|
+
}
|
|
36469
36558
|
function childrenIn(tree, pid) {
|
|
36470
36559
|
const children = [];
|
|
36471
|
-
for (const
|
|
36472
|
-
if (
|
|
36560
|
+
for (const child of tree.keys()) {
|
|
36561
|
+
if (linked(tree, child, pid)) {
|
|
36473
36562
|
children.push(child);
|
|
36474
36563
|
}
|
|
36475
36564
|
}
|
|
@@ -36497,13 +36586,15 @@ function descendantsIn(tree, pid) {
|
|
|
36497
36586
|
function ancestorsIn(tree, pid, root) {
|
|
36498
36587
|
const chain = [];
|
|
36499
36588
|
const seen = new Set([pid]);
|
|
36589
|
+
let child = pid;
|
|
36500
36590
|
let parent = tree.get(pid)?.parent;
|
|
36501
|
-
while (parent !== undefined && !seen.has(parent)) {
|
|
36591
|
+
while (parent !== undefined && !seen.has(parent) && linked(tree, child, parent)) {
|
|
36502
36592
|
chain.push(parent);
|
|
36503
36593
|
if (parent === root) {
|
|
36504
36594
|
break;
|
|
36505
36595
|
}
|
|
36506
36596
|
seen.add(parent);
|
|
36597
|
+
child = parent;
|
|
36507
36598
|
parent = tree.get(parent)?.parent;
|
|
36508
36599
|
}
|
|
36509
36600
|
return chain;
|
|
@@ -36559,7 +36650,7 @@ async function askWindows() {
|
|
|
36559
36650
|
"-NoProfile",
|
|
36560
36651
|
"-NonInteractive",
|
|
36561
36652
|
"-Command",
|
|
36562
|
-
'Get-CimInstance Win32_Process | ForEach-Object { "$($_.ProcessId) $($_.ParentProcessId) $($_.CommandLine)" }'
|
|
36653
|
+
'Get-CimInstance Win32_Process | ForEach-Object { $began = 0; if ($_.CreationDate) { $began = ([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds() }; "$($_.ProcessId) $($_.ParentProcessId) $began $($_.CommandLine)" }'
|
|
36563
36654
|
], { timeout: ASK_TIMEOUT_MS, windowsHide: true, maxBuffer: 16 * 1024 * 1024 });
|
|
36564
36655
|
return stdout;
|
|
36565
36656
|
}
|
|
@@ -36791,7 +36882,7 @@ var init_restart_note = __esm(() => {
|
|
|
36791
36882
|
import { existsSync as existsSync13, readdirSync as readdirSync5, readFileSync as readFileSync15, statSync as statSync3, unlinkSync } from "node:fs";
|
|
36792
36883
|
import { createConnection as createConnection3 } from "node:net";
|
|
36793
36884
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
36794
|
-
import { join as join17, resolve as resolve4 } from "node:path";
|
|
36885
|
+
import { dirname as dirname11, join as join17, resolve as resolve4 } from "node:path";
|
|
36795
36886
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
36796
36887
|
function runtimeDirectory(variables = process.env) {
|
|
36797
36888
|
const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
|
|
@@ -36900,11 +36991,31 @@ function announcedAndGone(projectPath) {
|
|
|
36900
36991
|
const wanted = resolve4(projectPath);
|
|
36901
36992
|
return gone.filter((one) => one.noticedAt >= since && resolve4(one.project) === wanted);
|
|
36902
36993
|
}
|
|
36994
|
+
function strangersAmong(candidates, now, startTimes = startTimesOf) {
|
|
36995
|
+
const toJudge = candidates.filter((one) => {
|
|
36996
|
+
if (now - one.writtenAt < JUDGED_AFTER_MS) {
|
|
36997
|
+
return false;
|
|
36998
|
+
}
|
|
36999
|
+
const known = verdicts.get(one.file);
|
|
37000
|
+
return known === undefined || known.writtenAt !== one.writtenAt || !known.stranger && now - known.judgedAt > CONFIRMED_FOR_MS;
|
|
37001
|
+
});
|
|
37002
|
+
if (toJudge.length > 0) {
|
|
37003
|
+
const began = startTimes(toJudge.map((one) => one.pid));
|
|
37004
|
+
for (const one of toJudge) {
|
|
37005
|
+
const at = began.get(one.pid);
|
|
37006
|
+
const stranger = at !== undefined && at - one.writtenAt > STARTED_AFTER_ANNOUNCING_MS;
|
|
37007
|
+
verdicts.set(one.file, { writtenAt: one.writtenAt, judgedAt: now, stranger });
|
|
37008
|
+
}
|
|
37009
|
+
}
|
|
37010
|
+
return new Set(candidates.filter((one) => verdicts.get(one.file)?.stranger === true).map((one) => one.file));
|
|
37011
|
+
}
|
|
36903
37012
|
function announcedIn(directory) {
|
|
36904
37013
|
if (!existsSync13(directory)) {
|
|
36905
37014
|
return [];
|
|
36906
37015
|
}
|
|
36907
37016
|
const found = [];
|
|
37017
|
+
const candidates = [];
|
|
37018
|
+
const entries = [];
|
|
36908
37019
|
for (const entry of readdirSync5(directory)) {
|
|
36909
37020
|
const report = ERROR_REPORT_PATTERN.exec(entry);
|
|
36910
37021
|
if (report) {
|
|
@@ -36918,20 +37029,59 @@ function announcedIn(directory) {
|
|
|
36918
37029
|
const file = join17(directory, entry);
|
|
36919
37030
|
const pid = Number.parseInt(match[1] ?? "", 10);
|
|
36920
37031
|
const alive = processAlive(pid);
|
|
36921
|
-
|
|
37032
|
+
entries.push({ file, pid, alive });
|
|
37033
|
+
if (alive) {
|
|
37034
|
+
try {
|
|
37035
|
+
candidates.push({ file, pid, writtenAt: statSync3(file).mtimeMs });
|
|
37036
|
+
} catch {}
|
|
37037
|
+
}
|
|
37038
|
+
}
|
|
37039
|
+
const strangers = strangersAmong(candidates, Date.now());
|
|
37040
|
+
for (const { file, pid, alive } of entries) {
|
|
37041
|
+
const announced = alive && !strangers.has(file) ? parseAnnouncement(file, pid) : { kind: "rubbish" };
|
|
36922
37042
|
if (announced.kind === "rubbish") {
|
|
36923
|
-
if (!alive) {
|
|
37043
|
+
if (!alive || strangers.has(file)) {
|
|
36924
37044
|
wentAway(file, pid);
|
|
36925
37045
|
}
|
|
36926
37046
|
try {
|
|
36927
37047
|
unlinkSync(file);
|
|
36928
37048
|
} catch {}
|
|
37049
|
+
verdicts.delete(file);
|
|
36929
37050
|
continue;
|
|
36930
37051
|
}
|
|
36931
37052
|
found.push(announced);
|
|
36932
37053
|
}
|
|
37054
|
+
const held = new Set(entries.map((one) => one.file));
|
|
37055
|
+
const here = join17(directory, ".");
|
|
37056
|
+
for (const file of verdicts.keys()) {
|
|
37057
|
+
if (dirname11(file) === here && !held.has(file)) {
|
|
37058
|
+
verdicts.delete(file);
|
|
37059
|
+
}
|
|
37060
|
+
}
|
|
36933
37061
|
return found;
|
|
36934
37062
|
}
|
|
37063
|
+
async function announcementEnded(pid, directories = runtimeDirectories()) {
|
|
37064
|
+
const files = directories.map((directory) => join17(directory, `runtime-${pid}.json`)).filter((file) => existsSync13(file));
|
|
37065
|
+
const first = files[0];
|
|
37066
|
+
if (first === undefined) {
|
|
37067
|
+
return;
|
|
37068
|
+
}
|
|
37069
|
+
const until = Date.now() + ENDED_WITHIN_MS;
|
|
37070
|
+
while (processAlive(pid)) {
|
|
37071
|
+
const left = until - Date.now();
|
|
37072
|
+
if (left <= 0) {
|
|
37073
|
+
return;
|
|
37074
|
+
}
|
|
37075
|
+
await delay2(Math.min(LOOK_EVERY_MS, left));
|
|
37076
|
+
}
|
|
37077
|
+
wentAway(first, pid);
|
|
37078
|
+
for (const file of files) {
|
|
37079
|
+
try {
|
|
37080
|
+
unlinkSync(file);
|
|
37081
|
+
} catch {}
|
|
37082
|
+
verdicts.delete(file);
|
|
37083
|
+
}
|
|
37084
|
+
}
|
|
36935
37085
|
function sweepErrorReport(path, pid, now = Date.now()) {
|
|
36936
37086
|
if (processAlive(pid)) {
|
|
36937
37087
|
return;
|
|
@@ -37112,16 +37262,18 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
|
|
|
37112
37262
|
});
|
|
37113
37263
|
});
|
|
37114
37264
|
}
|
|
37115
|
-
var RUNTIME_PROTOCOL = 2, ANNOUNCEMENT_PATTERN, ERROR_REPORT_PATTERN, ERROR_REPORT_KEEP_MS, GONE_WINDOW_MS = 120000, GONE_KEPT = 32, gone, LOOK_EVERY_MS = 50, ANNOUNCE_BUDGET_MS = 5000, nextRequestId = 1;
|
|
37265
|
+
var RUNTIME_PROTOCOL = 2, ANNOUNCEMENT_PATTERN, ERROR_REPORT_PATTERN, ERROR_REPORT_KEEP_MS, GONE_WINDOW_MS = 120000, GONE_KEPT = 32, gone, STARTED_AFTER_ANNOUNCING_MS = 2000, CONFIRMED_FOR_MS = 60000, JUDGED_AFTER_MS = 60000, verdicts, ENDED_WITHIN_MS = 3000, LOOK_EVERY_MS = 50, ANNOUNCE_BUDGET_MS = 5000, nextRequestId = 1;
|
|
37116
37266
|
var init_runtime_client = __esm(() => {
|
|
37267
|
+
init_process_children();
|
|
37117
37268
|
ANNOUNCEMENT_PATTERN = /^runtime-(\d+)\.json$/;
|
|
37118
37269
|
ERROR_REPORT_PATTERN = /^runtime-(\d+)\.log$/;
|
|
37119
37270
|
ERROR_REPORT_KEEP_MS = 60 * 60 * 1000;
|
|
37120
37271
|
gone = [];
|
|
37272
|
+
verdicts = new Map;
|
|
37121
37273
|
});
|
|
37122
37274
|
|
|
37123
37275
|
// src/run-record.ts
|
|
37124
|
-
import { execFileSync } from "node:child_process";
|
|
37276
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
37125
37277
|
import { mkdirSync as mkdirSync9, openSync, readdirSync as readdirSync6, readFileSync as readFileSync16, rmSync as rmSync9, statSync as statSync4, writeFileSync as writeFileSync11 } from "node:fs";
|
|
37126
37278
|
import { basename as basename3, join as join18 } from "node:path";
|
|
37127
37279
|
function runsDirectory() {
|
|
@@ -37247,7 +37399,7 @@ function runningAs(pid) {
|
|
|
37247
37399
|
const raw = readFileSync16(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
37248
37400
|
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
37249
37401
|
}
|
|
37250
|
-
const args =
|
|
37402
|
+
const args = execFileSync2("ps", ["-p", String(pid), "-o", "args="], {
|
|
37251
37403
|
encoding: "utf8",
|
|
37252
37404
|
timeout: 15000
|
|
37253
37405
|
}).trim();
|
|
@@ -37259,20 +37411,14 @@ function runningAs(pid) {
|
|
|
37259
37411
|
function listeningPid(port) {
|
|
37260
37412
|
try {
|
|
37261
37413
|
if (process.platform === "win32") {
|
|
37262
|
-
|
|
37263
|
-
"-NoProfile",
|
|
37264
|
-
"-NonInteractive",
|
|
37265
|
-
"-Command",
|
|
37266
|
-
`(Get-NetTCPConnection -State Listen -LocalPort ${port} -ErrorAction SilentlyContinue | Select-Object -First 1).OwningProcess`
|
|
37267
|
-
], { encoding: "utf8", timeout: 15000 }).trim();
|
|
37268
|
-
return /^\d+$/.test(said) ? Number(said) : null;
|
|
37414
|
+
return listeningPidInNetstat(execFileSync2("netstat", ["-ano"], { encoding: "utf8", timeout: 15000 }), port);
|
|
37269
37415
|
}
|
|
37270
37416
|
for (const [command, args, pattern] of [
|
|
37271
37417
|
["lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], /^(\d+)/],
|
|
37272
37418
|
["ss", ["-ltnpH", `sport = :${port}`], /pid=(\d+)/]
|
|
37273
37419
|
]) {
|
|
37274
37420
|
try {
|
|
37275
|
-
const said =
|
|
37421
|
+
const said = execFileSync2(command, [...args], { encoding: "utf8", timeout: 15000 }).trim();
|
|
37276
37422
|
const found = pattern.exec(said);
|
|
37277
37423
|
if (found?.[1] !== undefined) {
|
|
37278
37424
|
return Number(found[1]);
|
|
@@ -37284,9 +37430,22 @@ function listeningPid(port) {
|
|
|
37284
37430
|
return null;
|
|
37285
37431
|
}
|
|
37286
37432
|
}
|
|
37433
|
+
function listeningPidInNetstat(printed, port) {
|
|
37434
|
+
for (const line of printed.split(/\r?\n/)) {
|
|
37435
|
+
const fields = /^\s*TCP\s+(\S+)\s+\S+\s+LISTENING\s+(\d+)\s*$/i.exec(line);
|
|
37436
|
+
if (fields === null) {
|
|
37437
|
+
continue;
|
|
37438
|
+
}
|
|
37439
|
+
const local = fields[1] ?? "";
|
|
37440
|
+
if (Number(local.slice(local.lastIndexOf(":") + 1)) === port) {
|
|
37441
|
+
return Number(fields[2]);
|
|
37442
|
+
}
|
|
37443
|
+
}
|
|
37444
|
+
return null;
|
|
37445
|
+
}
|
|
37287
37446
|
function windowsCommandLine(pid) {
|
|
37288
37447
|
try {
|
|
37289
|
-
const answer =
|
|
37448
|
+
const answer = execFileSync2("powershell", [
|
|
37290
37449
|
"-NoProfile",
|
|
37291
37450
|
"-NonInteractive",
|
|
37292
37451
|
"-Command",
|
|
@@ -37304,7 +37463,7 @@ function startedAt(pid, windowsAnswer) {
|
|
|
37304
37463
|
return Number.isNaN(when) ? null : when;
|
|
37305
37464
|
}
|
|
37306
37465
|
try {
|
|
37307
|
-
const said =
|
|
37466
|
+
const said = execFileSync2("ps", ["-p", String(pid), "-o", "lstart="], {
|
|
37308
37467
|
encoding: "utf8",
|
|
37309
37468
|
timeout: 15000
|
|
37310
37469
|
}).trim();
|
|
@@ -37316,7 +37475,7 @@ function startedAt(pid, windowsAnswer) {
|
|
|
37316
37475
|
}
|
|
37317
37476
|
function windowsImage(pid) {
|
|
37318
37477
|
try {
|
|
37319
|
-
const csv =
|
|
37478
|
+
const csv = execFileSync2("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
|
|
37320
37479
|
encoding: "utf8",
|
|
37321
37480
|
timeout: 15000,
|
|
37322
37481
|
windowsHide: true
|
|
@@ -37419,7 +37578,7 @@ import {
|
|
|
37419
37578
|
readSync
|
|
37420
37579
|
} from "node:fs";
|
|
37421
37580
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
37422
|
-
import { basename as basename4, dirname as
|
|
37581
|
+
import { basename as basename4, dirname as dirname12, join as join19, normalize as normalize3, relative as relative3 } from "node:path";
|
|
37423
37582
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
37424
37583
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
37425
37584
|
import { promisify as promisify7 } from "node:util";
|
|
@@ -37458,13 +37617,16 @@ function stillRunning(run) {
|
|
|
37458
37617
|
}
|
|
37459
37618
|
return true;
|
|
37460
37619
|
}
|
|
37461
|
-
function runIsUp(run, editorSays) {
|
|
37620
|
+
function runIsUp(run, editorSays, now = Date.now()) {
|
|
37462
37621
|
if (run?.throughEditor !== true) {
|
|
37463
37622
|
return stillRunning(run);
|
|
37464
37623
|
}
|
|
37465
37624
|
if (run.announcedPid !== undefined && !alive(run.announcedPid)) {
|
|
37466
37625
|
return false;
|
|
37467
37626
|
}
|
|
37627
|
+
if (editorSays === false && run.seenPlaying !== true && now - run.startedAt < PLAY_STARTS_WITHIN_MS) {
|
|
37628
|
+
return true;
|
|
37629
|
+
}
|
|
37468
37630
|
return editorSays ?? stillRunning(run);
|
|
37469
37631
|
}
|
|
37470
37632
|
function endedPreviousRun(ended) {
|
|
@@ -37538,10 +37700,11 @@ function runtimeVerdict(endpoint, after) {
|
|
|
37538
37700
|
heldAt: after.heldAt
|
|
37539
37701
|
};
|
|
37540
37702
|
}
|
|
37703
|
+
const state = after.playingSeen === false ? "the editor has not yet reported the game as playing, which a restarted editor does not until its scan is over" : "the game is still running";
|
|
37541
37704
|
return {
|
|
37542
37705
|
listening: false,
|
|
37543
37706
|
mayYetAnnounce: after.running,
|
|
37544
|
-
note: after.running ? `nothing announced itself within ${after.budgetMs}ms and
|
|
37707
|
+
note: after.running ? `nothing announced itself within ${after.budgetMs}ms and ${state}, so it may announce a moment from now: editor_status says whether it has, and editor_run start takes runtimeWaitMs to wait longer than this${after.withArgs ? ", which a run carrying its own arguments may well need, since whatever they ask the game to do before its first frame is inside this wait" : ""}` : `nothing announced itself within ${after.budgetMs}ms and the game is no longer running, so nothing is going to: editor_output has what it printed on the way down`,
|
|
37545
37708
|
heldAt: null
|
|
37546
37709
|
};
|
|
37547
37710
|
}
|
|
@@ -38230,7 +38393,8 @@ class GodotServer {
|
|
|
38230
38393
|
...whichGame(args),
|
|
38231
38394
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
38232
38395
|
depth: readPositiveNumber(args, "depth") ?? 3,
|
|
38233
|
-
include_properties: readBoolean(args, "includeProperties") ?? false
|
|
38396
|
+
include_properties: readBoolean(args, "includeProperties") ?? false,
|
|
38397
|
+
properties: readArray(args, "properties") ?? []
|
|
38234
38398
|
});
|
|
38235
38399
|
case "find":
|
|
38236
38400
|
return await this.handleFindRuntimeNodes(args);
|
|
@@ -38632,11 +38796,11 @@ class GodotServer {
|
|
|
38632
38796
|
if (!engine.ok) {
|
|
38633
38797
|
return engine.response;
|
|
38634
38798
|
}
|
|
38635
|
-
const folder =
|
|
38799
|
+
const folder = dirname12(output.absolutePath);
|
|
38636
38800
|
try {
|
|
38637
38801
|
mkdirSync10(folder, { recursive: true });
|
|
38638
38802
|
} catch (error) {
|
|
38639
|
-
return this.createErrorResponse(`Cannot create ${
|
|
38803
|
+
return this.createErrorResponse(`Cannot create ${dirname12(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
38640
38804
|
}
|
|
38641
38805
|
const debug = readBoolean(args, "debug") ?? false;
|
|
38642
38806
|
const exportLogs = mkdtempSync3(join19(tmpdir5(), "gdharness-export-"));
|
|
@@ -39090,11 +39254,14 @@ class GodotServer {
|
|
|
39090
39254
|
return null;
|
|
39091
39255
|
}
|
|
39092
39256
|
try {
|
|
39093
|
-
const
|
|
39094
|
-
|
|
39095
|
-
|
|
39096
|
-
]);
|
|
39257
|
+
const askedAt = Date.now();
|
|
39258
|
+
const asked = this.godotBridge.invokeTool("playing_status", {});
|
|
39259
|
+
const answer = await Promise.race([asked, delay3(PLAYING_STATUS_MS).then(() => null)]);
|
|
39097
39260
|
if (answer === null) {
|
|
39261
|
+
this.logDebug(`playing_status did not answer within ${PLAYING_STATUS_MS}ms`);
|
|
39262
|
+
asked.then(() => {
|
|
39263
|
+
this.logDebug(`playing_status answered after ${Date.now() - askedAt}ms`);
|
|
39264
|
+
}).catch(() => {});
|
|
39098
39265
|
return null;
|
|
39099
39266
|
}
|
|
39100
39267
|
return {
|
|
@@ -39414,6 +39581,9 @@ class GodotServer {
|
|
|
39414
39581
|
let asking = null;
|
|
39415
39582
|
let editorAskedAt = 0;
|
|
39416
39583
|
let editorSaysGoing = true;
|
|
39584
|
+
let editorAsks = 0;
|
|
39585
|
+
const waitBegan = Date.now();
|
|
39586
|
+
const lastSaid = () => editorSaysGoing ? "playing" : "not playing";
|
|
39417
39587
|
const endpoint = await announcedSince(projectPath, before, {
|
|
39418
39588
|
budgetMs,
|
|
39419
39589
|
accept: (one) => this.isTheGameOf(this.currentRun(), one, projectPath),
|
|
@@ -39427,6 +39597,7 @@ class GodotServer {
|
|
|
39427
39597
|
}
|
|
39428
39598
|
if (asking === null && Date.now() - editorAskedAt >= RUN_POLL_MS) {
|
|
39429
39599
|
editorAskedAt = Date.now();
|
|
39600
|
+
editorAsks += 1;
|
|
39430
39601
|
asking = this.runStillGoing(run).then((going) => {
|
|
39431
39602
|
editorSaysGoing = going;
|
|
39432
39603
|
}).finally(() => {
|
|
@@ -39436,6 +39607,7 @@ class GodotServer {
|
|
|
39436
39607
|
return !editorSaysGoing;
|
|
39437
39608
|
}
|
|
39438
39609
|
});
|
|
39610
|
+
this.logDebug(`announce wait ended after ${Date.now() - waitBegan}ms: ${endpoint === null ? "nothing announced" : `pid ${endpoint.pid} announced`}, editor asked ${editorAsks} times, last said ${lastSaid()}`);
|
|
39439
39611
|
const going = this.currentRun();
|
|
39440
39612
|
if (endpoint !== null && going !== null && going.announcedPid === undefined) {
|
|
39441
39613
|
going.announcedPid = endpoint.pid;
|
|
@@ -39445,7 +39617,8 @@ class GodotServer {
|
|
|
39445
39617
|
budgetMs,
|
|
39446
39618
|
heldAt: this.dapClient?.whereItStopped() ?? null,
|
|
39447
39619
|
running: endpoint !== null || going !== null && await this.runStillGoing(going),
|
|
39448
|
-
withArgs
|
|
39620
|
+
withArgs,
|
|
39621
|
+
...going?.throughEditor === true ? { playingSeen: going.seenPlaying === true } : {}
|
|
39449
39622
|
});
|
|
39450
39623
|
}
|
|
39451
39624
|
async playThroughEditor(scene, refreshedClasses, projectPath, alreadyPlaying, runtimeWaitMs, ended) {
|
|
@@ -39475,6 +39648,7 @@ class GodotServer {
|
|
|
39475
39648
|
const startedAt = Date.now();
|
|
39476
39649
|
const transcript = openTranscript(startedAt);
|
|
39477
39650
|
closeSync(transcript.fd);
|
|
39651
|
+
const playAnswer = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
|
|
39478
39652
|
const played = {
|
|
39479
39653
|
process: null,
|
|
39480
39654
|
pid: null,
|
|
@@ -39486,7 +39660,8 @@ class GodotServer {
|
|
|
39486
39660
|
exitCode: null,
|
|
39487
39661
|
throughEditor: true,
|
|
39488
39662
|
brokeOn: null,
|
|
39489
|
-
announcedBefore: alreadyPlaying
|
|
39663
|
+
announcedBefore: alreadyPlaying,
|
|
39664
|
+
seenPlaying: readBoolean(playAnswer, "playing") === true
|
|
39490
39665
|
};
|
|
39491
39666
|
this.activeProcess = played;
|
|
39492
39667
|
writeEditorRunNote({ projectPath, transcript: transcript.path, startedAt });
|
|
@@ -39496,7 +39671,7 @@ class GodotServer {
|
|
|
39496
39671
|
through: "editor",
|
|
39497
39672
|
scene: scene === null ? "the main scene" : `res://${scene}`,
|
|
39498
39673
|
refreshedClasses,
|
|
39499
|
-
debugPort: readNumber(
|
|
39674
|
+
debugPort: readNumber(playAnswer, "debugPort"),
|
|
39500
39675
|
endedPreviousRun: endedPreviousRun(ended),
|
|
39501
39676
|
breakpoints: breakpoints.applied.length === 0 ? undefined : this.breakpointsAsProjectSpells(projectPath, breakpoints.applied),
|
|
39502
39677
|
breakpointsRefused: breakpoints.refused.length === 0 ? undefined : breakpoints.refused.map((one) => ({
|
|
@@ -40022,7 +40197,11 @@ class GodotServer {
|
|
|
40022
40197
|
if (announced !== undefined && editorSays === undefined) {
|
|
40023
40198
|
return stillRunning(run);
|
|
40024
40199
|
}
|
|
40025
|
-
|
|
40200
|
+
const said = editorSays === undefined ? (await this.editorPlayingState())?.playing ?? null : editorSays;
|
|
40201
|
+
if (said === true) {
|
|
40202
|
+
run.seenPlaying = true;
|
|
40203
|
+
}
|
|
40204
|
+
return runIsUp(run, said);
|
|
40026
40205
|
}
|
|
40027
40206
|
async handleGetDebugOutput(args, waitedMs) {
|
|
40028
40207
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
@@ -40105,11 +40284,19 @@ class GodotServer {
|
|
|
40105
40284
|
this.drainEditorOutput(stopped);
|
|
40106
40285
|
this.drainTranscript(stopped);
|
|
40107
40286
|
const wasRunning = await this.runStillGoing(stopped);
|
|
40108
|
-
const
|
|
40287
|
+
const announcedPid = this.announcedPidOf(stopped) ?? null;
|
|
40288
|
+
const endedPid = stopped.pid ?? announcedPid;
|
|
40109
40289
|
const children = readBoolean(args, "andChildren") === true && wasRunning ? await this.whatTheRunStarted(stopped) : null;
|
|
40110
40290
|
this.logDebug("Stopping the running game");
|
|
40111
40291
|
await this.endActiveGame("editor_run stop");
|
|
40112
40292
|
const ended = children === null ? null : endChildrenAmong(children);
|
|
40293
|
+
if (wasRunning) {
|
|
40294
|
+
for (const pid of new Set([endedPid, announcedPid])) {
|
|
40295
|
+
if (pid !== null) {
|
|
40296
|
+
await announcementEnded(pid);
|
|
40297
|
+
}
|
|
40298
|
+
}
|
|
40299
|
+
}
|
|
40113
40300
|
return this.jsonTextResponse({
|
|
40114
40301
|
stopped: true,
|
|
40115
40302
|
through: stopped.throughEditor ? "editor" : "gdharness",
|
|
@@ -40205,6 +40392,7 @@ class GodotServer {
|
|
|
40205
40392
|
const writtenBefore = projectPath === "" ? null : cacheWrittenAt(projectPath);
|
|
40206
40393
|
const blind = await this.classesTheEditorCannotSee(args);
|
|
40207
40394
|
const atRisk = before === null ? [] : blind.unseen.filter((one) => before.has(one.className));
|
|
40395
|
+
const ghosts = blind.stillHeld ?? [];
|
|
40208
40396
|
const first = await this.handleViaBridge("rescan_filesystem", args);
|
|
40209
40397
|
if (first.isError) {
|
|
40210
40398
|
return first;
|
|
@@ -40216,7 +40404,7 @@ class GodotServer {
|
|
|
40216
40404
|
const pending = Boolean(status["pending"]) && Date.now() - started < SCAN_START_MS;
|
|
40217
40405
|
busy = Boolean(status["scanning"]) || Boolean(status["importing"]) || pending;
|
|
40218
40406
|
}
|
|
40219
|
-
if (!busy && projectPath !== "" && (atRisk.length > 0 || blind.unchecked !== undefined)) {
|
|
40407
|
+
if (!busy && projectPath !== "" && (atRisk.length > 0 || ghosts.length > 0 || blind.unchecked !== undefined)) {
|
|
40220
40408
|
const until = Date.now() + CACHE_WRITE_MS;
|
|
40221
40409
|
while (Date.now() < until && cacheWrittenAt(projectPath) === writtenBefore) {
|
|
40222
40410
|
await new Promise((settle) => setTimeout(settle, 100));
|
|
@@ -40224,7 +40412,11 @@ class GodotServer {
|
|
|
40224
40412
|
}
|
|
40225
40413
|
const after = projectPath === "" || busy ? null : cachedClasses(projectPath);
|
|
40226
40414
|
const lost = before === null || after === null ? [] : [...before.keys()].filter((name) => !after.has(name));
|
|
40227
|
-
const
|
|
40415
|
+
const atMissingPaths = after === null ? [] : cachedAtMissingPaths(after, projectPath);
|
|
40416
|
+
const restored = lost.length > 0 || atMissingPaths.length > 0 ? await this.rebuildCacheAfterLoss(projectPath, lost) : [];
|
|
40417
|
+
const rebuilt = atMissingPaths.length > 0 ? cachedClasses(projectPath) : null;
|
|
40418
|
+
const dropped = rebuilt === null ? [] : atMissingPaths.filter((name) => !cachedAtMissingPaths(rebuilt, projectPath).includes(name));
|
|
40419
|
+
const stillAtMissingPaths = atMissingPaths.filter((name) => !dropped.includes(name));
|
|
40228
40420
|
const reloading = readNonEmptyString(args, "reloadScript");
|
|
40229
40421
|
let reloaded = {};
|
|
40230
40422
|
if (reloading !== undefined) {
|
|
@@ -40240,8 +40432,23 @@ class GodotServer {
|
|
|
40240
40432
|
const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
|
|
40241
40433
|
const unseen = checked.unseen;
|
|
40242
40434
|
const stillGone = lost.filter((name) => !restored.includes(name));
|
|
40435
|
+
const notes = [];
|
|
40436
|
+
if (busy) {
|
|
40437
|
+
notes.push("The editor was still scanning or importing when the wait ran out, so new files may not be visible yet.");
|
|
40438
|
+
} else if (stillGone.length > 0) {
|
|
40439
|
+
notes.push("The scan wrote the class cache from the list this editor is holding, that list is shorter than the file was, and rebuilding the cache from the files did not bring these back. Every engine that reads the cache next, including a test run, will report them as unknown identifiers. Restart the editor with editor_launch restart before scanning again.");
|
|
40440
|
+
} else if (lost.length > 0) {
|
|
40441
|
+
notes.push("The scan wrote the class cache from the list this editor is holding, which is shorter than the file was, so the cache was rebuilt from the files and these classes are back in it. Nothing was lost, but this editor is still holding the short list: do not rescan again until it has been restarted with editor_launch restart, because it will write the same list over the file each time.");
|
|
40442
|
+
} else if (unseen.length > 0) {
|
|
40443
|
+
notes.push("The scan finished and these classes are still not in the list the editor resolves against, so every use of them reads as an unknown identifier. Its walk skips a file another engine has already imported. Rescan again on its own, which is the measured cure and needs no change to the declaring script, or restart the editor with editor_launch restart.");
|
|
40444
|
+
}
|
|
40445
|
+
if (stillAtMissingPaths.length > 0) {
|
|
40446
|
+
notes.push(`The scan wrote the class cache with ${stillAtMissingPaths.join(", ")} at a path that is not on disk, and rebuilding the cache from the files did not take it out. The next engine to read the cache fails on "Could not parse global class" in whichever correct script shares the bare name. Restart the editor with editor_launch restart, then run project_import refresh_classes.`);
|
|
40447
|
+
} else if (dropped.length > 0) {
|
|
40448
|
+
notes.push(`The scan wrote the class cache from the list this editor is holding, and that list still has ${dropped.join(", ")} for a script that is gone, so the cache was rebuilt from the files without it. The editor goes on holding it and writes it back on every scan until it is restarted with editor_launch restart; until then a scan is followed by this same rebuild.`);
|
|
40449
|
+
}
|
|
40243
40450
|
return this.jsonTextResponse({
|
|
40244
|
-
ok: !busy && unseen.length === 0 && stillGone.length === 0 && checked.unchecked === undefined,
|
|
40451
|
+
ok: !busy && unseen.length === 0 && stillGone.length === 0 && stillAtMissingPaths.length === 0 && checked.unchecked === undefined,
|
|
40245
40452
|
stillWorking: busy,
|
|
40246
40453
|
waitedMs: Date.now() - started,
|
|
40247
40454
|
heldBeforeReload: reloaded.heldBefore,
|
|
@@ -40250,8 +40457,10 @@ class GodotServer {
|
|
|
40250
40457
|
unseenByEditor: unseen.length > 0 ? unseen : undefined,
|
|
40251
40458
|
cacheLost: lost.length > 0 ? lost : undefined,
|
|
40252
40459
|
cacheRestored: restored.length > 0 ? restored : undefined,
|
|
40460
|
+
cacheDropped: dropped.length > 0 ? dropped : undefined,
|
|
40461
|
+
cacheAtMissingPaths: stillAtMissingPaths.length > 0 ? stillAtMissingPaths : undefined,
|
|
40253
40462
|
classesUnchecked: checked.unchecked,
|
|
40254
|
-
note:
|
|
40463
|
+
note: notes.length > 0 ? notes.join(" ") : undefined
|
|
40255
40464
|
});
|
|
40256
40465
|
}
|
|
40257
40466
|
async rebuildCacheAfterLoss(projectPath, lost) {
|
|
@@ -40492,6 +40701,7 @@ class GodotServer {
|
|
|
40492
40701
|
property: readString(args, "property") ?? "",
|
|
40493
40702
|
value: args["value"],
|
|
40494
40703
|
...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
|
|
40704
|
+
...readBoolean(args, "includeHidden") === undefined ? {} : { include_hidden: readBoolean(args, "includeHidden") },
|
|
40495
40705
|
timeout_ms: timeoutMs
|
|
40496
40706
|
}, patience);
|
|
40497
40707
|
}
|
|
@@ -40500,7 +40710,7 @@ async function runGodotServer() {
|
|
|
40500
40710
|
const server = new GodotServer;
|
|
40501
40711
|
await server.run();
|
|
40502
40712
|
}
|
|
40503
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run7, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, CONSOLE_WRAPPER, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CACHE_WRITE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
40713
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run7, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, CONSOLE_WRAPPER, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CACHE_WRITE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
40504
40714
|
var init_server3 = __esm(() => {
|
|
40505
40715
|
init_mcp();
|
|
40506
40716
|
init_stdio2();
|
|
@@ -40534,7 +40744,7 @@ var init_server3 = __esm(() => {
|
|
|
40534
40744
|
init_tool_definitions();
|
|
40535
40745
|
init_update_check();
|
|
40536
40746
|
run7 = promisify7(execFile7);
|
|
40537
|
-
__dirname2 =
|
|
40747
|
+
__dirname2 = dirname12(fileURLToPath4(import.meta.url));
|
|
40538
40748
|
CONSOLE_WRAPPER = /_console(\.exe)?$/i;
|
|
40539
40749
|
VALUE_NAMES = {
|
|
40540
40750
|
string: "a string",
|
|
@@ -42085,6 +42295,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
42085
42295
|
| Where a control is, and whether it is visible | \`runtime_inspect\` \`find\`, \`rect\` |
|
|
42086
42296
|
| What a property reads right now, on a node whose path you already hold | \`runtime_inspect\` \`property\`, \`runtime_invoke\` |
|
|
42087
42297
|
| What a property on an object a node holds reads | the same two, with colons: \`_game:clock:speed\` |
|
|
42298
|
+
| Which control holds the focus, or anything only a method answers | the same path with a step written as a call, \`get_viewport():gui_get_focus_owner\`, which calls a method taking no arguments and walks into what it returned |
|
|
42088
42299
|
| Press a button | \`runtime_input click\`, which says what was under the pointer |
|
|
42089
42300
|
| Press the next thing after a click that changed screen | \`runtime_wait until\` with \`says\` and a word the new screen shows, then the click. A click sent straight after a screen change lands on a control the new screen is about to free, answers \`landed: true\`, and does nothing; a frame count is a number nobody can pick correctly across a cold start and a warm one, and waiting for the words answered in about 130ms where twenty frames had not been enough |
|
|
42090
42301
|
| Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\` with \`replace\`; a submitted field has to be clicked again |
|
|
@@ -42093,7 +42304,9 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
42093
42304
|
| Answer a dialog | \`runtime_input click\` on its button, or \`key\` Escape to dismiss it: an [AcceptDialog] reads that key itself and never asks the InputMap, so \`ui_cancel\` leaves it standing |
|
|
42094
42305
|
| Press a bound key | \`runtime_input action\` or \`key\`, a whole press unless you hold it |
|
|
42095
42306
|
| Where a 3D node is on screen | \`runtime_inspect\` \`rect\`, rather than unprojecting by hand |
|
|
42096
|
-
| Wait for something in a running game | \`runtime_wait\`, never a sleep; \`until\` with \`says\` for a panel that rebuilds its own labels |
|
|
42307
|
+
| Wait for something in a running game | \`runtime_wait\`, never a sleep; \`until\` with \`says\` for a panel that rebuilds its own labels. The words have to be shown: a button that exists hidden through an animation does not satisfy it until it appears, unless \`includeHidden\` says otherwise |
|
|
42308
|
+
| What every label on a screen says | \`runtime_inspect tree\` with \`properties\` naming \`text\`, which reads it off every node that has one; \`includeProperties\` is every stored property of every node and ran to seventy-six thousand characters for one row |
|
|
42309
|
+
| Whether this engine has a method or property, and what it takes | \`editor_classes info\` with \`member\`, which answers that one member and which class declares it rather than the whole class |
|
|
42097
42310
|
| Wait for a run to finish | \`editor_run wait\`, never a sleep or a shell loop on the pid: it answers as \`editor_output\` does, once the run is over |
|
|
42098
42311
|
| End a bench and the workers it opened | \`editor_run stop\` with \`andChildren\`, never a process listing by scene name: it ends every process under the run's, to any depth, that is a game of the project, announced as one or the project's engine run with \`--path\` on it, names them under \`endedChildren\` with what identified each, and leaves and names any other |
|
|
42099
42312
|
| A picture, for a person who asked to see one | \`runtime_capture\` |
|