gdharness 0.13.42 → 0.13.44
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 +384 -62
- package/build/godot/addons/gdharness_runtime/error_report.gd +62 -0
- package/build/godot/addons/gdharness_runtime/error_report.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +31 -0
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +12 -1
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +3 -2
- package/build/index.js +378 -58
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -10183,7 +10183,7 @@ import {
|
|
|
10183
10183
|
readSync
|
|
10184
10184
|
} from "node:fs";
|
|
10185
10185
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
10186
|
-
import { basename as
|
|
10186
|
+
import { basename as basename3, dirname as dirname6, join as join13, normalize as normalize2, relative as relative3 } from "node:path";
|
|
10187
10187
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
10188
10188
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
10189
10189
|
import { promisify as promisify5 } from "node:util";
|
|
@@ -25831,7 +25831,7 @@ function dictionary(entries) {
|
|
|
25831
25831
|
|
|
25832
25832
|
// src/game-log.ts
|
|
25833
25833
|
import { StringDecoder } from "node:string_decoder";
|
|
25834
|
-
var HEADLINE = /^(USER )?(SCRIPT ERROR|ERROR|WARNING):\s?(.*)$/;
|
|
25834
|
+
var HEADLINE = /^(USER )?(SCRIPT ERROR|SHADER ERROR|ERROR|WARNING):\s?(.*)$/;
|
|
25835
25835
|
var COLOUR_CODE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
25836
25836
|
function announced(line) {
|
|
25837
25837
|
const match = HEADLINE.exec(line);
|
|
@@ -27711,31 +27711,135 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
27711
27711
|
|
|
27712
27712
|
// src/process-children.ts
|
|
27713
27713
|
import { execFile as execFile3 } from "node:child_process";
|
|
27714
|
+
import { basename } from "node:path";
|
|
27714
27715
|
import { promisify as promisify3 } from "node:util";
|
|
27715
27716
|
var run3 = promisify3(execFile3);
|
|
27716
27717
|
var ASK_TIMEOUT_MS = 15000;
|
|
27717
|
-
async function
|
|
27718
|
+
async function processTree() {
|
|
27718
27719
|
try {
|
|
27719
|
-
const printed = process.platform === "win32" ? await askWindows(
|
|
27720
|
-
return printed
|
|
27721
|
-
} catch
|
|
27722
|
-
if (process.platform !== "win32" && error.code === 1) {
|
|
27723
|
-
return [];
|
|
27724
|
-
}
|
|
27720
|
+
const printed = process.platform === "win32" ? await askWindows() : await askPosix();
|
|
27721
|
+
return parseProcessTable(printed);
|
|
27722
|
+
} catch {
|
|
27725
27723
|
return;
|
|
27726
27724
|
}
|
|
27727
27725
|
}
|
|
27728
|
-
|
|
27726
|
+
function parseProcessTable(printed) {
|
|
27727
|
+
const tree = new Map;
|
|
27728
|
+
for (const line of printed.split(/\r?\n/)) {
|
|
27729
|
+
const fields = /^\s*(\d+)\s+(\d+)(?:\s+(.*))?$/.exec(line);
|
|
27730
|
+
if (fields === null) {
|
|
27731
|
+
continue;
|
|
27732
|
+
}
|
|
27733
|
+
const pid = Number(fields[1]);
|
|
27734
|
+
const parent = Number(fields[2]);
|
|
27735
|
+
if (pid > 0) {
|
|
27736
|
+
tree.set(pid, { parent, command: (fields[3] ?? "").trim() });
|
|
27737
|
+
}
|
|
27738
|
+
}
|
|
27739
|
+
return tree;
|
|
27740
|
+
}
|
|
27741
|
+
function childrenIn(tree, pid) {
|
|
27742
|
+
const children = [];
|
|
27743
|
+
for (const [child, listed] of tree) {
|
|
27744
|
+
if (listed.parent === pid) {
|
|
27745
|
+
children.push(child);
|
|
27746
|
+
}
|
|
27747
|
+
}
|
|
27748
|
+
return children;
|
|
27749
|
+
}
|
|
27750
|
+
function descendantsIn(tree, pid) {
|
|
27751
|
+
const found = [];
|
|
27752
|
+
const queue = [pid];
|
|
27753
|
+
const seen = new Set([pid]);
|
|
27754
|
+
while (queue.length > 0) {
|
|
27755
|
+
const next = queue.shift();
|
|
27756
|
+
if (next === undefined) {
|
|
27757
|
+
break;
|
|
27758
|
+
}
|
|
27759
|
+
for (const child of childrenIn(tree, next)) {
|
|
27760
|
+
if (!seen.has(child)) {
|
|
27761
|
+
seen.add(child);
|
|
27762
|
+
found.push(child);
|
|
27763
|
+
queue.push(child);
|
|
27764
|
+
}
|
|
27765
|
+
}
|
|
27766
|
+
}
|
|
27767
|
+
return found;
|
|
27768
|
+
}
|
|
27769
|
+
function ancestorsIn(tree, pid, root) {
|
|
27770
|
+
const chain = [];
|
|
27771
|
+
const seen = new Set([pid]);
|
|
27772
|
+
let parent = tree.get(pid)?.parent;
|
|
27773
|
+
while (parent !== undefined && !seen.has(parent)) {
|
|
27774
|
+
chain.push(parent);
|
|
27775
|
+
if (parent === root) {
|
|
27776
|
+
break;
|
|
27777
|
+
}
|
|
27778
|
+
seen.add(parent);
|
|
27779
|
+
parent = tree.get(parent)?.parent;
|
|
27780
|
+
}
|
|
27781
|
+
return chain;
|
|
27782
|
+
}
|
|
27783
|
+
function readCommandLine(command) {
|
|
27784
|
+
const words = commandWords(command);
|
|
27785
|
+
const executable = words[0] === undefined ? "" : basename(words[0].replaceAll("\\", "/"));
|
|
27786
|
+
const at = words.indexOf("--path");
|
|
27787
|
+
let projectPath = null;
|
|
27788
|
+
if (at !== -1 && at + 1 < words.length) {
|
|
27789
|
+
const taken = [];
|
|
27790
|
+
for (const word of words.slice(at + 1)) {
|
|
27791
|
+
if (taken.length > 0 && (word.startsWith("-") || /^[a-z]+:\/\//.test(word))) {
|
|
27792
|
+
break;
|
|
27793
|
+
}
|
|
27794
|
+
taken.push(word);
|
|
27795
|
+
}
|
|
27796
|
+
projectPath = taken.join(" ");
|
|
27797
|
+
}
|
|
27798
|
+
return {
|
|
27799
|
+
executable,
|
|
27800
|
+
projectPath,
|
|
27801
|
+
editor: words.some((word) => word === "-e" || word === "--editor")
|
|
27802
|
+
};
|
|
27803
|
+
}
|
|
27804
|
+
function commandWords(command) {
|
|
27805
|
+
const words = [];
|
|
27806
|
+
let word = "";
|
|
27807
|
+
let quoted = false;
|
|
27808
|
+
let started = false;
|
|
27809
|
+
for (const char of command) {
|
|
27810
|
+
if (char === '"') {
|
|
27811
|
+
quoted = !quoted;
|
|
27812
|
+
started = true;
|
|
27813
|
+
} else if (!quoted && /\s/.test(char)) {
|
|
27814
|
+
if (started) {
|
|
27815
|
+
words.push(word);
|
|
27816
|
+
word = "";
|
|
27817
|
+
started = false;
|
|
27818
|
+
}
|
|
27819
|
+
} else {
|
|
27820
|
+
word += char;
|
|
27821
|
+
started = true;
|
|
27822
|
+
}
|
|
27823
|
+
}
|
|
27824
|
+
if (started) {
|
|
27825
|
+
words.push(word);
|
|
27826
|
+
}
|
|
27827
|
+
return words;
|
|
27828
|
+
}
|
|
27829
|
+
async function askWindows() {
|
|
27729
27830
|
const { stdout } = await run3("powershell.exe", [
|
|
27730
27831
|
"-NoProfile",
|
|
27731
27832
|
"-NonInteractive",
|
|
27732
27833
|
"-Command",
|
|
27733
|
-
|
|
27734
|
-
], { timeout: ASK_TIMEOUT_MS, windowsHide: true });
|
|
27834
|
+
'Get-CimInstance Win32_Process | ForEach-Object { "$($_.ProcessId) $($_.ParentProcessId) $($_.CommandLine)" }'
|
|
27835
|
+
], { timeout: ASK_TIMEOUT_MS, windowsHide: true, maxBuffer: 16 * 1024 * 1024 });
|
|
27735
27836
|
return stdout;
|
|
27736
27837
|
}
|
|
27737
|
-
async function askPosix(
|
|
27738
|
-
const { stdout } = await run3("
|
|
27838
|
+
async function askPosix() {
|
|
27839
|
+
const { stdout } = await run3("ps", ["-e", "-o", "pid=,ppid=,args="], {
|
|
27840
|
+
timeout: ASK_TIMEOUT_MS,
|
|
27841
|
+
maxBuffer: 16 * 1024 * 1024
|
|
27842
|
+
});
|
|
27739
27843
|
return stdout;
|
|
27740
27844
|
}
|
|
27741
27845
|
|
|
@@ -28186,11 +28290,11 @@ function restartSettled(projectPath) {
|
|
|
28186
28290
|
|
|
28187
28291
|
// src/run-record.ts
|
|
28188
28292
|
import { execFileSync } from "node:child_process";
|
|
28189
|
-
import { mkdirSync as mkdirSync5, openSync, readdirSync as readdirSync5, readFileSync as readFileSync11, rmSync as rmSync5, statSync as
|
|
28190
|
-
import { basename, join as join11 } from "node:path";
|
|
28293
|
+
import { mkdirSync as mkdirSync5, openSync, readdirSync as readdirSync5, readFileSync as readFileSync11, rmSync as rmSync5, statSync as statSync4, writeFileSync as writeFileSync6 } from "node:fs";
|
|
28294
|
+
import { basename as basename2, join as join11 } from "node:path";
|
|
28191
28295
|
|
|
28192
28296
|
// src/runtime-client.ts
|
|
28193
|
-
import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync10, unlinkSync } from "node:fs";
|
|
28297
|
+
import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync10, statSync as statSync3, unlinkSync } from "node:fs";
|
|
28194
28298
|
import { createConnection as createConnection3 } from "node:net";
|
|
28195
28299
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
28196
28300
|
import { join as join10, resolve as resolve4 } from "node:path";
|
|
@@ -28271,6 +28375,17 @@ function runtimeDirectories(variables = process.env) {
|
|
|
28271
28375
|
}
|
|
28272
28376
|
return [...new Set(candidates.map((path) => resolve4(path)))];
|
|
28273
28377
|
}
|
|
28378
|
+
var ERROR_REPORT_PATTERN = /^runtime-(\d+)\.log$/;
|
|
28379
|
+
function errorReportOf(pid, directories = runtimeDirectories()) {
|
|
28380
|
+
for (const directory of directories) {
|
|
28381
|
+
const path = join10(directory, `runtime-${pid}.log`);
|
|
28382
|
+
if (existsSync7(path)) {
|
|
28383
|
+
return path;
|
|
28384
|
+
}
|
|
28385
|
+
}
|
|
28386
|
+
return null;
|
|
28387
|
+
}
|
|
28388
|
+
var ERROR_REPORT_KEEP_MS = 60 * 60 * 1000;
|
|
28274
28389
|
function processAlive(pid) {
|
|
28275
28390
|
try {
|
|
28276
28391
|
process.kill(pid, 0);
|
|
@@ -28362,6 +28477,11 @@ function announcedIn(directory) {
|
|
|
28362
28477
|
}
|
|
28363
28478
|
const found = [];
|
|
28364
28479
|
for (const entry of readdirSync4(directory)) {
|
|
28480
|
+
const report = ERROR_REPORT_PATTERN.exec(entry);
|
|
28481
|
+
if (report) {
|
|
28482
|
+
sweepErrorReport(join10(directory, entry), Number.parseInt(report[1] ?? "", 10));
|
|
28483
|
+
continue;
|
|
28484
|
+
}
|
|
28365
28485
|
const match = ANNOUNCEMENT_PATTERN.exec(entry);
|
|
28366
28486
|
if (!match) {
|
|
28367
28487
|
continue;
|
|
@@ -28383,6 +28503,16 @@ function announcedIn(directory) {
|
|
|
28383
28503
|
}
|
|
28384
28504
|
return found;
|
|
28385
28505
|
}
|
|
28506
|
+
function sweepErrorReport(path, pid, now = Date.now()) {
|
|
28507
|
+
if (processAlive(pid)) {
|
|
28508
|
+
return;
|
|
28509
|
+
}
|
|
28510
|
+
try {
|
|
28511
|
+
if (now - statSync3(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
|
|
28512
|
+
unlinkSync(path);
|
|
28513
|
+
}
|
|
28514
|
+
} catch {}
|
|
28515
|
+
}
|
|
28386
28516
|
var LOOK_EVERY_MS = 50;
|
|
28387
28517
|
var ANNOUNCE_BUDGET_MS = 5000;
|
|
28388
28518
|
async function announcedSince(projectPath, before, waiting = {}) {
|
|
@@ -28775,7 +28905,7 @@ function judgeRun(record, running, needed) {
|
|
|
28775
28905
|
if (running.startedAt !== undefined && running.startedAt - record.startedAt > SAME_RUN_WINDOW_MS) {
|
|
28776
28906
|
return false;
|
|
28777
28907
|
}
|
|
28778
|
-
const engine = record.command === undefined ? null :
|
|
28908
|
+
const engine = record.command === undefined ? null : basename2(record.command);
|
|
28779
28909
|
const said = process.platform === "win32" ? running.text.toLowerCase() : running.text;
|
|
28780
28910
|
const wanted = engine === null || process.platform !== "win32" ? engine : engine.toLowerCase();
|
|
28781
28911
|
if (wanted !== null && !said.includes(wanted)) {
|
|
@@ -28806,7 +28936,7 @@ function clearRunRecord(ours = () => true) {
|
|
|
28806
28936
|
}
|
|
28807
28937
|
function ageMs(path, now) {
|
|
28808
28938
|
try {
|
|
28809
|
-
return now -
|
|
28939
|
+
return now - statSync4(path).mtimeMs;
|
|
28810
28940
|
} catch {
|
|
28811
28941
|
return 0;
|
|
28812
28942
|
}
|
|
@@ -29500,7 +29630,7 @@ var TOOL_SPECS = [
|
|
|
29500
29630
|
andChildren: {
|
|
29501
29631
|
type: "boolean",
|
|
29502
29632
|
ops: ["stop"],
|
|
29503
|
-
description: "stop: also end what the game started for itself, with OS.create_process or otherwise, such as a bench's workers. Only the
|
|
29633
|
+
description: "stop: also end what the game started for itself, with OS.create_process or otherwise, such as a bench's workers and whatever those started in turn. Only the processes under the run's, to any depth, that are games of this project are ended: announced as one, or this project's engine run with --path on this project by its command line, which is how a bench whose workers deliberately carry no runtime is reached. They are named under endedChildren, with what identified each under identifiedBy; any other process under the run is left and named under childrenLeft, and childrenUnknown is true when nothing could be listed, with the note saying whether the platform would not or the run has no process to list under. Default false, and the run's process is ended either way."
|
|
29504
29634
|
}
|
|
29505
29635
|
},
|
|
29506
29636
|
requires: [],
|
|
@@ -29523,7 +29653,7 @@ var TOOL_SPECS = [
|
|
|
29523
29653
|
},
|
|
29524
29654
|
{
|
|
29525
29655
|
name: "editor_output",
|
|
29526
|
-
description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. Answers with the counts and the verdict as well as the entries. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering.",
|
|
29656
|
+
description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone. Answers with the counts and the verdict as well as the entries. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering.",
|
|
29527
29657
|
parameters: {
|
|
29528
29658
|
severity: {
|
|
29529
29659
|
type: "string",
|
|
@@ -29640,7 +29770,7 @@ var TOOL_SPECS = [
|
|
|
29640
29770
|
says: {
|
|
29641
29771
|
type: "string",
|
|
29642
29772
|
ops: ["find"],
|
|
29643
|
-
description: "find: part of what the node has written on it, case-insensitively, which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match. A bare word is a contains; write a glob and it is one, matched against the whole of what the node says, the same as namePattern."
|
|
29773
|
+
description: "find: part of what the node has written on it, case-insensitively, which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match. A bare word is a contains; write a glob and it is one, matched against the whole of what the node says, the same as namePattern. A label with a line break is matched with the break in the words, and a backslash followed by n counts as one."
|
|
29644
29774
|
},
|
|
29645
29775
|
limit: {
|
|
29646
29776
|
type: "number",
|
|
@@ -29858,7 +29988,7 @@ var TOOL_SPECS = [
|
|
|
29858
29988
|
says: {
|
|
29859
29989
|
type: "string",
|
|
29860
29990
|
ops: ["until"],
|
|
29861
|
-
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, hidden nodes included. 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."
|
|
29991
|
+
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, hidden nodes included; 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."
|
|
29862
29992
|
},
|
|
29863
29993
|
timeoutMs: {
|
|
29864
29994
|
type: "number",
|
|
@@ -30027,16 +30157,44 @@ function endedPreviousRun(ended) {
|
|
|
30027
30157
|
}
|
|
30028
30158
|
return ended.pid ?? true;
|
|
30029
30159
|
}
|
|
30160
|
+
var CONSOLE_WRAPPER = /_console(\.exe)?$/i;
|
|
30161
|
+
function engineNamesOf(executable) {
|
|
30162
|
+
return CONSOLE_WRAPPER.test(executable) ? [executable, executable.replace(CONSOLE_WRAPPER, "$1")] : [executable];
|
|
30163
|
+
}
|
|
30164
|
+
function gameBetween(tree, pid, root, games) {
|
|
30165
|
+
return ancestorsIn(tree, pid, root).some((one) => one !== root && games.has(one));
|
|
30166
|
+
}
|
|
30167
|
+
function endChildrenAmong(started) {
|
|
30168
|
+
if (started.unknown !== false) {
|
|
30169
|
+
return { ended: [], left: [], unknown: started.unknown };
|
|
30170
|
+
}
|
|
30171
|
+
for (const game of started.games) {
|
|
30172
|
+
try {
|
|
30173
|
+
process.kill(game.pid);
|
|
30174
|
+
} catch {}
|
|
30175
|
+
}
|
|
30176
|
+
return { ended: started.games, left: [...started.others], unknown: false };
|
|
30177
|
+
}
|
|
30030
30178
|
function aboutTheChildren(ended, throughEditor) {
|
|
30179
|
+
const reaches = "andChildren ends what it started that is a game of this project: announced as one, or this project's engine run with --path on it.";
|
|
30031
30180
|
if (ended === null) {
|
|
30032
|
-
return throughEditor ?
|
|
30181
|
+
return throughEditor ? `Anything that game started for itself, with OS.create_process or otherwise, is not the editor's to stop and was not signalled; ${reaches}` : `Anything that game started for itself, with OS.create_process or otherwise, is a separate process this stop did not signal; ${reaches}`;
|
|
30033
30182
|
}
|
|
30034
|
-
if (ended.unknown) {
|
|
30183
|
+
if (ended.unknown === "platform") {
|
|
30035
30184
|
return "andChildren was asked for and this platform would not list what the game had started, so nothing else was ended.";
|
|
30036
30185
|
}
|
|
30186
|
+
if (ended.unknown === "no process") {
|
|
30187
|
+
return "andChildren was asked for and this run has no process id to list under: the editor plays it and its game announced none, so nothing else was ended.";
|
|
30188
|
+
}
|
|
30037
30189
|
const counted = (n) => `${n} ${n === 1 ? "process" : "processes"}`;
|
|
30038
|
-
const
|
|
30039
|
-
const
|
|
30190
|
+
const announced = ended.ended.filter((one) => one.how === "announced").length;
|
|
30191
|
+
const byCommandLine = ended.ended.length - announced;
|
|
30192
|
+
const how = [
|
|
30193
|
+
announced === 0 ? "" : `${announced} announced as ${announced === 1 ? "a game" : "games"} of this project`,
|
|
30194
|
+
byCommandLine === 0 ? "" : `${byCommandLine} this project's engine run with --path on it, by ${byCommandLine === 1 ? "its" : "their"} command line`
|
|
30195
|
+
].filter((part) => part !== "").join(" and ");
|
|
30196
|
+
const endedToo = ended.ended.length === 0 ? "The game had started nothing that is a game of this project, announced as one or this project's engine run with --path on it, so nothing else was ended." : `${counted(ended.ended.length)} the game had started for itself ${ended.ended.length === 1 ? "was" : "were"} ended with it, named under endedChildren with what identified ${ended.ended.length === 1 ? "it" : "each"} under identifiedBy: ${how}.`;
|
|
30197
|
+
const leftToo = ended.left.length === 0 ? "" : ` ${counted(ended.left.length)} it had started ${ended.left.length === 1 ? "was" : "were"} left, named under childrenLeft: neither announced as a game of this project nor this project's engine run with --path on it, so what ${ended.left.length === 1 ? "it is" : "they are"} cannot be told from here.`;
|
|
30040
30198
|
return `${endedToo}${leftToo}`;
|
|
30041
30199
|
}
|
|
30042
30200
|
function endedToStartThis(ended) {
|
|
@@ -31144,7 +31302,7 @@ class GodotServer {
|
|
|
31144
31302
|
const name = application["config/name"];
|
|
31145
31303
|
const mainScene = application["run/main_scene"];
|
|
31146
31304
|
const info = {
|
|
31147
|
-
name: typeof name === "string" && name !== "" ? name :
|
|
31305
|
+
name: typeof name === "string" && name !== "" ? name : basename3(project.value.path),
|
|
31148
31306
|
path: project.value.path,
|
|
31149
31307
|
mainScene: typeof mainScene === "string" && mainScene !== "" ? mainScene : null,
|
|
31150
31308
|
godotVersion: await this.godotVersion(engine.value),
|
|
@@ -31956,6 +32114,7 @@ class GodotServer {
|
|
|
31956
32114
|
});
|
|
31957
32115
|
this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
31958
32116
|
const started = this.spawnKeptGame(engine.value, cmdArgs, project.value.path);
|
|
32117
|
+
started.announcedBefore = alreadyPlaying;
|
|
31959
32118
|
this.activeProcess = started;
|
|
31960
32119
|
const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && given.value.length > 0 ? " The editor cannot be handed arguments for a game it plays, so this one was started " + "here: the debug_* tools answer only for a game the editor is playing." : "";
|
|
31961
32120
|
const endedForThis = endedToStartThis(ended);
|
|
@@ -32005,7 +32164,7 @@ class GodotServer {
|
|
|
32005
32164
|
}
|
|
32006
32165
|
});
|
|
32007
32166
|
const going = this.currentRun();
|
|
32008
|
-
if (endpoint !== null && going
|
|
32167
|
+
if (endpoint !== null && going !== null && going.announcedPid === undefined) {
|
|
32009
32168
|
going.announcedPid = endpoint.pid;
|
|
32010
32169
|
}
|
|
32011
32170
|
return runtimeVerdict(endpoint, {
|
|
@@ -32076,7 +32235,11 @@ class GodotServer {
|
|
|
32076
32235
|
});
|
|
32077
32236
|
}
|
|
32078
32237
|
drainEditorOutput(game) {
|
|
32079
|
-
if (!game.throughEditor
|
|
32238
|
+
if (!game.throughEditor) {
|
|
32239
|
+
return;
|
|
32240
|
+
}
|
|
32241
|
+
this.drainErrorReport(game);
|
|
32242
|
+
if (!this.dapClient) {
|
|
32080
32243
|
return;
|
|
32081
32244
|
}
|
|
32082
32245
|
if (game.transcript === null) {
|
|
@@ -32091,9 +32254,44 @@ class GodotServer {
|
|
|
32091
32254
|
}
|
|
32092
32255
|
this.recordWhatItBrokeOn(game);
|
|
32093
32256
|
}
|
|
32257
|
+
drainErrorReport(game) {
|
|
32258
|
+
if (game.errorReport === undefined) {
|
|
32259
|
+
const its = this.announcedPidOf(game);
|
|
32260
|
+
const found = its === undefined ? null : errorReportOf(its);
|
|
32261
|
+
if (found === null) {
|
|
32262
|
+
return;
|
|
32263
|
+
}
|
|
32264
|
+
game.errorReport = found;
|
|
32265
|
+
game.errorReportOffset = 0;
|
|
32266
|
+
}
|
|
32267
|
+
let handle;
|
|
32268
|
+
try {
|
|
32269
|
+
handle = openSync2(game.errorReport, "r");
|
|
32270
|
+
} catch {
|
|
32271
|
+
return;
|
|
32272
|
+
}
|
|
32273
|
+
try {
|
|
32274
|
+
const from = game.errorReportOffset ?? 0;
|
|
32275
|
+
const size = fstatSync(handle).size;
|
|
32276
|
+
if (size <= from) {
|
|
32277
|
+
return;
|
|
32278
|
+
}
|
|
32279
|
+
const buffer = Buffer.alloc(size - from);
|
|
32280
|
+
const read = readSync(handle, buffer, 0, buffer.length, from);
|
|
32281
|
+
game.errorReportOffset = from + read;
|
|
32282
|
+
const reported = buffer.subarray(0, read);
|
|
32283
|
+
if (game.transcript !== null) {
|
|
32284
|
+
appendFileSync(game.transcript, reported);
|
|
32285
|
+
} else {
|
|
32286
|
+
game.log.append("stderr", reported);
|
|
32287
|
+
}
|
|
32288
|
+
} finally {
|
|
32289
|
+
closeSync(handle);
|
|
32290
|
+
}
|
|
32291
|
+
}
|
|
32094
32292
|
recordWhatItBrokeOn(game) {
|
|
32095
32293
|
const halt = this.dapClient?.whereItStopped() ?? null;
|
|
32096
|
-
if (halt?.reason !== "exception" || halt.text === "" || game.brokeOn === halt.text) {
|
|
32294
|
+
if (halt?.reason !== "exception" || halt.text === "" || game.brokeOn === halt.text || game.errorReport !== undefined) {
|
|
32097
32295
|
return;
|
|
32098
32296
|
}
|
|
32099
32297
|
game.brokeOn = halt.text;
|
|
@@ -32291,12 +32489,16 @@ class GodotServer {
|
|
|
32291
32489
|
this.activeProcess = picked;
|
|
32292
32490
|
}
|
|
32293
32491
|
announcedPidOf(run, announced) {
|
|
32294
|
-
if (
|
|
32492
|
+
if (run.announcedPid !== undefined) {
|
|
32493
|
+
return run.announcedPid;
|
|
32494
|
+
}
|
|
32495
|
+
const ours = this.announcedOfTheRunsProject(run, announced ?? runtimesAnnounced().running);
|
|
32496
|
+
if (run.pid !== null && ours.some((one) => one.pid === run.pid)) {
|
|
32497
|
+
run.announcedPid = run.pid;
|
|
32295
32498
|
return run.announcedPid;
|
|
32296
32499
|
}
|
|
32297
32500
|
const before = run.announcedBefore ?? new Set;
|
|
32298
|
-
const
|
|
32299
|
-
const candidates = fresh.filter((one) => this.playedByOurEditor(one, run.projectPath));
|
|
32501
|
+
const candidates = ours.filter((one) => !before.has(one.pid) && this.isTheGameOf(run, one, run.projectPath));
|
|
32300
32502
|
const theOne = candidates.length === 1 ? candidates[0] : undefined;
|
|
32301
32503
|
if (theOne !== undefined) {
|
|
32302
32504
|
run.announcedPid = theOne.pid;
|
|
@@ -32311,8 +32513,68 @@ class GodotServer {
|
|
|
32311
32513
|
if (projectPath !== undefined && run.projectPath !== null && !isSameDirectory(run.projectPath, projectPath)) {
|
|
32312
32514
|
return;
|
|
32313
32515
|
}
|
|
32314
|
-
const its =
|
|
32315
|
-
return its !==
|
|
32516
|
+
const its = this.announcedPidOf(run, running);
|
|
32517
|
+
return its !== undefined && running.some((one) => one.pid === its) ? its : undefined;
|
|
32518
|
+
}
|
|
32519
|
+
async ownGameThroughTheTree(running, projectPath) {
|
|
32520
|
+
const run = this.currentRun();
|
|
32521
|
+
if (run === null || !stillRunning(run) || run.announcedPid !== undefined) {
|
|
32522
|
+
return;
|
|
32523
|
+
}
|
|
32524
|
+
if (projectPath !== undefined && run.projectPath !== null && !isSameDirectory(run.projectPath, projectPath)) {
|
|
32525
|
+
return;
|
|
32526
|
+
}
|
|
32527
|
+
const its = await this.tiedThroughTheTree(run, running);
|
|
32528
|
+
return its !== undefined && running.some((one) => one.pid === its) ? its : undefined;
|
|
32529
|
+
}
|
|
32530
|
+
async tiedThroughTheTree(run, announced) {
|
|
32531
|
+
if (run.announcedPid !== undefined) {
|
|
32532
|
+
return run.announcedPid;
|
|
32533
|
+
}
|
|
32534
|
+
const ours = this.announcedOfTheRunsProject(run, announced);
|
|
32535
|
+
if (ours.length === 0) {
|
|
32536
|
+
return;
|
|
32537
|
+
}
|
|
32538
|
+
const tree = await processTree();
|
|
32539
|
+
if (tree === undefined) {
|
|
32540
|
+
return;
|
|
32541
|
+
}
|
|
32542
|
+
return this.tieThroughTheTree(run, tree, new Set(ours.map((one) => one.pid)));
|
|
32543
|
+
}
|
|
32544
|
+
async gameProcessOf(run) {
|
|
32545
|
+
if (run.gamePid !== undefined) {
|
|
32546
|
+
return run.gamePid;
|
|
32547
|
+
}
|
|
32548
|
+
const announced = runtimesAnnounced().running;
|
|
32549
|
+
const tied = await this.tiedThroughTheTree(run, announced);
|
|
32550
|
+
if (tied !== undefined) {
|
|
32551
|
+
return tied;
|
|
32552
|
+
}
|
|
32553
|
+
const tree = await processTree();
|
|
32554
|
+
if (tree === undefined) {
|
|
32555
|
+
return;
|
|
32556
|
+
}
|
|
32557
|
+
const found = this.gameByCommandLineIn(run, tree);
|
|
32558
|
+
if (found !== undefined) {
|
|
32559
|
+
run.gamePid = found;
|
|
32560
|
+
}
|
|
32561
|
+
return found;
|
|
32562
|
+
}
|
|
32563
|
+
tieThroughTheTree(run, tree, games) {
|
|
32564
|
+
const root = run.pid ?? (run.throughEditor ? this.godotBridge.getStatus().editorPid : undefined);
|
|
32565
|
+
if (root === undefined) {
|
|
32566
|
+
return;
|
|
32567
|
+
}
|
|
32568
|
+
if (games.has(root)) {
|
|
32569
|
+
run.announcedPid = root;
|
|
32570
|
+
return root;
|
|
32571
|
+
}
|
|
32572
|
+
const nearest = descendantsIn(tree, root).filter((one) => games.has(one) && !gameBetween(tree, one, root, games));
|
|
32573
|
+
const theOne = nearest.length === 1 ? nearest[0] : undefined;
|
|
32574
|
+
if (theOne !== undefined) {
|
|
32575
|
+
run.announcedPid = theOne;
|
|
32576
|
+
}
|
|
32577
|
+
return theOne;
|
|
32316
32578
|
}
|
|
32317
32579
|
playedByOurEditor(endpoint, projectPath) {
|
|
32318
32580
|
const editor = this.godotBridge.getStatus().editorPid;
|
|
@@ -32390,6 +32652,13 @@ class GodotServer {
|
|
|
32390
32652
|
announcedForOurProject(announced) {
|
|
32391
32653
|
return this.allAnnouncedForOurProject(announced)[0] ?? null;
|
|
32392
32654
|
}
|
|
32655
|
+
announcedOfTheRunsProject(run, announced) {
|
|
32656
|
+
const project = run.projectPath;
|
|
32657
|
+
if (project === null) {
|
|
32658
|
+
return this.allAnnouncedForOurProject(announced);
|
|
32659
|
+
}
|
|
32660
|
+
return announced.filter((one) => one.project.path !== "" && isSameDirectory(project, one.project.path));
|
|
32661
|
+
}
|
|
32393
32662
|
allAnnouncedForOurProject(announced) {
|
|
32394
32663
|
const status = this.godotBridge.getStatus();
|
|
32395
32664
|
const mine = this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
|
|
@@ -32504,7 +32773,8 @@ class GodotServer {
|
|
|
32504
32773
|
});
|
|
32505
32774
|
const hold = await this.holdOf(run, going);
|
|
32506
32775
|
const wanted = readBoolean(args, "cpu") ?? false;
|
|
32507
|
-
const
|
|
32776
|
+
const announced = this.announcedPidOf(run);
|
|
32777
|
+
const processToAsk = wanted ? announced ?? (stillRunning(run) ? await this.gameProcessOf(run) : undefined) ?? run.pid ?? null : null;
|
|
32508
32778
|
const cpuSeconds = wanted && processToAsk !== null && stillRunning(run) ? await cpuSecondsOf(processToAsk) : undefined;
|
|
32509
32779
|
const notes = [];
|
|
32510
32780
|
if (wanted && cpuSeconds === undefined && stillRunning(run)) {
|
|
@@ -32535,7 +32805,7 @@ class GodotServer {
|
|
|
32535
32805
|
running: going,
|
|
32536
32806
|
exitCode: run.exitCode,
|
|
32537
32807
|
through: run.throughEditor ? "editor" : "gdharness",
|
|
32538
|
-
pid: run.pid ??
|
|
32808
|
+
pid: run.pid ?? announced ?? run.announcedPid ?? null,
|
|
32539
32809
|
errors: run.log.count("error"),
|
|
32540
32810
|
warnings: run.log.count("warning"),
|
|
32541
32811
|
clean: run.consoleLost === true ? undefined : run.log.count("error") === 0,
|
|
@@ -32563,18 +32833,19 @@ class GodotServer {
|
|
|
32563
32833
|
this.drainTranscript(stopped);
|
|
32564
32834
|
const wasRunning = await this.runStillGoing(stopped);
|
|
32565
32835
|
const endedPid = stopped.pid ?? this.announcedPidOf(stopped) ?? null;
|
|
32566
|
-
const children = readBoolean(args, "andChildren") === true && wasRunning
|
|
32836
|
+
const children = readBoolean(args, "andChildren") === true && wasRunning ? await this.whatTheRunStarted(stopped) : null;
|
|
32567
32837
|
this.logDebug("Stopping the running game");
|
|
32568
32838
|
await this.endActiveGame("editor_run stop");
|
|
32569
|
-
const ended = children === null ? null :
|
|
32839
|
+
const ended = children === null ? null : endChildrenAmong(children);
|
|
32570
32840
|
return this.jsonTextResponse({
|
|
32571
32841
|
stopped: true,
|
|
32572
32842
|
through: stopped.throughEditor ? "editor" : "gdharness",
|
|
32573
32843
|
endedPid,
|
|
32574
32844
|
...ended === null ? {} : {
|
|
32575
|
-
endedChildren: ended.ended,
|
|
32845
|
+
endedChildren: ended.ended.map((one) => one.pid),
|
|
32846
|
+
identifiedBy: ended.ended.length === 0 ? undefined : Object.fromEntries(ended.ended.map((one) => [String(one.pid), one.identifiedBy])),
|
|
32576
32847
|
childrenLeft: ended.left.length === 0 ? undefined : ended.left,
|
|
32577
|
-
childrenUnknown: ended.unknown ?
|
|
32848
|
+
childrenUnknown: ended.unknown === false ? undefined : true
|
|
32578
32849
|
},
|
|
32579
32850
|
exitedBeforeStop: stopped.exitCode !== null || !wasRunning,
|
|
32580
32851
|
exitCode: stopped.exitCode,
|
|
@@ -32585,24 +32856,73 @@ class GodotServer {
|
|
|
32585
32856
|
entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
32586
32857
|
});
|
|
32587
32858
|
}
|
|
32588
|
-
|
|
32589
|
-
|
|
32590
|
-
|
|
32591
|
-
|
|
32592
|
-
|
|
32593
|
-
const
|
|
32594
|
-
const
|
|
32595
|
-
|
|
32596
|
-
|
|
32597
|
-
|
|
32859
|
+
async whatTheRunStarted(run) {
|
|
32860
|
+
const tree = await processTree();
|
|
32861
|
+
if (tree === undefined) {
|
|
32862
|
+
return { unknown: "platform" };
|
|
32863
|
+
}
|
|
32864
|
+
const announced = this.announcedOfTheRunsProject(run, runtimesAnnounced().running);
|
|
32865
|
+
const games = new Set(announced.map((one) => one.pid));
|
|
32866
|
+
const own = this.announcedPidOf(run, announced) ?? this.tieThroughTheTree(run, tree, games) ?? run.gamePid ?? this.gameByCommandLineIn(run, tree);
|
|
32867
|
+
if (own !== undefined && run.announcedPid === undefined) {
|
|
32868
|
+
run.gamePid = own;
|
|
32869
|
+
}
|
|
32870
|
+
const roots = [run.pid, own].filter((one) => one !== null && one !== undefined);
|
|
32871
|
+
if (roots.length === 0) {
|
|
32872
|
+
return { unknown: "no process" };
|
|
32873
|
+
}
|
|
32874
|
+
const under = new Set(roots.flatMap((root) => descendantsIn(tree, root)));
|
|
32875
|
+
for (const root of roots) {
|
|
32876
|
+
under.delete(root);
|
|
32877
|
+
}
|
|
32878
|
+
const started = [...under].sort((a, b) => a - b);
|
|
32879
|
+
const found = [];
|
|
32880
|
+
const others = [];
|
|
32881
|
+
for (const pid of started) {
|
|
32882
|
+
if (games.has(pid)) {
|
|
32883
|
+
found.push({ pid, how: "announced", identifiedBy: "announced as a game of this project" });
|
|
32598
32884
|
continue;
|
|
32599
32885
|
}
|
|
32600
|
-
|
|
32601
|
-
|
|
32602
|
-
|
|
32603
|
-
|
|
32886
|
+
const engines = [...roots, ...roots.flatMap((root) => ancestorsIn(tree, pid, root))];
|
|
32887
|
+
const read = this.theProjectsEngine(run, tree, pid, engines);
|
|
32888
|
+
if (read !== null) {
|
|
32889
|
+
found.push({
|
|
32890
|
+
pid,
|
|
32891
|
+
how: "command line",
|
|
32892
|
+
identifiedBy: `${read.executable} run with --path ${read.projectPath ?? ""}, this project's engine on this project`
|
|
32893
|
+
});
|
|
32894
|
+
continue;
|
|
32895
|
+
}
|
|
32896
|
+
others.push(pid);
|
|
32897
|
+
}
|
|
32898
|
+
return { unknown: false, games: found, others };
|
|
32899
|
+
}
|
|
32900
|
+
theProjectsEngine(run, tree, pid, engines) {
|
|
32901
|
+
const project = run.projectPath;
|
|
32902
|
+
if (project === null) {
|
|
32903
|
+
return null;
|
|
32904
|
+
}
|
|
32905
|
+
const read = readCommandLine(tree.get(pid)?.command ?? "");
|
|
32906
|
+
if (read.executable === "" || read.editor || read.projectPath === null || !isSameDirectory(read.projectPath, project)) {
|
|
32907
|
+
return null;
|
|
32908
|
+
}
|
|
32909
|
+
const names = engines.flatMap((one) => engineNamesOf(readCommandLine(tree.get(one)?.command ?? "").executable));
|
|
32910
|
+
const sameName = (one, other) => one !== "" && (process.platform === "win32" ? one.toLowerCase() === other.toLowerCase() : one === other);
|
|
32911
|
+
return names.some((name) => sameName(name, read.executable)) ? read : null;
|
|
32912
|
+
}
|
|
32913
|
+
gameByCommandLineIn(run, tree) {
|
|
32914
|
+
const root = run.pid ?? (run.throughEditor ? this.godotBridge.getStatus().editorPid : undefined);
|
|
32915
|
+
if (root === undefined || run.projectPath === null) {
|
|
32916
|
+
return;
|
|
32917
|
+
}
|
|
32918
|
+
const rootRead = readCommandLine(tree.get(root)?.command ?? "");
|
|
32919
|
+
const rootIsTheGame = !rootRead.editor && !CONSOLE_WRAPPER.test(rootRead.executable) && rootRead.projectPath !== null && isSameDirectory(rootRead.projectPath, run.projectPath);
|
|
32920
|
+
if (rootIsTheGame) {
|
|
32921
|
+
return;
|
|
32604
32922
|
}
|
|
32605
|
-
|
|
32923
|
+
const isEngine = (pid) => this.theProjectsEngine(run, tree, pid, [root]) !== null;
|
|
32924
|
+
const nearest = descendantsIn(tree, root).filter((pid) => isEngine(pid) && !ancestorsIn(tree, pid, root).some((one) => one !== root && isEngine(one)));
|
|
32925
|
+
return nearest.length === 1 ? nearest[0] : undefined;
|
|
32606
32926
|
}
|
|
32607
32927
|
async handleRescanFilesystem(args) {
|
|
32608
32928
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 30000;
|
|
@@ -32799,7 +33119,7 @@ class GodotServer {
|
|
|
32799
33119
|
const { op: _op, projectPath, pid, ...params } = asParams(args);
|
|
32800
33120
|
const announced = runtimesAnnounced();
|
|
32801
33121
|
const wanted = typeof projectPath === "string" ? projectPath : this.ownProject ?? undefined;
|
|
32802
|
-
const own = typeof pid === "number" ? undefined : this.ownGameAmong(announced.running, wanted);
|
|
33122
|
+
const own = typeof pid === "number" ? undefined : this.ownGameAmong(announced.running, wanted) ?? await this.ownGameThroughTheTree(announced.running, wanted);
|
|
32803
33123
|
const choice = chooseRuntime(announced.running, wanted, announced.unspoken, typeof pid === "number" ? pid : own);
|
|
32804
33124
|
if ("problem" in choice) {
|
|
32805
33125
|
return this.createErrorResponse(choice.problem);
|