gdharness 1.0.23 → 1.0.25
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/index.js
CHANGED
|
@@ -25504,14 +25504,43 @@ function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
|
|
|
25504
25504
|
};
|
|
25505
25505
|
}
|
|
25506
25506
|
var nextRequestId = 1;
|
|
25507
|
-
|
|
25507
|
+
var LATE_ANSWER_MS = 30 * 60000;
|
|
25508
|
+
var LATE_KEPT = 50;
|
|
25509
|
+
var lateAnswers = new Map;
|
|
25510
|
+
function keepLate(id, answer) {
|
|
25511
|
+
lateAnswers.delete(id);
|
|
25512
|
+
lateAnswers.set(id, answer);
|
|
25513
|
+
while (lateAnswers.size > LATE_KEPT) {
|
|
25514
|
+
const oldest = lateAnswers.keys().next().value;
|
|
25515
|
+
if (oldest === undefined) {
|
|
25516
|
+
break;
|
|
25517
|
+
}
|
|
25518
|
+
lateAnswers.delete(oldest);
|
|
25519
|
+
}
|
|
25520
|
+
}
|
|
25521
|
+
function lateAnswer(id) {
|
|
25522
|
+
return lateAnswers.get(id);
|
|
25523
|
+
}
|
|
25524
|
+
function runtimeRequest(endpoint, command, params, timeoutMs, keepListening = false) {
|
|
25508
25525
|
const id = nextRequestId++;
|
|
25526
|
+
const sentAt = Date.now();
|
|
25509
25527
|
return new Promise((settle) => {
|
|
25510
25528
|
let done = false;
|
|
25529
|
+
let late = false;
|
|
25511
25530
|
let buffered = "";
|
|
25512
25531
|
let welcomed = false;
|
|
25513
25532
|
const socket = createConnection({ port: endpoint.port, host: endpoint.address });
|
|
25533
|
+
let lateTimer = null;
|
|
25514
25534
|
const finish = (reply) => {
|
|
25535
|
+
if (late) {
|
|
25536
|
+
if (lateTimer !== null) {
|
|
25537
|
+
clearTimeout(lateTimer);
|
|
25538
|
+
}
|
|
25539
|
+
socket.destroy();
|
|
25540
|
+
keepLate(id, reply.ok || reply.reason === "error" ? { state: "answered", command, sentAt, answeredAt: Date.now(), reply } : { state: "never", command, sentAt, why: reply.message });
|
|
25541
|
+
late = false;
|
|
25542
|
+
return;
|
|
25543
|
+
}
|
|
25515
25544
|
if (done) {
|
|
25516
25545
|
return;
|
|
25517
25546
|
}
|
|
@@ -25521,10 +25550,32 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
|
|
|
25521
25550
|
settle(reply);
|
|
25522
25551
|
};
|
|
25523
25552
|
const timer = setTimeout(() => {
|
|
25524
|
-
|
|
25553
|
+
const still = welcomed ? `The game (${describe2(endpoint)}) has the request and did not answer '${command}' within ${timeoutMs}ms.` : `The game (${describe2(endpoint)}) did not answer '${command}' within ${timeoutMs}ms and had not taken the request: its frame has not ended since it was sent, which a long frame or a breakpoint does, and the request runs when it does.`;
|
|
25554
|
+
if (!keepListening) {
|
|
25555
|
+
finish({
|
|
25556
|
+
ok: false,
|
|
25557
|
+
reason: "busy",
|
|
25558
|
+
message: `${still} It is either working through something long or held at a breakpoint.`
|
|
25559
|
+
});
|
|
25560
|
+
return;
|
|
25561
|
+
}
|
|
25562
|
+
done = true;
|
|
25563
|
+
late = true;
|
|
25564
|
+
keepLate(id, { state: "waiting", command, sentAt });
|
|
25565
|
+
lateTimer = setTimeout(() => {
|
|
25566
|
+
finish({
|
|
25567
|
+
ok: false,
|
|
25568
|
+
reason: "busy",
|
|
25569
|
+
message: `No answer came within ${LATE_ANSWER_MS / 60000} minutes.`
|
|
25570
|
+
});
|
|
25571
|
+
}, LATE_ANSWER_MS);
|
|
25572
|
+
lateTimer.unref();
|
|
25573
|
+
socket.unref();
|
|
25574
|
+
settle({
|
|
25525
25575
|
ok: false,
|
|
25526
25576
|
reason: "busy",
|
|
25527
|
-
|
|
25577
|
+
requestId: id,
|
|
25578
|
+
message: `${still} Nothing was cancelled: a call that takes longer than the wait goes on and does everything it was asked, so read the state back rather than repeating it. runtime_invoke op result with requestId ${id} answers with its reply once it comes, and timeoutMs waits longer next time. A game held at a breakpoint instead is one debug_state stack answers about.`
|
|
25528
25579
|
});
|
|
25529
25580
|
}, timeoutMs);
|
|
25530
25581
|
socket.setEncoding("utf8");
|
|
@@ -25536,7 +25587,7 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
|
|
|
25536
25587
|
buffered += chunk;
|
|
25537
25588
|
let newline = buffered.indexOf(`
|
|
25538
25589
|
`);
|
|
25539
|
-
while (newline !== -1 && !done) {
|
|
25590
|
+
while (newline !== -1 && (!done || late)) {
|
|
25540
25591
|
const line = buffered.slice(0, newline).trim();
|
|
25541
25592
|
buffered = buffered.slice(newline + 1);
|
|
25542
25593
|
newline = buffered.indexOf(`
|
|
@@ -28192,6 +28243,81 @@ function whyNoReport(printed, asked) {
|
|
|
28192
28243
|
}
|
|
28193
28244
|
return printed.some((line) => NOTHING_RAN.test(line)) ? `no test cases found at ${asked}` : null;
|
|
28194
28245
|
}
|
|
28246
|
+
var ESCAPE = String.fromCharCode(27);
|
|
28247
|
+
var PRINTED_ACTUAL = new RegExp(` but was\\n '([\\s\\S]*?)'((?:${ESCAPE}\\[[0-9;]*m)*)\\tat ('[^'\\n]*' in \\S+:\\d+)`, "g");
|
|
28248
|
+
var COLOUR = new RegExp(`${ESCAPE}\\[([0-9;]*)m`, "g");
|
|
28249
|
+
var SPELLED = {
|
|
28250
|
+
"<LF>": `
|
|
28251
|
+
`,
|
|
28252
|
+
"<CR>": "\r",
|
|
28253
|
+
"<TAB>": "\t",
|
|
28254
|
+
"<BS>": "\b",
|
|
28255
|
+
"<FF>": "\f",
|
|
28256
|
+
"<VT>": "\v",
|
|
28257
|
+
"<BEL>": "\x07",
|
|
28258
|
+
"<ESC>": ESCAPE,
|
|
28259
|
+
"<DEL>": ""
|
|
28260
|
+
};
|
|
28261
|
+
function unspelled(text) {
|
|
28262
|
+
return text.replace(/<(?:LF|CR|TAB|BS|FF|VT|BEL|ESC|DEL|0x[0-9A-F]{2})>/g, (spelling) => spelling.startsWith("<0x") ? String.fromCharCode(Number.parseInt(spelling.slice(3, 5), 16)) : SPELLED[spelling] ?? spelling);
|
|
28263
|
+
}
|
|
28264
|
+
function readDiff(printed) {
|
|
28265
|
+
let merged = "";
|
|
28266
|
+
let actual = "";
|
|
28267
|
+
let background = null;
|
|
28268
|
+
let last = 0;
|
|
28269
|
+
const take = (text) => {
|
|
28270
|
+
merged += text;
|
|
28271
|
+
if (background === null) {
|
|
28272
|
+
actual += text;
|
|
28273
|
+
} else if (background === "added") {
|
|
28274
|
+
actual += unspelled(text);
|
|
28275
|
+
}
|
|
28276
|
+
};
|
|
28277
|
+
for (const match of printed.matchAll(COLOUR)) {
|
|
28278
|
+
take(printed.slice(last, match.index));
|
|
28279
|
+
last = match.index + match[0].length;
|
|
28280
|
+
const codes = (match[1] ?? "").split(";");
|
|
28281
|
+
if (codes[0] === "48" && codes[1] === "2") {
|
|
28282
|
+
background = Number(codes[2]) > Number(codes[3]) ? "missing" : "added";
|
|
28283
|
+
} else if (codes[0] === "0" || codes[0] === "") {
|
|
28284
|
+
background = null;
|
|
28285
|
+
}
|
|
28286
|
+
}
|
|
28287
|
+
take(printed.slice(last));
|
|
28288
|
+
return { merged, actual };
|
|
28289
|
+
}
|
|
28290
|
+
var BOTH_SIDES_ALIKE_NOTE = "[gdharness: the expected and the actual printed alike because gdUnit4 drops the marks that tell them apart when it writes its report, and the runner printed no copy of this one, so neither side above is the value the assertion found.]";
|
|
28291
|
+
function withActualsPrinted(failed, printed) {
|
|
28292
|
+
const diffs = [...printed.replaceAll(`\r
|
|
28293
|
+
`, `
|
|
28294
|
+
`).matchAll(PRINTED_ACTUAL)].map((match) => ({
|
|
28295
|
+
at: match[3] ?? "",
|
|
28296
|
+
...readDiff(match[1] ?? "")
|
|
28297
|
+
}));
|
|
28298
|
+
return failed.map((entry) => {
|
|
28299
|
+
if (entry.detail === null) {
|
|
28300
|
+
return entry;
|
|
28301
|
+
}
|
|
28302
|
+
let detail = entry.detail;
|
|
28303
|
+
for (const diff of diffs) {
|
|
28304
|
+
const wrong = ` but was
|
|
28305
|
+
'${diff.merged.replaceAll("\r", "")}'
|
|
28306
|
+
at ${diff.at}`;
|
|
28307
|
+
if (detail.includes(wrong)) {
|
|
28308
|
+
detail = detail.replace(wrong, () => ` but was
|
|
28309
|
+
'${diff.actual}'
|
|
28310
|
+
at ${diff.at}`);
|
|
28311
|
+
}
|
|
28312
|
+
}
|
|
28313
|
+
const alike = /Expecting:\n '([\s\S]*)'\n but was\n '\1'\n\tat /.test(detail);
|
|
28314
|
+
if (alike) {
|
|
28315
|
+
detail = `${detail}
|
|
28316
|
+
${BOTH_SIDES_ALIKE_NOTE}`;
|
|
28317
|
+
}
|
|
28318
|
+
return detail === entry.detail ? entry : { ...entry, detail };
|
|
28319
|
+
});
|
|
28320
|
+
}
|
|
28195
28321
|
var RUNNING_SUITE = /Run Test Suite:\s*(\S+)/;
|
|
28196
28322
|
var ORPHANS = /(\d+)\s+orphans/;
|
|
28197
28323
|
var OVERALL = "Overall Summary";
|
|
@@ -29260,14 +29386,19 @@ function editorNoteAt(path) {
|
|
|
29260
29386
|
}
|
|
29261
29387
|
return { projectPath, transcript, startedAt };
|
|
29262
29388
|
}
|
|
29263
|
-
function recordRunEnded(pid,
|
|
29389
|
+
function recordRunEnded(pid, ending) {
|
|
29264
29390
|
const path = recordPath();
|
|
29265
29391
|
const record = recordAt(path);
|
|
29266
29392
|
if (record === null || record.pid !== pid) {
|
|
29267
29393
|
return;
|
|
29268
29394
|
}
|
|
29395
|
+
const written = {
|
|
29396
|
+
...record,
|
|
29397
|
+
...ending.exitCode === null ? {} : { exitCode: ending.exitCode },
|
|
29398
|
+
...ending.exitSignal === null ? {} : { exitSignal: ending.exitSignal }
|
|
29399
|
+
};
|
|
29269
29400
|
try {
|
|
29270
|
-
writeFileSync9(path, JSON.stringify(
|
|
29401
|
+
writeFileSync9(path, JSON.stringify(written, null, 2), "utf8");
|
|
29271
29402
|
} catch {}
|
|
29272
29403
|
}
|
|
29273
29404
|
function readRunRecord() {
|
|
@@ -29304,7 +29435,8 @@ function recordAt(path) {
|
|
|
29304
29435
|
projectPath: typeof fields["projectPath"] === "string" ? fields["projectPath"] : "",
|
|
29305
29436
|
arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : [],
|
|
29306
29437
|
...typeof fields["command"] === "string" ? { command: fields["command"] } : {},
|
|
29307
|
-
...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {}
|
|
29438
|
+
...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
|
|
29439
|
+
...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {}
|
|
29308
29440
|
};
|
|
29309
29441
|
}
|
|
29310
29442
|
function runningAs(pid) {
|
|
@@ -29718,7 +29850,7 @@ var TILEMAP_CELLS = {
|
|
|
29718
29850
|
additionalProperties: false
|
|
29719
29851
|
}
|
|
29720
29852
|
};
|
|
29721
|
-
var
|
|
29853
|
+
var COLOUR2 = {
|
|
29722
29854
|
type: "object",
|
|
29723
29855
|
description: "set_theme_color: r, g and b from 0 to 1, with a optional and opaque by default.",
|
|
29724
29856
|
properties: { r: { type: "number" }, g: { type: "number" }, b: { type: "number" }, a: { type: "number" } },
|
|
@@ -30148,7 +30280,7 @@ var TOOL_SPECS = [
|
|
|
30148
30280
|
sources: TILESET_SOURCES,
|
|
30149
30281
|
controlType: { type: "string", description: 'Theme ops: the Control class, such as "Button".' },
|
|
30150
30282
|
colorName: { type: "string", description: 'set_theme_color: such as "font_color".' },
|
|
30151
|
-
color:
|
|
30283
|
+
color: COLOUR2,
|
|
30152
30284
|
fontSizeName: { type: "string", description: 'set_theme_font_size: such as "font_size".' },
|
|
30153
30285
|
size: { type: "number", description: "set_theme_font_size: pixels." }
|
|
30154
30286
|
},
|
|
@@ -30244,7 +30376,7 @@ var TOOL_SPECS = [
|
|
|
30244
30376
|
},
|
|
30245
30377
|
{
|
|
30246
30378
|
name: "editor_output",
|
|
30247
|
-
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. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, which is where the output worth reading usually is, since the class cache, the plugins and the language server all say what they have to say before any harness has connected. The answer carries the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing.",
|
|
30379
|
+
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 ended by a signal has no exit code, and exitSignal names the signal instead. 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. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, which is where the output worth reading usually is, since the class cache, the plugins and the language server all say what they have to say before any harness has connected. The answer carries the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing.",
|
|
30248
30380
|
parameters: {
|
|
30249
30381
|
severity: {
|
|
30250
30382
|
type: "string",
|
|
@@ -30364,7 +30496,7 @@ var TOOL_SPECS = [
|
|
|
30364
30496
|
property: {
|
|
30365
30497
|
type: "string",
|
|
30366
30498
|
ops: ["property", "find"],
|
|
30367
|
-
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.'
|
|
30499
|
+
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 list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.'
|
|
30368
30500
|
},
|
|
30369
30501
|
depth: { type: "number", ops: ["tree"], description: "tree: levels to descend. Default 3." },
|
|
30370
30502
|
includeProperties: {
|
|
@@ -30410,6 +30542,10 @@ var TOOL_SPECS = [
|
|
|
30410
30542
|
items: { type: "string" },
|
|
30411
30543
|
ops: ["metrics"],
|
|
30412
30544
|
description: "metrics: which to read. Default all."
|
|
30545
|
+
},
|
|
30546
|
+
timeoutMs: {
|
|
30547
|
+
type: "number",
|
|
30548
|
+
description: "How long to wait for the answer before answering pending with a requestId, which runtime_invoke op result collects the reply by. Default 10000, or GDHARNESS_RUNTIME_TIMEOUT_MS."
|
|
30413
30549
|
}
|
|
30414
30550
|
},
|
|
30415
30551
|
requires: [],
|
|
@@ -30437,11 +30573,15 @@ var TOOL_SPECS = [
|
|
|
30437
30573
|
},
|
|
30438
30574
|
{
|
|
30439
30575
|
name: "runtime_invoke",
|
|
30440
|
-
description: "Sets a property or calls a method on a node in the running game. Needs the game running with the runtime addon.",
|
|
30576
|
+
description: "Sets a property or calls a method on a node in the running game. Needs the game running with the runtime addon. A call that takes longer than timeoutMs is not cancelled: the answer is pending: true with a requestId, the call goes on and does everything it was asked, and op result with that requestId collects its reply once it comes.",
|
|
30441
30577
|
parameters: {
|
|
30442
30578
|
projectPath: RUNNING_PROJECT_PATH,
|
|
30443
30579
|
pid: RUNNING_PID,
|
|
30444
|
-
nodePath: {
|
|
30580
|
+
nodePath: {
|
|
30581
|
+
type: "string",
|
|
30582
|
+
ops: ["set", "call"],
|
|
30583
|
+
description: 'Absolute node path, such as "/root/Main/Player".'
|
|
30584
|
+
},
|
|
30445
30585
|
property: {
|
|
30446
30586
|
type: "string",
|
|
30447
30587
|
ops: ["set"],
|
|
@@ -30461,17 +30601,31 @@ var TOOL_SPECS = [
|
|
|
30461
30601
|
type: "array",
|
|
30462
30602
|
ops: ["call"],
|
|
30463
30603
|
description: `call: the arguments, fitted to the method's parameter types. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths.`
|
|
30604
|
+
},
|
|
30605
|
+
timeoutMs: {
|
|
30606
|
+
type: "number",
|
|
30607
|
+
ops: ["set", "call"],
|
|
30608
|
+
description: "set, call: how long to wait for the answer before answering pending with a requestId, which does not stop the call. Default 10000, or GDHARNESS_RUNTIME_TIMEOUT_MS. Give a longer one for a call known to take a while, such as one that plays many turns in one go."
|
|
30609
|
+
},
|
|
30610
|
+
requestId: {
|
|
30611
|
+
type: "number",
|
|
30612
|
+
ops: ["result"],
|
|
30613
|
+
description: "result: the requestId a pending answer gave."
|
|
30464
30614
|
}
|
|
30465
30615
|
},
|
|
30466
|
-
requires: [
|
|
30616
|
+
requires: [],
|
|
30467
30617
|
operations: {
|
|
30468
30618
|
set: {
|
|
30469
30619
|
summary: "set a property, on a node or on an object it holds",
|
|
30470
|
-
requires: ["property", "value"]
|
|
30620
|
+
requires: ["nodePath", "property", "value"]
|
|
30471
30621
|
},
|
|
30472
30622
|
call: {
|
|
30473
30623
|
summary: "call a method, on a node or on an object it holds, and return its result",
|
|
30474
|
-
requires: ["method"]
|
|
30624
|
+
requires: ["nodePath", "method"]
|
|
30625
|
+
},
|
|
30626
|
+
result: {
|
|
30627
|
+
summary: "collect the reply to a set or call whose wait ran out, by the requestId it answered with",
|
|
30628
|
+
requires: ["requestId"]
|
|
30475
30629
|
}
|
|
30476
30630
|
}
|
|
30477
30631
|
},
|
|
@@ -30632,7 +30786,7 @@ var TOOL_SPECS = [
|
|
|
30632
30786
|
says: {
|
|
30633
30787
|
type: "string",
|
|
30634
30788
|
ops: ["until"],
|
|
30635
|
-
description: "until: wait for these words to appear anywhere under nodePath, or anywhere in the game when no nodePath is given, 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."
|
|
30789
|
+
description: "until: wait for these words to appear anywhere under nodePath, or anywhere in the game when no nodePath is given, 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. It looks every frame while a look is cheap and less often on a screen large enough for a look to cost a real share of a frame, so the game keeps its speed; the answer counts the frames the wait spanned and the looks it took."
|
|
30636
30790
|
},
|
|
30637
30791
|
includeHidden: {
|
|
30638
30792
|
type: "boolean",
|
|
@@ -30803,8 +30957,21 @@ function describeHalt(halt) {
|
|
|
30803
30957
|
return `stopped with reason "${halt.reason}"`;
|
|
30804
30958
|
}
|
|
30805
30959
|
}
|
|
30960
|
+
function exited(run) {
|
|
30961
|
+
return run.exitCode !== null || run.exitSignal !== null;
|
|
30962
|
+
}
|
|
30963
|
+
function endingOf(failed) {
|
|
30964
|
+
return {
|
|
30965
|
+
exitCode: typeof failed.code === "number" ? failed.code : null,
|
|
30966
|
+
exitSignal: typeof failed.signal === "string" ? failed.signal : null,
|
|
30967
|
+
failure: typeof failed.code === "string" ? failed.code : null
|
|
30968
|
+
};
|
|
30969
|
+
}
|
|
30970
|
+
function howItExited(run) {
|
|
30971
|
+
return run.exitSignal === null ? `exit code ${run.exitCode ?? "unknown"}` : `ended by ${run.exitSignal}, with no exit code`;
|
|
30972
|
+
}
|
|
30806
30973
|
function stillRunning(run) {
|
|
30807
|
-
if (run
|
|
30974
|
+
if (run === null || exited(run) || run.endedUnwatched === true) {
|
|
30808
30975
|
return false;
|
|
30809
30976
|
}
|
|
30810
30977
|
if (typeof run.endedHere === "string") {
|
|
@@ -30835,9 +31002,10 @@ function runIsUp(run, editorSays, now = Date.now()) {
|
|
|
30835
31002
|
return editorSays ?? stillRunning(run);
|
|
30836
31003
|
}
|
|
30837
31004
|
function noCodeWillCome(run) {
|
|
30838
|
-
return run
|
|
31005
|
+
return !exited(run) && run.process === null;
|
|
30839
31006
|
}
|
|
30840
31007
|
var STOP_WAIT_MS = 1e4;
|
|
31008
|
+
var EXIT_REPORTED_WITHIN_MS = 2000;
|
|
30841
31009
|
async function untilGone(handle, pids, withinMs = STOP_WAIT_MS) {
|
|
30842
31010
|
const gone = () => (handle?.exitCode !== null || handle.signalCode !== null) && pids.every((pid) => !alive(pid));
|
|
30843
31011
|
const deadline = Date.now() + withinMs;
|
|
@@ -31716,7 +31884,8 @@ class GodotServer {
|
|
|
31716
31884
|
return await this.handleEditorStatus();
|
|
31717
31885
|
case "editor_rescan":
|
|
31718
31886
|
return await this.handleRescanFilesystem(args);
|
|
31719
|
-
case "runtime_inspect":
|
|
31887
|
+
case "runtime_inspect": {
|
|
31888
|
+
const patience = readPositiveNumber(args, "timeoutMs") ?? this.runtimeTimeoutMs();
|
|
31720
31889
|
switch (op) {
|
|
31721
31890
|
case "tree":
|
|
31722
31891
|
return await this.handleRuntimeCommand("get_tree", {
|
|
@@ -31725,45 +31894,45 @@ class GodotServer {
|
|
|
31725
31894
|
depth: readPositiveNumber(args, "depth") ?? 3,
|
|
31726
31895
|
include_properties: readBoolean(args, "includeProperties") ?? false,
|
|
31727
31896
|
properties: readArray(args, "properties") ?? []
|
|
31728
|
-
});
|
|
31897
|
+
}, patience);
|
|
31729
31898
|
case "find":
|
|
31730
|
-
return await this.handleFindRuntimeNodes(args);
|
|
31899
|
+
return await this.handleFindRuntimeNodes(args, patience);
|
|
31731
31900
|
case "text":
|
|
31732
31901
|
return await this.handleRuntimeCommand("read_text", {
|
|
31733
31902
|
...whichGame(args),
|
|
31734
31903
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
31735
31904
|
include_hidden: readBoolean(args, "includeHidden") ?? false,
|
|
31736
31905
|
limit: readPositiveNumber(args, "limit") ?? 500
|
|
31737
|
-
});
|
|
31906
|
+
}, patience);
|
|
31738
31907
|
case "rect":
|
|
31739
|
-
return await this.handleRuntimeCommand("get_rect", {
|
|
31740
|
-
...whichGame(args),
|
|
31741
|
-
path: readNonEmptyString(args, "nodePath") ?? ""
|
|
31742
|
-
});
|
|
31908
|
+
return await this.handleRuntimeCommand("get_rect", { ...whichGame(args), path: readNonEmptyString(args, "nodePath") ?? "" }, patience);
|
|
31743
31909
|
case "property":
|
|
31744
31910
|
return await this.handleRuntimeCommand("get_property", {
|
|
31745
31911
|
...whichGame(args),
|
|
31746
31912
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
31747
31913
|
property: readNonEmptyString(args, "property") ?? ""
|
|
31748
|
-
});
|
|
31914
|
+
}, patience);
|
|
31749
31915
|
default:
|
|
31750
|
-
return await this.handleRuntimeCommand("get_metrics", {
|
|
31751
|
-
...whichGame(args),
|
|
31752
|
-
metrics: readArray(args, "metrics") ?? []
|
|
31753
|
-
});
|
|
31916
|
+
return await this.handleRuntimeCommand("get_metrics", { ...whichGame(args), metrics: readArray(args, "metrics") ?? [] }, patience);
|
|
31754
31917
|
}
|
|
31755
|
-
|
|
31918
|
+
}
|
|
31919
|
+
case "runtime_invoke": {
|
|
31920
|
+
if (op === "result") {
|
|
31921
|
+
return this.handleLateAnswer(args);
|
|
31922
|
+
}
|
|
31923
|
+
const patience = readPositiveNumber(args, "timeoutMs") ?? this.runtimeTimeoutMs();
|
|
31756
31924
|
return op === "set" ? await this.handleRuntimeCommand("set_property", {
|
|
31757
31925
|
...whichGame(args),
|
|
31758
31926
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
31759
31927
|
property: readString(args, "property") ?? "",
|
|
31760
31928
|
value: args["value"]
|
|
31761
|
-
}) : await this.handleRuntimeCommand("call_method", {
|
|
31929
|
+
}, patience) : await this.handleRuntimeCommand("call_method", {
|
|
31762
31930
|
...whichGame(args),
|
|
31763
31931
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
31764
31932
|
method: readString(args, "method") ?? "",
|
|
31765
31933
|
args: readArray(args, "args") ?? []
|
|
31766
|
-
});
|
|
31934
|
+
}, patience);
|
|
31935
|
+
}
|
|
31767
31936
|
case "runtime_capture":
|
|
31768
31937
|
return await this.handleRuntimeCommand(op === "screenshot" ? "capture_screenshot" : "capture_viewport", args);
|
|
31769
31938
|
case "runtime_input":
|
|
@@ -31914,10 +32083,10 @@ class GodotServer {
|
|
|
31914
32083
|
}
|
|
31915
32084
|
async debuggedGame(intent) {
|
|
31916
32085
|
const game = this.activeProcess;
|
|
31917
|
-
if (game !== null && game
|
|
32086
|
+
if (game !== null && exited(game)) {
|
|
31918
32087
|
return {
|
|
31919
32088
|
ok: false,
|
|
31920
|
-
response: this.createErrorResponse(`The last run has already finished (
|
|
32089
|
+
response: this.createErrorResponse(`The last run has already finished (${howItExited(game)}), so there is no debug session to answer for.`, [
|
|
31921
32090
|
"editor_output still reads what it printed, until the next run starts",
|
|
31922
32091
|
"editor_run start plays another, which the debugger can hold"
|
|
31923
32092
|
])
|
|
@@ -32191,10 +32360,11 @@ class GodotServer {
|
|
|
32191
32360
|
];
|
|
32192
32361
|
this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
|
|
32193
32362
|
const log = new GameLog;
|
|
32194
|
-
let
|
|
32363
|
+
let ending = { exitCode: 0, exitSignal: null, failure: null };
|
|
32195
32364
|
try {
|
|
32196
32365
|
const { stdout, stderr } = await run5(engine.value, exportArgs, {
|
|
32197
32366
|
timeout: 300000,
|
|
32367
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
32198
32368
|
signal: callSignal()
|
|
32199
32369
|
});
|
|
32200
32370
|
log.append("stdout", stdout);
|
|
@@ -32206,26 +32376,32 @@ class GodotServer {
|
|
|
32206
32376
|
const failed = error;
|
|
32207
32377
|
log.append("stdout", failed.stdout);
|
|
32208
32378
|
log.append("stderr", failed.stderr);
|
|
32209
|
-
|
|
32379
|
+
ending = endingOf(failed);
|
|
32210
32380
|
} finally {
|
|
32211
32381
|
discard(exportLogs);
|
|
32212
32382
|
}
|
|
32213
32383
|
log.finish();
|
|
32214
32384
|
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
32215
32385
|
const verdict = {
|
|
32216
|
-
exported: exitCode === 0 && log.count("error") === 0 && existsSync10(output.absolutePath),
|
|
32386
|
+
exported: ending.exitCode === 0 && log.count("error") === 0 && existsSync10(output.absolutePath),
|
|
32217
32387
|
preset,
|
|
32218
32388
|
outputPath: output.relativePath,
|
|
32219
32389
|
debug,
|
|
32220
|
-
exitCode,
|
|
32390
|
+
exitCode: ending.exitCode,
|
|
32391
|
+
exitSignal: ending.exitSignal ?? undefined,
|
|
32392
|
+
failure: ending.failure ?? undefined,
|
|
32221
32393
|
errors: log.count("error"),
|
|
32222
32394
|
warnings: log.count("warning"),
|
|
32223
32395
|
entries: forAnswer(problems.entries)
|
|
32224
32396
|
};
|
|
32225
32397
|
if (!verdict.exported) {
|
|
32398
|
+
const why = ending.exitSignal === null ? ending.failure === null ? "" : ` The engine was ended by Node (${ending.failure}), so it has no exit code.` : ` The engine was ended by ${ending.exitSignal}, so it has no exit code: the export ran past its five minutes or the call was cancelled.`;
|
|
32226
32399
|
return {
|
|
32227
32400
|
content: [
|
|
32228
|
-
{
|
|
32401
|
+
{
|
|
32402
|
+
type: "text",
|
|
32403
|
+
text: `Export with preset '${preset}' did not produce ${output.relativePath}.${why}`
|
|
32404
|
+
},
|
|
32229
32405
|
{ type: "text", text: JSON.stringify(verdict, null, 2) }
|
|
32230
32406
|
],
|
|
32231
32407
|
isError: true
|
|
@@ -32280,6 +32456,10 @@ class GodotServer {
|
|
|
32280
32456
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
32281
32457
|
const userData = mkdtempSync2(join16(tmpdir4(), "gdharness-tests-"));
|
|
32282
32458
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
32459
|
+
const console2 = [];
|
|
32460
|
+
run.process.stdout?.on("data", (data) => {
|
|
32461
|
+
console2.push(data);
|
|
32462
|
+
});
|
|
32283
32463
|
const hung = await new Promise((resolve) => {
|
|
32284
32464
|
const timer = setTimeout(() => {
|
|
32285
32465
|
run.process.kill();
|
|
@@ -32324,7 +32504,7 @@ class GodotServer {
|
|
|
32324
32504
|
const printed = run.log.select({ severity: "info", sinceLastCall: false, limit: 200 }).entries;
|
|
32325
32505
|
const said = printed.map((entry) => entry.text);
|
|
32326
32506
|
const nothingRan = hung ? null : whyNoReport(said, asked);
|
|
32327
|
-
const verdict = hung ? `hung: killed after ${timeoutMs} ms` : nothingRan ?? verdicts[exitCode ??
|
|
32507
|
+
const verdict = hung ? `hung: killed after ${timeoutMs} ms` : nothingRan ?? (exitCode === null ? undefined : verdicts[exitCode]) ?? (run.exitSignal === null ? `exit ${exitCode ?? "unknown"}` : howItExited(run));
|
|
32328
32508
|
const savesNote = savesStayPut() && nothingRan === null ? SAVES_NOT_MOVED_NOTE : undefined;
|
|
32329
32509
|
if (report === null) {
|
|
32330
32510
|
const note = nothingRan === null ? `The test run wrote no report (${verdict}${reportProblem ? `; ${reportProblem}` : ""}).` : `No tests ran: ${verdict}.${elsewhereIn(project.value.path, asked)}`;
|
|
@@ -32338,6 +32518,7 @@ class GodotServer {
|
|
|
32338
32518
|
verdict,
|
|
32339
32519
|
tests: 0,
|
|
32340
32520
|
exitCode,
|
|
32521
|
+
exitSignal: run.exitSignal ?? undefined,
|
|
32341
32522
|
hung,
|
|
32342
32523
|
arguments: cmdArgs,
|
|
32343
32524
|
entries: forAnswer(printed.slice(0, 60)),
|
|
@@ -32348,7 +32529,7 @@ class GodotServer {
|
|
|
32348
32529
|
isError: true
|
|
32349
32530
|
};
|
|
32350
32531
|
}
|
|
32351
|
-
const failed = report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ ...entry, path: suite.path })));
|
|
32532
|
+
const failed = withActualsPrinted(report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ ...entry, path: suite.path }))), Buffer.concat(console2).toString("utf8"));
|
|
32352
32533
|
const orphans = orphansPrinted(said);
|
|
32353
32534
|
const leftBehind = new Map(orphans.suites.map((suite) => [suite.path, suite.orphans]));
|
|
32354
32535
|
const orphansIn = (path) => path === null ? 0 : leftBehind.get(path) ?? 0;
|
|
@@ -32365,6 +32546,7 @@ class GodotServer {
|
|
|
32365
32546
|
passed: !hung && exitCode === 0 && report.failures === 0 && report.errors === 0,
|
|
32366
32547
|
verdict,
|
|
32367
32548
|
exitCode,
|
|
32549
|
+
exitSignal: run.exitSignal ?? undefined,
|
|
32368
32550
|
tests: report.tests,
|
|
32369
32551
|
failures: report.failures,
|
|
32370
32552
|
errors: report.errors,
|
|
@@ -33096,6 +33278,7 @@ class GodotServer {
|
|
|
33096
33278
|
projectPath,
|
|
33097
33279
|
startedAt,
|
|
33098
33280
|
exitCode: null,
|
|
33281
|
+
exitSignal: null,
|
|
33099
33282
|
throughEditor: true,
|
|
33100
33283
|
brokeOn: null,
|
|
33101
33284
|
announcedBefore: alreadyPlaying,
|
|
@@ -33249,6 +33432,7 @@ class GodotServer {
|
|
|
33249
33432
|
projectPath: null,
|
|
33250
33433
|
startedAt: Date.now(),
|
|
33251
33434
|
exitCode: null,
|
|
33435
|
+
exitSignal: null,
|
|
33252
33436
|
throughEditor: false,
|
|
33253
33437
|
brokeOn: null
|
|
33254
33438
|
};
|
|
@@ -33258,13 +33442,11 @@ class GodotServer {
|
|
|
33258
33442
|
child.stderr.on("data", (data) => {
|
|
33259
33443
|
log.append("stderr", data);
|
|
33260
33444
|
});
|
|
33261
|
-
child.on("exit", (code) => {
|
|
33262
|
-
this.logDebug(`Godot process exited with code ${code ?? "none"}`);
|
|
33445
|
+
child.on("exit", (code, signal) => {
|
|
33446
|
+
this.logDebug(`Godot process exited with code ${code ?? "none"}, signal ${signal ?? "none"}`);
|
|
33263
33447
|
log.finish();
|
|
33264
|
-
started.exitCode = code
|
|
33265
|
-
|
|
33266
|
-
recordRunEnded(started.pid, started.exitCode);
|
|
33267
|
-
}
|
|
33448
|
+
started.exitCode = code;
|
|
33449
|
+
started.exitSignal = code === null ? signal : null;
|
|
33268
33450
|
});
|
|
33269
33451
|
child.on("error", (err) => {
|
|
33270
33452
|
console.error("Failed to start Godot process:", err);
|
|
@@ -33298,14 +33480,19 @@ class GodotServer {
|
|
|
33298
33480
|
projectPath,
|
|
33299
33481
|
startedAt,
|
|
33300
33482
|
exitCode: null,
|
|
33483
|
+
exitSignal: null,
|
|
33301
33484
|
throughEditor: false,
|
|
33302
33485
|
brokeOn: null
|
|
33303
33486
|
};
|
|
33304
|
-
child.on("exit", (code) => {
|
|
33305
|
-
this.logDebug(`Godot process exited with code ${code ?? "none"}`);
|
|
33487
|
+
child.on("exit", (code, signal) => {
|
|
33488
|
+
this.logDebug(`Godot process exited with code ${code ?? "none"}, signal ${signal ?? "none"}`);
|
|
33306
33489
|
this.drainTranscript(started);
|
|
33307
33490
|
log.finish();
|
|
33308
|
-
started.exitCode = code
|
|
33491
|
+
started.exitCode = code;
|
|
33492
|
+
started.exitSignal = code === null ? signal : null;
|
|
33493
|
+
if (started.pid !== null) {
|
|
33494
|
+
recordRunEnded(started.pid, { exitCode: started.exitCode, exitSignal: started.exitSignal });
|
|
33495
|
+
}
|
|
33309
33496
|
});
|
|
33310
33497
|
child.on("error", (err) => {
|
|
33311
33498
|
console.error("Failed to start Godot process:", err);
|
|
@@ -33388,6 +33575,7 @@ class GodotServer {
|
|
|
33388
33575
|
projectPath: project,
|
|
33389
33576
|
startedAt: note?.startedAt ?? Date.now(),
|
|
33390
33577
|
exitCode: null,
|
|
33578
|
+
exitSignal: null,
|
|
33391
33579
|
throughEditor: true,
|
|
33392
33580
|
brokeOn: null,
|
|
33393
33581
|
pickedUpPlaying: true
|
|
@@ -33524,6 +33712,7 @@ class GodotServer {
|
|
|
33524
33712
|
projectPath: record.projectPath === "" ? null : record.projectPath,
|
|
33525
33713
|
startedAt: record.startedAt,
|
|
33526
33714
|
exitCode: null,
|
|
33715
|
+
exitSignal: null,
|
|
33527
33716
|
throughEditor: false,
|
|
33528
33717
|
brokeOn: null
|
|
33529
33718
|
};
|
|
@@ -33531,7 +33720,8 @@ class GodotServer {
|
|
|
33531
33720
|
if (!alive(record.pid) || !couldStillBeTheRecordedRun(record)) {
|
|
33532
33721
|
adopted.log.finish();
|
|
33533
33722
|
adopted.exitCode = record.exitCode ?? null;
|
|
33534
|
-
adopted.
|
|
33723
|
+
adopted.exitSignal = record.exitSignal ?? null;
|
|
33724
|
+
adopted.endedUnwatched = !exited(adopted);
|
|
33535
33725
|
}
|
|
33536
33726
|
this.activeProcess = adopted;
|
|
33537
33727
|
return adopted;
|
|
@@ -33630,6 +33820,7 @@ class GodotServer {
|
|
|
33630
33820
|
booted: !hung && boot.exitCode === 0 && errors === 0,
|
|
33631
33821
|
hung,
|
|
33632
33822
|
exitCode: boot.exitCode,
|
|
33823
|
+
exitSignal: boot.exitSignal ?? undefined,
|
|
33633
33824
|
durationMs: Date.now() - boot.startedAt,
|
|
33634
33825
|
frames,
|
|
33635
33826
|
errors,
|
|
@@ -33656,7 +33847,11 @@ class GodotServer {
|
|
|
33656
33847
|
}
|
|
33657
33848
|
async runStillGoing(run, editorSays) {
|
|
33658
33849
|
if (!run.throughEditor) {
|
|
33659
|
-
|
|
33850
|
+
const going = stillRunning(run);
|
|
33851
|
+
if (!going && run.process !== null && !exited(run)) {
|
|
33852
|
+
await untilGone(run.process, [], EXIT_REPORTED_WITHIN_MS);
|
|
33853
|
+
}
|
|
33854
|
+
return going;
|
|
33660
33855
|
}
|
|
33661
33856
|
const announced = this.announcedPidOf(run);
|
|
33662
33857
|
if (announced !== undefined && !alive(announced)) {
|
|
@@ -33764,8 +33959,8 @@ class GodotServer {
|
|
|
33764
33959
|
}
|
|
33765
33960
|
if (typeof run.endedHere === "string") {
|
|
33766
33961
|
notes.push(`This run was ended here, by ${run.endedHere}.`);
|
|
33767
|
-
} else if (!stillRunning(run) && !run.throughEditor && run
|
|
33768
|
-
notes.push(run.exitCode === 0 ? "This run quit on its own, cleanly: exit code 0." : "Nothing here ended this run: it stopped on its own or something outside this server stopped it.");
|
|
33962
|
+
} else if (!stillRunning(run) && !run.throughEditor && exited(run)) {
|
|
33963
|
+
notes.push(run.exitCode === 0 ? "This run quit on its own, cleanly: exit code 0." : run.exitSignal === null ? "Nothing here ended this run: it stopped on its own or something outside this server stopped it." : `Nothing here ended this run: something outside this server sent it ${run.exitSignal}, so it has no exit code.`);
|
|
33769
33964
|
}
|
|
33770
33965
|
if (run.endedUnwatched === true) {
|
|
33771
33966
|
notes.push(endedWithoutACode({
|
|
@@ -33788,6 +33983,7 @@ class GodotServer {
|
|
|
33788
33983
|
return this.jsonTextResponse({
|
|
33789
33984
|
running: going,
|
|
33790
33985
|
exitCode: run.exitCode,
|
|
33986
|
+
exitSignal: run.exitSignal ?? undefined,
|
|
33791
33987
|
through: run.throughEditor ? "editor" : "gdharness",
|
|
33792
33988
|
pid: run.pid ?? announced ?? run.announcedPid ?? null,
|
|
33793
33989
|
errors: run.log.count("error"),
|
|
@@ -33841,10 +34037,11 @@ class GodotServer {
|
|
|
33841
34037
|
},
|
|
33842
34038
|
exitedBeforeStop: !wasRunning,
|
|
33843
34039
|
exitCode: stopped.exitCode,
|
|
34040
|
+
exitSignal: stopped.exitSignal ?? undefined,
|
|
33844
34041
|
errors: stopped.log.count("error"),
|
|
33845
34042
|
warnings: stopped.log.count("warning"),
|
|
33846
34043
|
clean: stopped.log.count("error") === 0,
|
|
33847
|
-
note: !wasRunning ? "This run was over before the stop, so nothing was ended here: editor_output has what it printed and how it ended. Anything that game started for itself, with OS.create_process or otherwise, is a separate process and may still be running." : `${stopped.throughEditor ? endedPid === null ? "The editor was asked to stop the scene it is playing. Its game had not announced a runtime, so no process was named under endedPid, and its exit code stays with the editor." : "The editor was asked to stop the scene it is playing." : "The process named under endedPid was ended."}${gone ? "" : ` It had not exited ${STOP_WAIT_MS / 1000} seconds after being told to, so it may still be going: editor_status says whether it is.`} ${aboutTheChildren(ended, stopped.throughEditor)}`,
|
|
34044
|
+
note: !wasRunning ? "This run was over before the stop, so nothing was ended here: editor_output has what it printed and how it ended. Anything that game started for itself, with OS.create_process or otherwise, is a separate process and may still be running." : `${stopped.throughEditor ? endedPid === null ? "The editor was asked to stop the scene it is playing. Its game had not announced a runtime, so no process was named under endedPid, and its exit code stays with the editor." : "The editor was asked to stop the scene it is playing." : "The process named under endedPid was ended."}${stopped.exitSignal === null ? "" : ` It was ended by ${stopped.exitSignal} and so has no exit code.`}${gone ? "" : ` It had not exited ${STOP_WAIT_MS / 1000} seconds after being told to, so it may still be going: editor_status says whether it is.`} ${aboutTheChildren(ended, stopped.throughEditor)}`,
|
|
33848
34045
|
entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
33849
34046
|
});
|
|
33850
34047
|
}
|
|
@@ -34260,7 +34457,17 @@ class GodotServer {
|
|
|
34260
34457
|
const screenshotDir = expectsScreenshot ? mkdtempSync2(join16(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
|
|
34261
34458
|
const screenshotPath = screenshotDir ? join16(screenshotDir, "capture.png") : null;
|
|
34262
34459
|
try {
|
|
34263
|
-
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
|
|
34460
|
+
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs, !expectsScreenshot);
|
|
34461
|
+
if (!reply.ok && reply.requestId !== undefined) {
|
|
34462
|
+
return this.jsonTextResponse({
|
|
34463
|
+
pending: true,
|
|
34464
|
+
requestId: reply.requestId,
|
|
34465
|
+
command,
|
|
34466
|
+
waitedMs: timeoutMs,
|
|
34467
|
+
note: reply.message,
|
|
34468
|
+
...answeredBy
|
|
34469
|
+
});
|
|
34470
|
+
}
|
|
34264
34471
|
if (!reply.ok) {
|
|
34265
34472
|
return this.createErrorResponse(reply.message);
|
|
34266
34473
|
}
|
|
@@ -34296,7 +34503,36 @@ class GodotServer {
|
|
|
34296
34503
|
discard(screenshotDir);
|
|
34297
34504
|
}
|
|
34298
34505
|
}
|
|
34299
|
-
|
|
34506
|
+
handleLateAnswer(args) {
|
|
34507
|
+
const requestId = readPositiveNumber(args, "requestId") ?? 0;
|
|
34508
|
+
const late = lateAnswer(requestId);
|
|
34509
|
+
if (late === undefined) {
|
|
34510
|
+
return this.createErrorResponse(`No request ${requestId} is waiting to be collected here: an id is kept only from a pending answer this server gave, and only for its fifty most recent.`);
|
|
34511
|
+
}
|
|
34512
|
+
if (late.state === "waiting") {
|
|
34513
|
+
return this.jsonTextResponse({
|
|
34514
|
+
pending: true,
|
|
34515
|
+
requestId,
|
|
34516
|
+
command: late.command,
|
|
34517
|
+
sentAgoMs: Date.now() - late.sentAt,
|
|
34518
|
+
note: "The game has not answered it yet. It is still being listened for, and this answers with the reply once it comes."
|
|
34519
|
+
});
|
|
34520
|
+
}
|
|
34521
|
+
if (late.state === "never") {
|
|
34522
|
+
return this.createErrorResponse(`Request ${requestId} ('${late.command}') got no answer: ${late.why} Read the state back to see whether it ran.`);
|
|
34523
|
+
}
|
|
34524
|
+
const reply = late.reply;
|
|
34525
|
+
if (!reply.ok) {
|
|
34526
|
+
return this.createErrorResponse(`Request ${requestId} ('${late.command}') was answered with a refusal: ${reply.message}`);
|
|
34527
|
+
}
|
|
34528
|
+
const { id: _id, ...payload } = reply.payload;
|
|
34529
|
+
return this.jsonTextResponse({
|
|
34530
|
+
...payload,
|
|
34531
|
+
requestId,
|
|
34532
|
+
answeredAfterMs: late.answeredAt - late.sentAt
|
|
34533
|
+
});
|
|
34534
|
+
}
|
|
34535
|
+
async handleFindRuntimeNodes(args, patience) {
|
|
34300
34536
|
const filters = {};
|
|
34301
34537
|
const className = readNonEmptyString(args, "className");
|
|
34302
34538
|
const script = readNonEmptyString(args, "script");
|
|
@@ -34325,7 +34561,7 @@ class GodotServer {
|
|
|
34325
34561
|
limit: readPositiveNumber(args, "limit") ?? 100,
|
|
34326
34562
|
...property === undefined ? {} : { property },
|
|
34327
34563
|
...includeHidden === undefined ? {} : { include_hidden: includeHidden }
|
|
34328
|
-
});
|
|
34564
|
+
}, patience);
|
|
34329
34565
|
}
|
|
34330
34566
|
async handleRuntimeWait(op, args) {
|
|
34331
34567
|
if (op === "frames") {
|