gdharness 1.2.23 → 1.2.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/cli.js +527 -178
- package/build/godot/addons/gdharness_editor/serialisation.gd +29 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +24 -4
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +5 -0
- package/build/godot/addons/gdharness_runtime/serialisation.gd +29 -0
- package/build/godot/operations/godot_operations.gd +7 -0
- package/build/godot/operations/serialisation.gd +29 -0
- package/build/index.js +538 -193
- package/build/keeper.js +14 -9
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -70,6 +70,7 @@ var init_errors = __esm(() => {
|
|
|
70
70
|
|
|
71
71
|
// src/windows-ask.ts
|
|
72
72
|
import { spawn } from "node:child_process";
|
|
73
|
+
import { tmpdir } from "node:os";
|
|
73
74
|
import process2 from "node:process";
|
|
74
75
|
function holding(child, wanted) {
|
|
75
76
|
if (held === wanted) {
|
|
@@ -94,7 +95,7 @@ function running() {
|
|
|
94
95
|
"Bypass",
|
|
95
96
|
"-EncodedCommand",
|
|
96
97
|
Buffer.from(SCRIPT, "utf16le").toString("base64")
|
|
97
|
-
], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
|
|
98
|
+
], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true, cwd: tmpdir() });
|
|
98
99
|
buffered = "";
|
|
99
100
|
child.stdout.setEncoding("utf8");
|
|
100
101
|
child.stdout.on("data", (chunk) => {
|
|
@@ -651,18 +652,18 @@ function engineFlags(given) {
|
|
|
651
652
|
const setBy = Object.hasOwn(SET_BY_ARGUMENT, flag) ? SET_BY_ARGUMENT[flag] : undefined;
|
|
652
653
|
return {
|
|
653
654
|
ok: false,
|
|
654
|
-
reason: setBy !== undefined ? `${flag} is set by this tool's own argument, ${setBy}, rather than through engineArgs.` : `${flag} is not one of the engine flags engineArgs takes, which are those that change what the engine logs, draws for debugging, checks on the GPU or how it times frames: ${offered}. A value goes as the next entry, as in ["--fixed-fps", "60"].`
|
|
655
|
+
reason: setBy !== undefined ? `${flag} is set by this tool's own argument, ${setBy}, rather than through engineArgs.` : `${flag} is not one of the engine flags engineArgs takes, which are those that change what the engine logs, draws for debugging, checks on the GPU or how it times frames, and Movie Maker's --write-movie: ${offered}. A value goes as the next entry, as in ["--fixed-fps", "60"].`
|
|
655
656
|
};
|
|
656
657
|
}
|
|
657
658
|
if (takes === "none") {
|
|
658
659
|
continue;
|
|
659
660
|
}
|
|
660
661
|
const value = given[at + 1];
|
|
661
|
-
const
|
|
662
|
-
if (!
|
|
662
|
+
const kind = VALUE_KIND[takes];
|
|
663
|
+
if (value === undefined || !kind.pattern.test(value)) {
|
|
663
664
|
return {
|
|
664
665
|
ok: false,
|
|
665
|
-
reason: `${flag} takes ${
|
|
666
|
+
reason: `${flag} takes ${kind.named} as the next entry, and ${value === undefined ? "nothing followed it" : `${JSON.stringify(value)} is not one`}.`
|
|
666
667
|
};
|
|
667
668
|
}
|
|
668
669
|
at += 1;
|
|
@@ -725,7 +726,7 @@ function environmentFor(options, variables = process.env) {
|
|
|
725
726
|
const base = options.savesIn === undefined ? { ...variables } : userDataIn(options.savesIn, variables);
|
|
726
727
|
return { ...base, ...options.env, GDHARNESS_RUNTIME_DIR: options.runtimeDirectory };
|
|
727
728
|
}
|
|
728
|
-
var ENGINE_FLAGS, SET_BY_ARGUMENT, OPENED_BY_A_SERVER = "GDHARNESS_OPENED_BY_A_SERVER", EDITOR_PID_VARIABLE = "GDHARNESS_EDITOR_PID", RESERVED_VARIABLE_PREFIX = "GDHARNESS_", SAVES_NOT_MOVED_NOTE = "On macOS the engine reads user:// off HOME and ignores the variables that move it elsewhere, measured on Godot 4.7.2, so this run wrote its saves where the player keeps theirs. A suite that saves a game has written into the same folder as the copy being played, and one that tidies up after itself may have removed real saves.";
|
|
729
|
+
var ENGINE_FLAGS, VALUE_KIND, SET_BY_ARGUMENT, OPENED_BY_A_SERVER = "GDHARNESS_OPENED_BY_A_SERVER", EDITOR_PID_VARIABLE = "GDHARNESS_EDITOR_PID", RESERVED_VARIABLE_PREFIX = "GDHARNESS_", SAVES_NOT_MOVED_NOTE = "On macOS the engine reads user:// off HOME and ignores the variables that move it elsewhere, measured on Godot 4.7.2, so this run wrote its saves where the player keeps theirs. A suite that saves a game has written into the same folder as the copy being played, and one that tidies up after itself may have removed real saves.";
|
|
729
730
|
var init_launch = __esm(() => {
|
|
730
731
|
ENGINE_FLAGS = Object.freeze({
|
|
731
732
|
"--verbose": "none",
|
|
@@ -746,8 +747,14 @@ var init_launch = __esm(() => {
|
|
|
746
747
|
"--time-scale": "number",
|
|
747
748
|
"--fixed-fps": "number",
|
|
748
749
|
"--max-fps": "number",
|
|
749
|
-
"--resolution": "size"
|
|
750
|
+
"--resolution": "size",
|
|
751
|
+
"--write-movie": "file"
|
|
750
752
|
});
|
|
753
|
+
VALUE_KIND = {
|
|
754
|
+
number: { pattern: /^\d+(?:\.\d+)?$/, named: "a number" },
|
|
755
|
+
size: { pattern: /^\d+x\d+$/, named: "a size such as 1280x720" },
|
|
756
|
+
file: { pattern: /^[^-]/, named: "a file path" }
|
|
757
|
+
};
|
|
751
758
|
SET_BY_ARGUMENT = Object.freeze({
|
|
752
759
|
"--headless": "headless",
|
|
753
760
|
"--audio-driver": "silent",
|
|
@@ -8214,13 +8221,13 @@ var init_alive = () => {};
|
|
|
8214
8221
|
|
|
8215
8222
|
// src/scratch.ts
|
|
8216
8223
|
import { existsSync as existsSync6, mkdtempSync, readdirSync as readdirSync3, rmSync as rmSync4 } from "node:fs";
|
|
8217
|
-
import { tmpdir } from "node:os";
|
|
8224
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
8218
8225
|
import { join as join6 } from "node:path";
|
|
8219
8226
|
import process6 from "node:process";
|
|
8220
8227
|
function scratchDirectory(kind) {
|
|
8221
|
-
return mkdtempSync(join6(
|
|
8228
|
+
return mkdtempSync(join6(tmpdir2(), `gdharness-${kind}-${process6.pid}-`));
|
|
8222
8229
|
}
|
|
8223
|
-
function sweepAbandonedScratch(directory =
|
|
8230
|
+
function sweepAbandonedScratch(directory = tmpdir2()) {
|
|
8224
8231
|
let entries;
|
|
8225
8232
|
try {
|
|
8226
8233
|
entries = readdirSync3(directory);
|
|
@@ -8330,14 +8337,20 @@ function snakeCased(params) {
|
|
|
8330
8337
|
return result;
|
|
8331
8338
|
}
|
|
8332
8339
|
function engineExtras(outcome) {
|
|
8340
|
+
const after = outcome.afterAnswerMessages ?? [];
|
|
8333
8341
|
return {
|
|
8334
8342
|
...outcome.messages.length > 0 ? { engine_messages: outcome.messages } : {},
|
|
8335
8343
|
...outcome.messagesOmitted === undefined ? {} : { engine_messages_omitted: outcome.messagesOmitted },
|
|
8344
|
+
...after.length > 0 ? { engine_messages_after_answer: after, engine_messages_after_answer_note: AFTER_ANSWER_NOTE } : {},
|
|
8345
|
+
...outcome.afterAnswerOmitted === undefined ? {} : { engine_messages_after_answer_omitted: outcome.afterAnswerOmitted },
|
|
8336
8346
|
...outcome.afterAnswer === undefined ? {} : { engine_after_answer: outcome.afterAnswer }
|
|
8337
8347
|
};
|
|
8338
8348
|
}
|
|
8339
8349
|
function problemsIn(log) {
|
|
8340
|
-
return log.everything()
|
|
8350
|
+
return problemsAmong(log.everything());
|
|
8351
|
+
}
|
|
8352
|
+
function problemsAmong(entries) {
|
|
8353
|
+
return entries.flatMap((entry) => {
|
|
8341
8354
|
if (entry.severity !== "info") {
|
|
8342
8355
|
return [entry];
|
|
8343
8356
|
}
|
|
@@ -8466,12 +8479,20 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
8466
8479
|
} finally {
|
|
8467
8480
|
discard(paramsDir);
|
|
8468
8481
|
}
|
|
8469
|
-
const
|
|
8482
|
+
const everything = ran.log.everything();
|
|
8483
|
+
const answered = everything.findIndex((entry) => entry.source === "stderr" && entry.text === ANSWERED);
|
|
8484
|
+
const problems = problemsAmong(answered === -1 ? everything : everything.slice(0, answered));
|
|
8485
|
+
const after = answered === -1 ? [] : problemsAmong(everything.slice(answered + 1));
|
|
8486
|
+
const afterAnswered = {
|
|
8487
|
+
...after.length === 0 ? {} : { afterAnswerMessages: after.slice(-MESSAGES_KEPT) },
|
|
8488
|
+
...after.length > MESSAGES_KEPT ? { afterAnswerOmitted: after.length - MESSAGES_KEPT } : {}
|
|
8489
|
+
};
|
|
8470
8490
|
if (answer.kind === "unreadable") {
|
|
8471
8491
|
return {
|
|
8472
8492
|
ok: false,
|
|
8473
8493
|
message: `${operation} wrote an answer that could not be read (${answer.why})`,
|
|
8474
|
-
...reported2(problems)
|
|
8494
|
+
...reported2(problems),
|
|
8495
|
+
...afterAnswered
|
|
8475
8496
|
};
|
|
8476
8497
|
}
|
|
8477
8498
|
if (answer.kind === "none") {
|
|
@@ -8487,14 +8508,16 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
8487
8508
|
return {
|
|
8488
8509
|
ok: false,
|
|
8489
8510
|
message: `${operation} hit an error in the operations script, so its answer is left out: part of it was ` + `never computed. ${fault.text} (${fault.detail[0]?.slice("at: ".length) ?? "no location given"})`,
|
|
8490
|
-
...reported2(problems)
|
|
8511
|
+
...reported2(problems),
|
|
8512
|
+
...afterAnswered
|
|
8491
8513
|
};
|
|
8492
8514
|
}
|
|
8493
|
-
const afterAnswer = ran.exitCode === 0 && ran.failure === null ? undefined : `The operation finished and wrote this answer, and then the engine did not exit cleanly (${howItEnded(ran)}). Anything it reported on the way out is under engine_messages.`;
|
|
8515
|
+
const afterAnswer = ran.exitCode === 0 && ran.failure === null ? undefined : `The operation finished and wrote this answer, and then the engine did not exit cleanly (${howItEnded(ran)}). Anything it reported on the way out is under ${answered === -1 ? "engine_messages" : "engine_messages_after_answer"}.`;
|
|
8494
8516
|
return {
|
|
8495
8517
|
ok: true,
|
|
8496
8518
|
payload: answer.payload,
|
|
8497
8519
|
...reported2(problems),
|
|
8520
|
+
...afterAnswered,
|
|
8498
8521
|
...afterAnswer === undefined ? {} : { afterAnswer }
|
|
8499
8522
|
};
|
|
8500
8523
|
}
|
|
@@ -8513,7 +8536,7 @@ function answerIn(file) {
|
|
|
8513
8536
|
};
|
|
8514
8537
|
}
|
|
8515
8538
|
}
|
|
8516
|
-
var OWN_ERROR = "[ERROR] ", MESSAGES_KEPT = 200, ERRORS_NAMED = 3;
|
|
8539
|
+
var AFTER_ANSWER_NOTE = "Printed after this operation had written its answer, while the engine ran the project's autoloads and shut down: Godot starts a project's autoloads in every script run and cannot be told not to. An error raised in a project script here is that autoload's own, and none of these changes the answer.", OWN_ERROR = "[ERROR] ", ANSWERED = "[gdharness] answered", MESSAGES_KEPT = 200, ERRORS_NAMED = 3;
|
|
8517
8540
|
var init_headless = __esm(() => {
|
|
8518
8541
|
init_engine_run();
|
|
8519
8542
|
init_reimport();
|
|
@@ -8522,7 +8545,7 @@ var init_headless = __esm(() => {
|
|
|
8522
8545
|
|
|
8523
8546
|
// src/update-check.ts
|
|
8524
8547
|
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
8525
|
-
import { homedir as homedir3, tmpdir as
|
|
8548
|
+
import { homedir as homedir3, tmpdir as tmpdir3 } from "node:os";
|
|
8526
8549
|
import { join as join8 } from "node:path";
|
|
8527
8550
|
function packageUrl(root) {
|
|
8528
8551
|
return `${root.replace(/\/+$/, "")}/gdharness/latest`;
|
|
@@ -8558,7 +8581,7 @@ function cacheFile(environment = process.env) {
|
|
|
8558
8581
|
mkdirSync3(directory, { recursive: true, mode: 448 });
|
|
8559
8582
|
return join8(directory, "update-check.json");
|
|
8560
8583
|
} catch {
|
|
8561
|
-
return join8(
|
|
8584
|
+
return join8(tmpdir3(), "gdharness-update-check.json");
|
|
8562
8585
|
}
|
|
8563
8586
|
}
|
|
8564
8587
|
function readCache(path) {
|
|
@@ -11372,7 +11395,12 @@ var init_tool_definitions = __esm(() => {
|
|
|
11372
11395
|
ignore: {
|
|
11373
11396
|
type: "array",
|
|
11374
11397
|
items: { type: "string" },
|
|
11375
|
-
description: 'Suites or cases to leave out, as "suite_name" or "suite_name:test_name".'
|
|
11398
|
+
description: 'Suites or cases to leave out, as "suite_name" or "suite_name:test_name". A bare test name is read as a suite name and leaves out nothing; an entry that named a case which ran all the same comes back under ignoreNotApplied with the form that would have left it out.'
|
|
11399
|
+
},
|
|
11400
|
+
engineArgs: {
|
|
11401
|
+
type: "array",
|
|
11402
|
+
items: { type: "string" },
|
|
11403
|
+
description: `The engine's own flags for the run, such as ["--verbose"], a value going as the next entry. Only those that change what the engine logs, draws for debugging, checks on the GPU or how it times frames: ${Object.keys(ENGINE_FLAGS).join(", ")}. Any other is refused. With --verbose, engineEntries holds the ordinary lines the run printed as well, which is where the engine names the instances leaked and the resources still in use at exit.`
|
|
11376
11404
|
},
|
|
11377
11405
|
failFast: {
|
|
11378
11406
|
type: "boolean",
|
|
@@ -11799,7 +11827,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
11799
11827
|
{
|
|
11800
11828
|
name: "editor_launch",
|
|
11801
11829
|
title: "Open, restart or close the editor",
|
|
11802
|
-
description: "Opens the Godot editor on a project, or restarts or closes the one already connected. close saves open scenes, quits the editor and waits for its process to go, which is the only way to end a hidden one short of killing it; settingsDropped in its answer says what that save took out of project.godot. open refuses while an editor of the project is already running and has not reached this server, naming its pid, rather than putting a second on the project. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out and answers with the version that came back. An editor a server opened is started again by the server, visible unless hidden: true is passed again, once the old one has gone: it waits 90 seconds for that, or GDHARNESS_EDITOR_RESTART_TIMEOUT_MS, and an old editor still running after it is not replaced, since the new one would find its ports held. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so under restartInterrupted in editor_status rather than waiting for an editor that is not coming, and open finishes it, saying so under finishesRestart. An open is refused when the editor it would start could never reach this server: a project other than the one this server serves, whose editor dials that project's own server; a project without the gdharness editor addon installed and enabled; and a project whose editor a restart is already bringing back.",
|
|
11830
|
+
description: "Opens the Godot editor on a project, or restarts or closes the one already connected. close saves open scenes, quits the editor and waits for its process to go, which is the only way to end a hidden one short of killing it; settingsDropped in its answer says what that save took out of project.godot. An editor this server opened that has not connected yet has nothing of this server's to save, so close stops its process instead and answers beforeConnecting. open answers once the process has started, and the editor dials in after its import; a call needing the editor made before then waits for the one this server opened, for as long as a restart waits, and is refused naming it if it has not arrived. open refuses while an editor of the project is already running and has not reached this server, naming its pid, rather than putting a second on the project. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out and answers with the version that came back. An editor a server opened is started again by the server, visible unless hidden: true is passed again, once the old one has gone: it waits 90 seconds for that, or GDHARNESS_EDITOR_RESTART_TIMEOUT_MS, and an old editor still running after it is not replaced, since the new one would find its ports held. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so under restartInterrupted in editor_status rather than waiting for an editor that is not coming, and open finishes it, saying so under finishesRestart. An open is refused when the editor it would start could never reach this server: a project other than the one this server serves, whose editor dials that project's own server; a project without the gdharness editor addon installed and enabled; and a project whose editor a restart is already bringing back.",
|
|
11803
11831
|
parameters: {
|
|
11804
11832
|
projectPath: { ...PROJECT_PATH, ops: ["open"] },
|
|
11805
11833
|
hidden: {
|
|
@@ -11849,7 +11877,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
11849
11877
|
type: "array",
|
|
11850
11878
|
items: { type: "string" },
|
|
11851
11879
|
ops: ["start", "check"],
|
|
11852
|
-
description: `The engine's own flags, before the separator, such as ["--verbose"] or ["--fixed-fps", "60"], a value going as the next entry. Only those that change what the engine logs, draws for debugging, checks on the GPU or how it times frames: ${Object.keys(ENGINE_FLAGS).join(", ")}. Any other is refused, since the engine's command line can also run scripts, open other projects and write files. A run with any is started by this server, as one with args is. A check given --verbose answers with everything printed rather than only errors and warnings, since what verbose adds, the instances leaked at exit among it, is printed as ordinary lines.`
|
|
11880
|
+
description: `The engine's own flags, before the separator, such as ["--verbose"] or ["--fixed-fps", "60"], a value going as the next entry. Only those that change what the engine logs, draws for debugging, checks on the GPU or how it times frames, and Movie Maker's --write-movie: ${Object.keys(ENGINE_FLAGS).join(", ")}. Any other is refused, since the engine's command line can also run scripts, open other projects and write files. --write-movie takes an absolute path outside the project, in a directory that exists, ending in .avi (one file, video and sound) or .png (one numbered PNG per frame and a .wav beside them), where nothing it would write is there already; it needs a start with headless: false, since the engine crashes writing a movie headless, and it records at the project's editor/movie_writer/fps unless --fixed-fps says otherwise. A run with any is started by this server, as one with args is. A check given --verbose answers with everything printed rather than only errors and warnings, since what verbose adds, the instances leaked at exit among it, is printed as ordinary lines.`
|
|
11853
11881
|
},
|
|
11854
11882
|
headless: { type: "boolean", ops: ["start"], description: "start: force a window or no window." },
|
|
11855
11883
|
visible: {
|
|
@@ -11954,6 +11982,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
11954
11982
|
title: "Game and editor output",
|
|
11955
11983
|
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, with errorsUnread true and no clean, since what it raised reaches nothing here. Answers with the counts and the verdict as well as the entries: clean is true when no error was reported, and warnings, counted beside it, do not make it false. 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. startedAt is when the run began and elapsedMs how long it has been going; for a run that is over, endedAt is when it ended and elapsedMs how long it ran, and both are left out when nobody saw the end, as for a game closed in the editor or a run picked up from a note written before ends were recorded.",
|
|
11956
11984
|
parameters: {
|
|
11985
|
+
projectPath: {
|
|
11986
|
+
type: "string",
|
|
11987
|
+
description: "The project whose output you mean to read. It picks nothing, since this reads the one run or editor this server holds, and a call naming another project than that one's is refused rather than answered with output from a different game."
|
|
11988
|
+
},
|
|
11957
11989
|
severity: {
|
|
11958
11990
|
type: "string",
|
|
11959
11991
|
enum: ["error", "warning", "info"],
|
|
@@ -12005,7 +12037,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12005
12037
|
title: "Editor status",
|
|
12006
12038
|
effect: "read",
|
|
12007
12039
|
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and which games with the runtime addon are reachable, of this project and of every other on the machine. Read connected with mayYetConnect: the editor dials this server and backs off between tries, so for the first half-minute after a server starts false means not reached yet as often as no editor; mayYetConnect true is worth waiting out, false is final, and restartInterrupted, restartUnderway, closedHere and editorsNotYetConnected say why. An editor that has connected and not yet greeted reports nothing about itself, and every other call made in that window waits up to ten seconds for it to settle. playingInEditor null means unanswered rather than nothing playing: processActive and runtimes do not depend on the editor answering. game.run names the run editor_output and editor_run stop answer for, and stays named after it ends. addonIsStale, on the editor and on each game, says it runs addon code other than what this server ships, and staleNote says which half moves: gdharness upgrade in the project, a restart, or a reconnect of this server. A false editor.lspListening or editor.dapListening comes with serversNote, and editor_launch restart binds the port again. editor.scan says whether the editor is scanning, which editor_run waits out before starting a game. A game announced in a protocol this server does not speak is listed under unreadable, during an upgrade that moves the protocol, until the halves match.",
|
|
12008
|
-
answer: "runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. startedAt says when it began, and endedAt when it ended, wherever something saw the end: a run adopted from a note another server wrote can have ended hours before this server started. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote, and editor_launch restart binds it again. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
|
|
12040
|
+
answer: "server names this server: its pid, its version, and lifecycleLog, the file every gdharness server of the user appends a line to when it starts, when its client first names itself, when it replaces another server of its project, and when it ends, with why it ended (the client closing its end as stdin:end, a signal by name, superseded with the successor's pid, an uncaught exception or rejection with its stack, or the exit code) and the calls it was answering then. The parent's command line follows on its own line a few seconds after the start. A server killed without warning writes no ended line, and on Windows every kill is that, so a started line with no ended line after it is a server something killed. " + "runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. startedAt says when it began, and endedAt when it ended, wherever something saw the end: a run adopted from a note another server wrote can have ended hours before this server started. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote, and editor_launch restart binds it again. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
|
|
12009
12041
|
parameters: {},
|
|
12010
12042
|
requires: []
|
|
12011
12043
|
},
|
|
@@ -12208,7 +12240,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12208
12240
|
value: {
|
|
12209
12241
|
ops: ["set"],
|
|
12210
12242
|
blank: true,
|
|
12211
|
-
description: `set: the value, fitted to the property's type. "" writes an empty string. A property typed as or holding an object takes a path naming one the game holds, the way call's args do, and holds that instance, or null, which the text null also means, to empty it. One declared as Variant or untyped takes anything, as the engine does: a value of the kind it holds is fitted to it, an int staying an int, and any other is stored as sent. A typed list or map, such as Array[int] or Dictionary[String, int], takes a JSON list or object with each element fitted to its element type, and an element that cannot become one is refused by its index or key, as is an object of another script than the one the list holds, whether or not that script has a class_name.`
|
|
12243
|
+
description: `set: the value, fitted to the property's type. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}, which is the form a read answers with. "" writes an empty string. A property typed as or holding an object takes a path naming one the game holds, the way call's args do, and holds that instance, or null, which the text null also means, to empty it. One declared as Variant or untyped takes anything, as the engine does: a value of the kind it holds is fitted to it, an int staying an int, and any other is stored as sent. A typed list or map, such as Array[int] or Dictionary[String, int], takes a JSON list or object with each element fitted to its element type, and an element that cannot become one is refused by its index or key, as is an object of another script than the one the list holds, whether or not that script has a class_name.`
|
|
12212
12244
|
},
|
|
12213
12245
|
method: {
|
|
12214
12246
|
type: "string",
|
|
@@ -12218,7 +12250,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12218
12250
|
args: {
|
|
12219
12251
|
type: "array",
|
|
12220
12252
|
ops: ["call"],
|
|
12221
|
-
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, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. 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. The method itself takes its arguments here only: brackets on it holding arguments are refused.`
|
|
12253
|
+
description: `call: the arguments, fitted to the method's parameter types. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}. 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, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. 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. The method itself takes its arguments here only: brackets on it holding arguments are refused.`
|
|
12222
12254
|
},
|
|
12223
12255
|
properties: {
|
|
12224
12256
|
type: ["boolean", "array"],
|
|
@@ -12312,7 +12344,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12312
12344
|
name: "runtime_input",
|
|
12313
12345
|
title: "Send input to the game",
|
|
12314
12346
|
description: "Input to the running game: a whole click on a Control or a 3D node named by path or on a control named by the words on it, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it. On gdharness's own desktop, where a windowed run goes, Windows refuses to say where the pointer is: every pointer answer there carries pointer_on_hidden_desktop, and the first of the run pointer_note saying why it matters: get_mouse_position() on the root window, and the mouse positions derived from it, read a meaningless position, while what reads the event itself (the control under it, hover, tooltips, input handlers) gets the injected position.",
|
|
12315
|
-
answer: 'click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens.',
|
|
12347
|
+
answer: 'click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens. action, key and mouse_click: the event as it was sent, with whole saying whether the press was let go of again, and for a key the keycode, physical_keycode and unicode it carried. mouse_motion: the position and the relative movement sent. None of these is aimed at anything, so none says what the event reached: click on a nodePath or says answers what was under the pointer.',
|
|
12316
12348
|
parameters: {
|
|
12317
12349
|
projectPath: RUNNING_PROJECT_PATH,
|
|
12318
12350
|
pid: RUNNING_PID,
|
|
@@ -34322,7 +34354,7 @@ var init_stdio = __esm(() => {
|
|
|
34322
34354
|
// src/runtime-client.ts
|
|
34323
34355
|
import { existsSync as existsSync13, readdirSync as readdirSync8, readFileSync as readFileSync12, statSync as statSync6, unlinkSync } from "node:fs";
|
|
34324
34356
|
import { createConnection } from "node:net";
|
|
34325
|
-
import { tmpdir as
|
|
34357
|
+
import { tmpdir as tmpdir4 } from "node:os";
|
|
34326
34358
|
import { dirname as dirname8, join as join14, resolve as resolve4 } from "node:path";
|
|
34327
34359
|
import { setTimeout as delay } from "node:timers/promises";
|
|
34328
34360
|
function runtimeDirectory(variables = process.env) {
|
|
@@ -34331,7 +34363,7 @@ function runtimeDirectory(variables = process.env) {
|
|
|
34331
34363
|
return explicit;
|
|
34332
34364
|
}
|
|
34333
34365
|
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
34334
|
-
return join14(perUser ??
|
|
34366
|
+
return join14(perUser ?? tmpdir4(), "gdharness");
|
|
34335
34367
|
}
|
|
34336
34368
|
function runtimeDirectories(variables = process.env) {
|
|
34337
34369
|
const candidates = [runtimeDirectory(variables)];
|
|
@@ -39747,6 +39779,23 @@ function count(attributes, name) {
|
|
|
39747
39779
|
const value = Number(attributes[name] ?? "0");
|
|
39748
39780
|
return Number.isFinite(value) ? value : 0;
|
|
39749
39781
|
}
|
|
39782
|
+
function ignoredYetRan(report, ignored) {
|
|
39783
|
+
const found = [];
|
|
39784
|
+
for (const entry of ignored) {
|
|
39785
|
+
const bare = entry.replace("res://", "");
|
|
39786
|
+
const at = bare.lastIndexOf(":");
|
|
39787
|
+
const suiteNamed = at === -1 ? null : bare.slice(0, at);
|
|
39788
|
+
const testNamed = at === -1 ? bare : bare.slice(at + 1);
|
|
39789
|
+
for (const suite of report.suites) {
|
|
39790
|
+
const suiteMatches = suiteNamed === null || suite.name === suiteNamed || (suite.path ?? "").includes(suiteNamed);
|
|
39791
|
+
const ran = suiteMatches ? suite.cases.find((one) => one.name === testNamed) : undefined;
|
|
39792
|
+
if (ran !== undefined) {
|
|
39793
|
+
found.push({ entry, ran: `${suite.name}:${ran.name}` });
|
|
39794
|
+
}
|
|
39795
|
+
}
|
|
39796
|
+
}
|
|
39797
|
+
return found;
|
|
39798
|
+
}
|
|
39750
39799
|
function caseOf(suite, element) {
|
|
39751
39800
|
const of = (kind) => element.children.filter((child) => child.name === kind).map((child) => {
|
|
39752
39801
|
const detail = child.text.trim();
|
|
@@ -40098,11 +40147,58 @@ function askTheKeeperToStop(address, withinMs) {
|
|
|
40098
40147
|
}
|
|
40099
40148
|
var init_keeper_channel = () => {};
|
|
40100
40149
|
|
|
40150
|
+
// src/lifecycle-log.ts
|
|
40151
|
+
import { appendFileSync, mkdirSync as mkdirSync11, renameSync, statSync as statSync8 } from "node:fs";
|
|
40152
|
+
import { dirname as dirname14, join as join21 } from "node:path";
|
|
40153
|
+
import process10 from "node:process";
|
|
40154
|
+
function lifecycleLogPath() {
|
|
40155
|
+
return join21(runtimeDirectory(), "servers.log");
|
|
40156
|
+
}
|
|
40157
|
+
function writeLifecycle(event, fields = {}) {
|
|
40158
|
+
const path = lifecycleLogPath();
|
|
40159
|
+
try {
|
|
40160
|
+
mkdirSync11(dirname14(path), { recursive: true });
|
|
40161
|
+
if ((statSync8(path, { throwIfNoEntry: false })?.size ?? 0) > LIFECYCLE_LOG_LIMIT) {
|
|
40162
|
+
try {
|
|
40163
|
+
renameSync(path, `${path}.1`);
|
|
40164
|
+
} catch {}
|
|
40165
|
+
}
|
|
40166
|
+
const line = {
|
|
40167
|
+
at: new Date().toISOString(),
|
|
40168
|
+
pid: process10.pid,
|
|
40169
|
+
ppid: process10.ppid,
|
|
40170
|
+
version: SERVER_VERSION,
|
|
40171
|
+
event
|
|
40172
|
+
};
|
|
40173
|
+
for (const [name, value] of Object.entries(fields)) {
|
|
40174
|
+
if (!Object.hasOwn(line, name)) {
|
|
40175
|
+
line[name] = value;
|
|
40176
|
+
}
|
|
40177
|
+
}
|
|
40178
|
+
appendFileSync(path, `${JSON.stringify(line)}
|
|
40179
|
+
`, "utf8");
|
|
40180
|
+
} catch {
|
|
40181
|
+
console.error(`[SERVER] Could not write the lifecycle log at ${path}.`);
|
|
40182
|
+
}
|
|
40183
|
+
}
|
|
40184
|
+
function callsInFlight(calls, now = Date.now()) {
|
|
40185
|
+
return [...calls].map((call) => ({
|
|
40186
|
+
tool: call.tool,
|
|
40187
|
+
...call.op === "" ? {} : { op: call.op },
|
|
40188
|
+
runningMs: now - call.startedAt
|
|
40189
|
+
}));
|
|
40190
|
+
}
|
|
40191
|
+
var LIFECYCLE_LOG_LIMIT = 1e6;
|
|
40192
|
+
var init_lifecycle_log = __esm(() => {
|
|
40193
|
+
init_runtime_client();
|
|
40194
|
+
init_server_version();
|
|
40195
|
+
});
|
|
40196
|
+
|
|
40101
40197
|
// src/lsp_client.ts
|
|
40102
40198
|
import { existsSync as existsSync17, realpathSync as realpathSync4 } from "node:fs";
|
|
40103
40199
|
import { readFile, realpath } from "node:fs/promises";
|
|
40104
40200
|
import { createConnection as createConnection3 } from "node:net";
|
|
40105
|
-
import { dirname as
|
|
40201
|
+
import { dirname as dirname15, join as join22, resolve as resolve5 } from "node:path";
|
|
40106
40202
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
40107
40203
|
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
|
|
40108
40204
|
function initializeTimeoutMs() {
|
|
@@ -40264,7 +40360,7 @@ class GodotLSPClient {
|
|
|
40264
40360
|
if (this.initialized) {
|
|
40265
40361
|
return;
|
|
40266
40362
|
}
|
|
40267
|
-
const rootPath = this.rootPath ??
|
|
40363
|
+
const rootPath = this.rootPath ?? dirname15(resolve5(filePath));
|
|
40268
40364
|
await this.initialize(rootPath);
|
|
40269
40365
|
}
|
|
40270
40366
|
async sendRequest(method, params, timeoutMs = this.timeouts.requestMs) {
|
|
@@ -40765,7 +40861,7 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
|
|
|
40765
40861
|
} catch {
|
|
40766
40862
|
throw new ArgumentRefusal(`Project path does not exist: ${requestedProjectPath}`);
|
|
40767
40863
|
}
|
|
40768
|
-
if (!existsSync17(
|
|
40864
|
+
if (!existsSync17(join22(projectPath, "project.godot"))) {
|
|
40769
40865
|
throw new ArgumentRefusal(`Not a Godot project: ${projectPath}. Point projectPath at the directory holding project.godot.`);
|
|
40770
40866
|
}
|
|
40771
40867
|
const contained = resolveWithinProject(projectPath, scriptPathValue);
|
|
@@ -41205,8 +41301,8 @@ var init_prompts = __esm(() => {
|
|
|
41205
41301
|
});
|
|
41206
41302
|
|
|
41207
41303
|
// src/rename.ts
|
|
41208
|
-
import { existsSync as existsSync18, mkdirSync as
|
|
41209
|
-
import { dirname as
|
|
41304
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync12, readFileSync as readFileSync19, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync13 } from "node:fs";
|
|
41305
|
+
import { dirname as dirname16, posix as posix2 } from "node:path";
|
|
41210
41306
|
|
|
41211
41307
|
class Script {
|
|
41212
41308
|
path;
|
|
@@ -41918,15 +42014,15 @@ function applyRewrites(rewrites) {
|
|
|
41918
42014
|
const done = [];
|
|
41919
42015
|
try {
|
|
41920
42016
|
for (const rewrite of rewrites) {
|
|
41921
|
-
|
|
42017
|
+
mkdirSync12(dirname16(rewrite.writeTo), { recursive: true });
|
|
41922
42018
|
const staging = `${rewrite.writeTo}.gdharness-rename`;
|
|
41923
42019
|
writeFileSync13(staging, rewrite.after, "utf8");
|
|
41924
|
-
|
|
42020
|
+
renameSync2(staging, rewrite.writeTo);
|
|
41925
42021
|
let movedUid = false;
|
|
41926
42022
|
if (rewrite.writeTo !== rewrite.absolute) {
|
|
41927
42023
|
unlinkSync2(rewrite.absolute);
|
|
41928
42024
|
if (existsSync18(`${rewrite.absolute}.uid`)) {
|
|
41929
|
-
|
|
42025
|
+
renameSync2(`${rewrite.absolute}.uid`, `${rewrite.writeTo}.uid`);
|
|
41930
42026
|
movedUid = true;
|
|
41931
42027
|
}
|
|
41932
42028
|
}
|
|
@@ -41939,7 +42035,7 @@ function applyRewrites(rewrites) {
|
|
|
41939
42035
|
if (rewrite.writeTo !== rewrite.absolute) {
|
|
41940
42036
|
unlinkSync2(rewrite.writeTo);
|
|
41941
42037
|
if (movedUid) {
|
|
41942
|
-
|
|
42038
|
+
renameSync2(`${rewrite.writeTo}.uid`, `${rewrite.absolute}.uid`);
|
|
41943
42039
|
}
|
|
41944
42040
|
}
|
|
41945
42041
|
} catch {}
|
|
@@ -41971,8 +42067,8 @@ var init_rename = __esm(() => {
|
|
|
41971
42067
|
});
|
|
41972
42068
|
|
|
41973
42069
|
// src/rename-symbol.ts
|
|
41974
|
-
import { existsSync as existsSync19, mkdirSync as
|
|
41975
|
-
import { basename as basename5, dirname as
|
|
42070
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync13, readdirSync as readdirSync9, realpathSync as realpathSync5, rmSync as rmSync10, writeFileSync as writeFileSync14 } from "node:fs";
|
|
42071
|
+
import { basename as basename5, dirname as dirname17, join as join23, relative as relative4, resolve as resolve6 } from "node:path";
|
|
41976
42072
|
function resourcePathOf(projectPath, absolute) {
|
|
41977
42073
|
const inside = relative4(onDiskSpelling(projectPath), onDiskSpelling(absolute));
|
|
41978
42074
|
if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
|
|
@@ -41985,9 +42081,9 @@ function onDiskSpelling(path) {
|
|
|
41985
42081
|
let at = resolve6(path);
|
|
41986
42082
|
for (;; ) {
|
|
41987
42083
|
try {
|
|
41988
|
-
return
|
|
42084
|
+
return join23(realpathSync5.native(at), ...missing.reverse());
|
|
41989
42085
|
} catch {
|
|
41990
|
-
const parent =
|
|
42086
|
+
const parent = dirname17(at);
|
|
41991
42087
|
if (parent === at) {
|
|
41992
42088
|
return resolve6(path);
|
|
41993
42089
|
}
|
|
@@ -42413,15 +42509,15 @@ ${mention.kind}`;
|
|
|
42413
42509
|
};
|
|
42414
42510
|
}
|
|
42415
42511
|
function writeLeftAloneDetail(projectPath, symbol, detail) {
|
|
42416
|
-
const directory =
|
|
42417
|
-
|
|
42512
|
+
const directory = join23(projectPath, ".godot", "gdharness-renames");
|
|
42513
|
+
mkdirSync13(directory, { recursive: true });
|
|
42418
42514
|
detailWrites += 1;
|
|
42419
|
-
const path =
|
|
42515
|
+
const path = join23(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
|
|
42420
42516
|
writeFileSync14(path, `${JSON.stringify(detail, null, 2)}
|
|
42421
42517
|
`, "utf8");
|
|
42422
42518
|
const kept = readdirSync9(directory).filter((name) => name.endsWith(".json")).sort().reverse();
|
|
42423
42519
|
for (const old of kept.slice(10)) {
|
|
42424
|
-
rmSync10(
|
|
42520
|
+
rmSync10(join23(directory, old), { force: true });
|
|
42425
42521
|
}
|
|
42426
42522
|
return path;
|
|
42427
42523
|
}
|
|
@@ -42461,15 +42557,15 @@ var init_rename_symbol = __esm(() => {
|
|
|
42461
42557
|
});
|
|
42462
42558
|
|
|
42463
42559
|
// src/restart-note.ts
|
|
42464
|
-
import { mkdirSync as
|
|
42465
|
-
import { dirname as
|
|
42560
|
+
import { mkdirSync as mkdirSync14, readFileSync as readFileSync20, rmSync as rmSync11, writeFileSync as writeFileSync15 } from "node:fs";
|
|
42561
|
+
import { dirname as dirname18, join as join24 } from "node:path";
|
|
42466
42562
|
function restartNotePath(projectPath) {
|
|
42467
|
-
return
|
|
42563
|
+
return join24(projectPath, ".godot", "gdharness-restart.json");
|
|
42468
42564
|
}
|
|
42469
42565
|
function noteRestartBegun(note) {
|
|
42470
42566
|
const path = restartNotePath(note.projectPath);
|
|
42471
42567
|
try {
|
|
42472
|
-
|
|
42568
|
+
mkdirSync14(dirname18(path), { recursive: true });
|
|
42473
42569
|
writeFileSync15(path, `${JSON.stringify(note, null, 2)}
|
|
42474
42570
|
`, "utf8");
|
|
42475
42571
|
} catch (error) {
|
|
@@ -42503,7 +42599,7 @@ var init_restart_note = __esm(() => {
|
|
|
42503
42599
|
});
|
|
42504
42600
|
|
|
42505
42601
|
// src/whole-file.ts
|
|
42506
|
-
import { renameSync as
|
|
42602
|
+
import { renameSync as renameSync3, rmSync as rmSync12, writeFileSync as writeFileSync16 } from "node:fs";
|
|
42507
42603
|
function writeWhole(path, text) {
|
|
42508
42604
|
const partial = `${path}.${process.pid}.partial`;
|
|
42509
42605
|
try {
|
|
@@ -42514,7 +42610,7 @@ function writeWhole(path, text) {
|
|
|
42514
42610
|
const until = Date.now() + HELD_FILE_MS;
|
|
42515
42611
|
for (;; ) {
|
|
42516
42612
|
try {
|
|
42517
|
-
|
|
42613
|
+
renameSync3(partial, path);
|
|
42518
42614
|
return true;
|
|
42519
42615
|
} catch (error) {
|
|
42520
42616
|
const code = error.code ?? "";
|
|
@@ -42536,20 +42632,20 @@ var init_whole_file = __esm(() => {
|
|
|
42536
42632
|
// src/run-record.ts
|
|
42537
42633
|
import { execFile as execFile4 } from "node:child_process";
|
|
42538
42634
|
import { createHash as createHash4 } from "node:crypto";
|
|
42539
|
-
import { mkdirSync as
|
|
42540
|
-
import { basename as basename6, dirname as
|
|
42635
|
+
import { mkdirSync as mkdirSync15, openSync as openSync2, readdirSync as readdirSync10, readFileSync as readFileSync21, realpathSync as realpathSync6, rmSync as rmSync13, statSync as statSync9 } from "node:fs";
|
|
42636
|
+
import { basename as basename6, dirname as dirname19, join as join25, resolve as resolve7 } from "node:path";
|
|
42541
42637
|
import { promisify as promisify4 } from "node:util";
|
|
42542
42638
|
function runsDirectory() {
|
|
42543
|
-
return
|
|
42639
|
+
return join25(runtimeDirectory(), "runs");
|
|
42544
42640
|
}
|
|
42545
42641
|
function realAsFarAsItExists(path) {
|
|
42546
42642
|
const missing = [];
|
|
42547
42643
|
let at = path;
|
|
42548
42644
|
for (;; ) {
|
|
42549
42645
|
try {
|
|
42550
|
-
return
|
|
42646
|
+
return join25(realpathSync6.native(at), ...missing);
|
|
42551
42647
|
} catch {
|
|
42552
|
-
const parent =
|
|
42648
|
+
const parent = dirname19(at);
|
|
42553
42649
|
if (parent === at) {
|
|
42554
42650
|
return path;
|
|
42555
42651
|
}
|
|
@@ -42564,14 +42660,14 @@ function projectKey(projectPath) {
|
|
|
42564
42660
|
return createHash4("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
|
|
42565
42661
|
}
|
|
42566
42662
|
function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
42567
|
-
return
|
|
42663
|
+
return join25(directory, "runs", `note-${projectKey(projectPath)}.json`);
|
|
42568
42664
|
}
|
|
42569
42665
|
function latestRunPath(servedBy, directory = runtimeDirectory()) {
|
|
42570
|
-
return
|
|
42666
|
+
return join25(directory, "runs", `latest-${projectKey(servedBy)}.json`);
|
|
42571
42667
|
}
|
|
42572
42668
|
function writeLatestRun(servedBy, latest) {
|
|
42573
42669
|
try {
|
|
42574
|
-
|
|
42670
|
+
mkdirSync15(runsDirectory(), { recursive: true });
|
|
42575
42671
|
writeWhole(latestRunPath(servedBy), JSON.stringify(latest, null, 2));
|
|
42576
42672
|
} catch {}
|
|
42577
42673
|
}
|
|
@@ -42591,21 +42687,21 @@ function readLatestRun(servedBy, directory = runtimeDirectory()) {
|
|
|
42591
42687
|
}
|
|
42592
42688
|
}
|
|
42593
42689
|
function openTranscript(startedAt) {
|
|
42594
|
-
|
|
42595
|
-
const path =
|
|
42690
|
+
mkdirSync15(runsDirectory(), { recursive: true });
|
|
42691
|
+
const path = join25(runsDirectory(), `run-${startedAt}.log`);
|
|
42596
42692
|
return { path, fd: openSync2(path, "a") };
|
|
42597
42693
|
}
|
|
42598
42694
|
function profilePath(startedAt) {
|
|
42599
|
-
return
|
|
42695
|
+
return join25(runsDirectory(), `run-${startedAt}.profile.json`);
|
|
42600
42696
|
}
|
|
42601
42697
|
function noteIsAbout(note, playing) {
|
|
42602
42698
|
return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
|
|
42603
42699
|
}
|
|
42604
42700
|
function editorNotePath(projectPath, directory = runtimeDirectory()) {
|
|
42605
|
-
return
|
|
42701
|
+
return join25(directory, "runs", `editor-${projectKey(projectPath)}.json`);
|
|
42606
42702
|
}
|
|
42607
42703
|
function writeEditorRunNote(note) {
|
|
42608
|
-
|
|
42704
|
+
mkdirSync15(runsDirectory(), { recursive: true });
|
|
42609
42705
|
const path = editorNotePath(note.projectPath);
|
|
42610
42706
|
if (!writeWhole(path, JSON.stringify(note, null, 2))) {
|
|
42611
42707
|
throw new Error(`the editor run note could not be written at ${path}`);
|
|
@@ -42613,7 +42709,7 @@ function writeEditorRunNote(note) {
|
|
|
42613
42709
|
}
|
|
42614
42710
|
function readEditorRunNote(projectPath) {
|
|
42615
42711
|
for (const directory of runtimeDirectories()) {
|
|
42616
|
-
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(
|
|
42712
|
+
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join25(directory, "runs", "editor-run.json"));
|
|
42617
42713
|
if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
|
|
42618
42714
|
return note;
|
|
42619
42715
|
}
|
|
@@ -42647,7 +42743,7 @@ function readRunRecord(projectPath) {
|
|
|
42647
42743
|
if (own !== null) {
|
|
42648
42744
|
return own;
|
|
42649
42745
|
}
|
|
42650
|
-
const shared = recordAt(
|
|
42746
|
+
const shared = recordAt(join25(directory, "runs", SHARED_NOTE));
|
|
42651
42747
|
if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
|
|
42652
42748
|
return shared;
|
|
42653
42749
|
}
|
|
@@ -42659,13 +42755,13 @@ function everyRunRecord() {
|
|
|
42659
42755
|
for (const directory of runtimeDirectories()) {
|
|
42660
42756
|
let entries;
|
|
42661
42757
|
try {
|
|
42662
|
-
entries = readdirSync10(
|
|
42758
|
+
entries = readdirSync10(join25(directory, "runs"));
|
|
42663
42759
|
} catch {
|
|
42664
42760
|
continue;
|
|
42665
42761
|
}
|
|
42666
42762
|
for (const entry of entries) {
|
|
42667
42763
|
if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
|
|
42668
|
-
const record = recordAt(
|
|
42764
|
+
const record = recordAt(join25(directory, "runs", entry));
|
|
42669
42765
|
if (record !== null) {
|
|
42670
42766
|
found.push(record);
|
|
42671
42767
|
}
|
|
@@ -42875,7 +42971,7 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
|
|
|
42875
42971
|
}
|
|
42876
42972
|
function clearRunRecord(projectPath) {
|
|
42877
42973
|
for (const directory of runtimeDirectories()) {
|
|
42878
|
-
const shared =
|
|
42974
|
+
const shared = join25(directory, "runs", SHARED_NOTE);
|
|
42879
42975
|
const sharedRecord = recordAt(shared);
|
|
42880
42976
|
const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
|
|
42881
42977
|
for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
|
|
@@ -42887,7 +42983,7 @@ function clearRunRecord(projectPath) {
|
|
|
42887
42983
|
}
|
|
42888
42984
|
function ageMs(path, now) {
|
|
42889
42985
|
try {
|
|
42890
|
-
return now -
|
|
42986
|
+
return now - statSync9(path).mtimeMs;
|
|
42891
42987
|
} catch {
|
|
42892
42988
|
return 0;
|
|
42893
42989
|
}
|
|
@@ -42909,7 +43005,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
42909
43005
|
if (!transcript && !entry.endsWith(".partial")) {
|
|
42910
43006
|
continue;
|
|
42911
43007
|
}
|
|
42912
|
-
const path =
|
|
43008
|
+
const path = join25(directory, entry);
|
|
42913
43009
|
if (kept.has(path) || ageMs(path, now) < keepMs) {
|
|
42914
43010
|
continue;
|
|
42915
43011
|
}
|
|
@@ -42930,7 +43026,7 @@ var init_run_record = __esm(() => {
|
|
|
42930
43026
|
});
|
|
42931
43027
|
|
|
42932
43028
|
// src/script-origins.ts
|
|
42933
|
-
import { join as
|
|
43029
|
+
import { join as join26 } from "node:path";
|
|
42934
43030
|
function unresolvedClassIn(message) {
|
|
42935
43031
|
return UNRESOLVED.exec(message)?.[1] ?? null;
|
|
42936
43032
|
}
|
|
@@ -42983,7 +43079,7 @@ function checkWithEngine(godotPath, projectPath) {
|
|
|
42983
43079
|
const ran = await runEngine(godotPath, [
|
|
42984
43080
|
"--headless",
|
|
42985
43081
|
"--log-file",
|
|
42986
|
-
|
|
43082
|
+
join26(logDir, "engine.log"),
|
|
42987
43083
|
"--path",
|
|
42988
43084
|
projectPath,
|
|
42989
43085
|
"--check-only",
|
|
@@ -43212,21 +43308,21 @@ var init_server_instructions = __esm(() => {
|
|
|
43212
43308
|
import { execFile as execFile5, spawn as spawn4 } from "node:child_process";
|
|
43213
43309
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
43214
43310
|
import {
|
|
43215
|
-
appendFileSync,
|
|
43311
|
+
appendFileSync as appendFileSync2,
|
|
43216
43312
|
closeSync as closeSync2,
|
|
43217
43313
|
constants,
|
|
43218
43314
|
copyFileSync,
|
|
43219
43315
|
existsSync as existsSync20,
|
|
43220
43316
|
fstatSync,
|
|
43221
|
-
mkdirSync as
|
|
43317
|
+
mkdirSync as mkdirSync16,
|
|
43222
43318
|
openSync as openSync3,
|
|
43223
43319
|
readdirSync as readdirSync11,
|
|
43224
43320
|
readFileSync as readFileSync23,
|
|
43225
43321
|
readSync as readSync2,
|
|
43226
43322
|
rmdirSync as rmdirSync2,
|
|
43227
|
-
statSync as
|
|
43323
|
+
statSync as statSync10
|
|
43228
43324
|
} from "node:fs";
|
|
43229
|
-
import { basename as basename7, dirname as
|
|
43325
|
+
import { basename as basename7, dirname as dirname20, extname as extname3, isAbsolute as isAbsolute3, join as join27, normalize as normalize2, relative as relative5 } from "node:path";
|
|
43230
43326
|
import { setTimeout as delay4 } from "node:timers/promises";
|
|
43231
43327
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
43232
43328
|
import { promisify as promisify5 } from "node:util";
|
|
@@ -43244,29 +43340,62 @@ function restartUnderway(note, now = Date.now()) {
|
|
|
43244
43340
|
return alive(note.byPid) && now - Date.parse(note.quitAt) < editorRestartTimeoutMs() + 30000;
|
|
43245
43341
|
}
|
|
43246
43342
|
function captureDestinationRefusal(outputPath, projects) {
|
|
43247
|
-
|
|
43248
|
-
|
|
43343
|
+
return writtenFileRefusal(outputPath, {
|
|
43344
|
+
named: "outputPath",
|
|
43345
|
+
what: "a capture",
|
|
43346
|
+
extensions: [".png"],
|
|
43347
|
+
projects,
|
|
43348
|
+
writes: (where) => [where]
|
|
43349
|
+
});
|
|
43350
|
+
}
|
|
43351
|
+
function writtenFileRefusal(path, rule) {
|
|
43352
|
+
const { named, what } = rule;
|
|
43353
|
+
if (/^(res|user):\/\//.test(path)) {
|
|
43354
|
+
return `${named} ${path} is a Godot path; ${what} is saved outside the project, at an absolute path on this machine.`;
|
|
43249
43355
|
}
|
|
43250
|
-
if (!isAbsolute3(
|
|
43251
|
-
return
|
|
43356
|
+
if (!isAbsolute3(path)) {
|
|
43357
|
+
return `${named} ${path} is not absolute; name the whole path, so where it is written does not depend on this server's directory.`;
|
|
43252
43358
|
}
|
|
43253
|
-
if (extname3(
|
|
43254
|
-
return
|
|
43359
|
+
if (!rule.extensions.includes(extname3(path).toLowerCase())) {
|
|
43360
|
+
return `${named} ${path} does not end in ${rule.extensions.join(" or ")}, which is what ${what} is.`;
|
|
43255
43361
|
}
|
|
43256
|
-
const directory = realPathOr(
|
|
43362
|
+
const directory = realPathOr(dirname20(path));
|
|
43257
43363
|
if (!existsSync20(directory)) {
|
|
43258
|
-
return
|
|
43364
|
+
return `${named}'s directory ${dirname20(path)} does not exist; ${what} creates no directories.`;
|
|
43259
43365
|
}
|
|
43260
|
-
const where =
|
|
43261
|
-
const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
43366
|
+
const where = join27(directory, basename7(path));
|
|
43367
|
+
const inside = rule.projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
43262
43368
|
if (inside !== undefined) {
|
|
43263
|
-
return
|
|
43369
|
+
return `${named} ${path} is inside the project at ${inside}; ${what} is saved outside it, so it never becomes part of the game.`;
|
|
43264
43370
|
}
|
|
43265
|
-
|
|
43266
|
-
|
|
43371
|
+
const taken = rule.writes(where).find((file) => existsSync20(file));
|
|
43372
|
+
if (taken !== undefined) {
|
|
43373
|
+
return taken === where ? `${named} ${path} already exists; ${what} is not written over a file. Name a new one.` : `${named} ${path} would write ${taken}, which already exists; ${what} is not written over a file. Name a new one.`;
|
|
43267
43374
|
}
|
|
43268
43375
|
return null;
|
|
43269
43376
|
}
|
|
43377
|
+
function movieRefusal(engineArgs, projects, headless) {
|
|
43378
|
+
const at = engineArgs.indexOf("--write-movie");
|
|
43379
|
+
if (at === -1) {
|
|
43380
|
+
return null;
|
|
43381
|
+
}
|
|
43382
|
+
if (headless) {
|
|
43383
|
+
return "--write-movie needs a run that draws, and this one is headless: Movie Maker reads every frame back from the renderer, and the engine crashed on the first frame when asked headless. Start the run with headless: false; it is drawn on a desktop of its own and does not show.";
|
|
43384
|
+
}
|
|
43385
|
+
return writtenFileRefusal(engineArgs[at + 1] ?? "", {
|
|
43386
|
+
named: "--write-movie",
|
|
43387
|
+
what: "a movie",
|
|
43388
|
+
extensions: [".avi", ".png"],
|
|
43389
|
+
projects,
|
|
43390
|
+
writes: (where) => {
|
|
43391
|
+
if (extname3(where).toLowerCase() !== ".png") {
|
|
43392
|
+
return [where];
|
|
43393
|
+
}
|
|
43394
|
+
const base = where.slice(0, -".png".length);
|
|
43395
|
+
return [`${base}00000000.png`, `${base}.wav`];
|
|
43396
|
+
}
|
|
43397
|
+
});
|
|
43398
|
+
}
|
|
43270
43399
|
function captureDetail(payload) {
|
|
43271
43400
|
const parts = [];
|
|
43272
43401
|
const region = payload["region"];
|
|
@@ -43755,6 +43884,23 @@ function engineFlagsNote(args) {
|
|
|
43755
43884
|
const named = among.join(", ");
|
|
43756
43885
|
return `${named} ${among.length === 1 ? "is a flag" : "are flags"} the engine reads, and args goes to the game after --, where the engine does not read ${among.length === 1 ? "it" : "them"}. engineArgs hands the engine its own flags; ${among.length === 1 ? "leave it" : "leave them"} in args only for a game that reads ${among.length === 1 ? "that flag" : "those flags"} itself.`;
|
|
43757
43886
|
}
|
|
43887
|
+
function editorAddonMissing(projectPath) {
|
|
43888
|
+
if (!existsSync20(join27(projectPath, "project.godot"))) {
|
|
43889
|
+
return null;
|
|
43890
|
+
}
|
|
43891
|
+
const report = inspectProject(projectPath);
|
|
43892
|
+
const installed = report.addons.some((addon) => addon.name === "gdharness_editor" && addon.installed);
|
|
43893
|
+
if (installed && report.pluginsEnabled.includes("gdharness_editor")) {
|
|
43894
|
+
return null;
|
|
43895
|
+
}
|
|
43896
|
+
return {
|
|
43897
|
+
why: `the gdharness editor addon is not ${installed ? "enabled" : "installed"} in ${projectPath}`,
|
|
43898
|
+
remedies: installed ? [
|
|
43899
|
+
"gdharness setup in the project enables it",
|
|
43900
|
+
"Or enable it under Project Settings, Plugins, in an editor opened by hand"
|
|
43901
|
+
] : ["gdharness setup in the project installs and enables it"]
|
|
43902
|
+
};
|
|
43903
|
+
}
|
|
43758
43904
|
function opElsewhere(tool, requested) {
|
|
43759
43905
|
const ops = Object.hasOwn(OPS_ELSEWHERE, tool) ? OPS_ELSEWHERE[tool] : undefined;
|
|
43760
43906
|
const where = ops !== undefined && typeof requested === "string" && Object.hasOwn(ops, requested) ? ops[requested] : undefined;
|
|
@@ -43975,7 +44121,7 @@ function profileFrameFunctions(projectPath) {
|
|
|
43975
44121
|
let projectGodot = null;
|
|
43976
44122
|
if (projectPath !== null) {
|
|
43977
44123
|
try {
|
|
43978
|
-
projectGodot = readFileSync23(
|
|
44124
|
+
projectGodot = readFileSync23(join27(projectPath, "project.godot"), "utf8");
|
|
43979
44125
|
} catch {}
|
|
43980
44126
|
}
|
|
43981
44127
|
return frameFunctionsFor(queuedMessagesOf(projectGodot));
|
|
@@ -44149,7 +44295,7 @@ function foldedEntriesAnswer(log, floor = "warning") {
|
|
|
44149
44295
|
}
|
|
44150
44296
|
function gdUnitFailsOnScriptErrors(projectPath) {
|
|
44151
44297
|
try {
|
|
44152
|
-
return settingKeys(readFileSync23(
|
|
44298
|
+
return settingKeys(readFileSync23(join27(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
|
|
44153
44299
|
} catch {
|
|
44154
44300
|
return true;
|
|
44155
44301
|
}
|
|
@@ -44164,7 +44310,7 @@ function uncaughtNote(uncaught) {
|
|
|
44164
44310
|
}
|
|
44165
44311
|
function elsewhereIn(projectPath, asked) {
|
|
44166
44312
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
44167
|
-
if (wanted === "" || existsSync20(
|
|
44313
|
+
if (wanted === "" || existsSync20(join27(projectPath, wanted))) {
|
|
44168
44314
|
return "";
|
|
44169
44315
|
}
|
|
44170
44316
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
@@ -44254,13 +44400,13 @@ function scriptsToDiagnose(projectPath, given) {
|
|
|
44254
44400
|
}
|
|
44255
44401
|
function isDirectory(path) {
|
|
44256
44402
|
try {
|
|
44257
|
-
return
|
|
44403
|
+
return statSync10(path).isDirectory();
|
|
44258
44404
|
} catch {
|
|
44259
44405
|
return false;
|
|
44260
44406
|
}
|
|
44261
44407
|
}
|
|
44262
44408
|
function scriptsUnder(directory, prefix) {
|
|
44263
|
-
if (existsSync20(
|
|
44409
|
+
if (existsSync20(join27(directory, ".gdignore"))) {
|
|
44264
44410
|
return [];
|
|
44265
44411
|
}
|
|
44266
44412
|
const found = [];
|
|
@@ -44270,7 +44416,7 @@ function scriptsUnder(directory, prefix) {
|
|
|
44270
44416
|
}
|
|
44271
44417
|
const path = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
44272
44418
|
if (entry.isDirectory()) {
|
|
44273
|
-
found.push(...scriptsUnder(
|
|
44419
|
+
found.push(...scriptsUnder(join27(directory, entry.name), path));
|
|
44274
44420
|
} else if (entry.isFile() && entry.name.endsWith(".gd")) {
|
|
44275
44421
|
found.push(path);
|
|
44276
44422
|
}
|
|
@@ -44286,7 +44432,7 @@ function runtimeAddonAnnouncesEditors(projectPath) {
|
|
|
44286
44432
|
return false;
|
|
44287
44433
|
}
|
|
44288
44434
|
try {
|
|
44289
|
-
return readFileSync23(
|
|
44435
|
+
return readFileSync23(join27(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
|
|
44290
44436
|
} catch {
|
|
44291
44437
|
return false;
|
|
44292
44438
|
}
|
|
@@ -44311,7 +44457,7 @@ function needsItsOwnProcess(asked) {
|
|
|
44311
44457
|
class GodotServer {
|
|
44312
44458
|
stdio = null;
|
|
44313
44459
|
locator = new GodotLocator;
|
|
44314
|
-
operationsScript =
|
|
44460
|
+
operationsScript = join27(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
44315
44461
|
godotBridge;
|
|
44316
44462
|
tools = buildToolDefinitions();
|
|
44317
44463
|
activeProcess = null;
|
|
@@ -44335,6 +44481,11 @@ class GodotServer {
|
|
|
44335
44481
|
launchedFrom = null;
|
|
44336
44482
|
launchedDropped = null;
|
|
44337
44483
|
closedHere = null;
|
|
44484
|
+
inFlight = new Map;
|
|
44485
|
+
callsTaken = 0;
|
|
44486
|
+
clientNoted = null;
|
|
44487
|
+
parentCommand = null;
|
|
44488
|
+
endNoted = false;
|
|
44338
44489
|
ownProject;
|
|
44339
44490
|
announcedAt = null;
|
|
44340
44491
|
handingOverFrom = null;
|
|
@@ -44371,6 +44522,7 @@ class GodotServer {
|
|
|
44371
44522
|
return { content: [{ type: "text", text: answerJson(payload) }] };
|
|
44372
44523
|
}
|
|
44373
44524
|
async run() {
|
|
44525
|
+
this.noteTheStart();
|
|
44374
44526
|
const located = await this.locator.find();
|
|
44375
44527
|
if (!located.ok) {
|
|
44376
44528
|
console.error(`[SERVER] ${GodotLocator.refusal(located).message} Until that is fixed every tool that runs the engine will say so.`);
|
|
@@ -44487,6 +44639,7 @@ class GodotServer {
|
|
|
44487
44639
|
return;
|
|
44488
44640
|
}
|
|
44489
44641
|
console.error(`[SERVER] pid ${successor} now serves ${this.ownProject}; this server is finished and is stopping.`);
|
|
44642
|
+
this.noteTheEnd("superseded", { successor });
|
|
44490
44643
|
this.handleShutdown(`superseded by ${successor}`, 0);
|
|
44491
44644
|
}, SUCCESSOR_CHECK_MS);
|
|
44492
44645
|
watch.unref();
|
|
@@ -44558,10 +44711,67 @@ class GodotServer {
|
|
|
44558
44711
|
}
|
|
44559
44712
|
await this.stdio?.close();
|
|
44560
44713
|
}
|
|
44714
|
+
noteTheStart() {
|
|
44715
|
+
const cwd = process.cwd();
|
|
44716
|
+
const mcpJson = join27(cwd, ".mcp.json");
|
|
44717
|
+
const announced = this.ownProject === null ? null : readAnnouncement(announcementPath(this.ownProject));
|
|
44718
|
+
const predecessor = announced !== null && announced.pid !== process.pid && alive(announced.pid) ? announced.pid : null;
|
|
44719
|
+
writeLifecycle("started", {
|
|
44720
|
+
argv: process.argv.slice(1),
|
|
44721
|
+
cwd,
|
|
44722
|
+
project: this.ownProject,
|
|
44723
|
+
mcpJson: existsSync20(mcpJson) ? mcpJson : null
|
|
44724
|
+
});
|
|
44725
|
+
if (predecessor !== null) {
|
|
44726
|
+
writeLifecycle("replaced", { project: this.ownProject, predecessor });
|
|
44727
|
+
}
|
|
44728
|
+
const ask = setTimeout(() => {
|
|
44729
|
+
processTree().then((tree) => {
|
|
44730
|
+
this.parentCommand = tree?.get(process.ppid)?.command ?? null;
|
|
44731
|
+
writeLifecycle("parent", {
|
|
44732
|
+
command: this.parentCommand,
|
|
44733
|
+
...tree === undefined ? { why: whyTheProcessTreeFailed() } : {}
|
|
44734
|
+
});
|
|
44735
|
+
});
|
|
44736
|
+
}, PARENT_ASKED_AFTER_MS);
|
|
44737
|
+
ask.unref();
|
|
44738
|
+
}
|
|
44739
|
+
noteTheEnd(cause, more = {}) {
|
|
44740
|
+
if (this.endNoted) {
|
|
44741
|
+
return;
|
|
44742
|
+
}
|
|
44743
|
+
this.endNoted = true;
|
|
44744
|
+
writeLifecycle("ended", {
|
|
44745
|
+
cause,
|
|
44746
|
+
project: this.ownProject,
|
|
44747
|
+
...this.parentCommand === null ? {} : { parentCommand: this.parentCommand },
|
|
44748
|
+
...this.clientNoted === null ? {} : { client: this.clientNoted },
|
|
44749
|
+
inFlight: callsInFlight(this.inFlight.values()),
|
|
44750
|
+
...more
|
|
44751
|
+
});
|
|
44752
|
+
}
|
|
44753
|
+
noteTheClient(client) {
|
|
44754
|
+
if (client === undefined) {
|
|
44755
|
+
return;
|
|
44756
|
+
}
|
|
44757
|
+
const said = `${client.name} ${client.version}`;
|
|
44758
|
+
if (said === this.clientNoted) {
|
|
44759
|
+
return;
|
|
44760
|
+
}
|
|
44761
|
+
this.clientNoted = said;
|
|
44762
|
+
writeLifecycle("client", { client: { name: client.name, version: client.version } });
|
|
44763
|
+
}
|
|
44561
44764
|
setupShutdownHandlers() {
|
|
44562
44765
|
const requestShutdown = (source, exitCode) => {
|
|
44563
44766
|
this.handleShutdown(source, exitCode);
|
|
44564
44767
|
};
|
|
44768
|
+
process.on("uncaughtException", (error, origin) => {
|
|
44769
|
+
this.noteTheEnd(origin === "unhandledRejection" ? "unhandledRejection" : "uncaughtException", {
|
|
44770
|
+
error: error instanceof Error ? error.stack ?? error.message : String(error)
|
|
44771
|
+
});
|
|
44772
|
+
console.error(error);
|
|
44773
|
+
process.exit(1);
|
|
44774
|
+
});
|
|
44565
44775
|
process.once("SIGINT", () => {
|
|
44566
44776
|
requestShutdown("SIGINT", 0);
|
|
44567
44777
|
});
|
|
@@ -44574,7 +44784,8 @@ class GodotServer {
|
|
|
44574
44784
|
process.once("beforeExit", (code) => {
|
|
44575
44785
|
requestShutdown(`beforeExit:${code}`);
|
|
44576
44786
|
});
|
|
44577
|
-
process.once("exit", () => {
|
|
44787
|
+
process.once("exit", (code) => {
|
|
44788
|
+
this.noteTheEnd(`exit:${code}`);
|
|
44578
44789
|
this.forceCleanupOnExit();
|
|
44579
44790
|
});
|
|
44580
44791
|
}
|
|
@@ -44583,6 +44794,7 @@ class GodotServer {
|
|
|
44583
44794
|
return;
|
|
44584
44795
|
}
|
|
44585
44796
|
this.shutdownInitiated = true;
|
|
44797
|
+
this.noteTheEnd(source);
|
|
44586
44798
|
this.logDebug(`Shutting down server via ${source}`);
|
|
44587
44799
|
try {
|
|
44588
44800
|
await this.cleanup();
|
|
@@ -44622,9 +44834,23 @@ class GodotServer {
|
|
|
44622
44834
|
return mcp;
|
|
44623
44835
|
}
|
|
44624
44836
|
setupToolHandlers(protocol) {
|
|
44625
|
-
|
|
44837
|
+
const clientOf = (envelope) => {
|
|
44838
|
+
const named = envelope?.["io.modelcontextprotocol/clientInfo"];
|
|
44839
|
+
if (typeof named === "object" && named !== null) {
|
|
44840
|
+
const { name, version } = named;
|
|
44841
|
+
if (typeof name === "string" && typeof version === "string") {
|
|
44842
|
+
return { name, version };
|
|
44843
|
+
}
|
|
44844
|
+
}
|
|
44845
|
+
return protocol.getClientVersion();
|
|
44846
|
+
};
|
|
44847
|
+
protocol.setRequestHandler("tools/list", (_request, ctx) => {
|
|
44848
|
+
this.noteTheClient(clientOf(ctx.mcpReq.envelope));
|
|
44849
|
+
return { tools: this.tools };
|
|
44850
|
+
});
|
|
44626
44851
|
protocol.setRequestHandler("tools/call", async (request, ctx) => {
|
|
44627
44852
|
this.logDebug(`Handling tool request: ${request.params.name}`);
|
|
44853
|
+
this.noteTheClient(clientOf(ctx.mcpReq.envelope));
|
|
44628
44854
|
const spec = toolSpec(request.params.name);
|
|
44629
44855
|
if (!spec) {
|
|
44630
44856
|
throw new ProtocolError(ProtocolErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}. Tools: ${TOOL_SPECS.map((tool) => tool.name).join(", ")}`);
|
|
@@ -44635,7 +44861,7 @@ class GodotServer {
|
|
|
44635
44861
|
return checked.response;
|
|
44636
44862
|
}
|
|
44637
44863
|
const named = args["projectPath"];
|
|
44638
|
-
if (typeof named === "string" && existsSync20(
|
|
44864
|
+
if (typeof named === "string" && existsSync20(join27(named, "project.godot"))) {
|
|
44639
44865
|
this.lastProjectPath = named;
|
|
44640
44866
|
}
|
|
44641
44867
|
this.updates.refresh();
|
|
@@ -44648,8 +44874,14 @@ class GodotServer {
|
|
|
44648
44874
|
this.logDebug(`Progress for ${spec.name} was not sent: ${errorMessage(error)}`);
|
|
44649
44875
|
});
|
|
44650
44876
|
};
|
|
44651
|
-
const
|
|
44652
|
-
|
|
44877
|
+
const call = this.callsTaken++;
|
|
44878
|
+
this.inFlight.set(call, { tool: spec.name, op: checked.op ?? "", startedAt: Date.now() });
|
|
44879
|
+
try {
|
|
44880
|
+
const answer = await withCallSignal(ctx.mcpReq.signal, () => this.answered(spec.name, checked.op ?? "", args), progress);
|
|
44881
|
+
return this.withFeedbackNotice(this.withUpdateNotice(answer));
|
|
44882
|
+
} finally {
|
|
44883
|
+
this.inFlight.delete(call);
|
|
44884
|
+
}
|
|
44653
44885
|
});
|
|
44654
44886
|
}
|
|
44655
44887
|
async answered(tool, op, args) {
|
|
@@ -44868,10 +45100,12 @@ class GodotServer {
|
|
|
44868
45100
|
]);
|
|
44869
45101
|
}
|
|
44870
45102
|
if (!this.godotBridge.isConnected()) {
|
|
44871
|
-
|
|
44872
|
-
|
|
44873
|
-
'Open the project with editor_launch, or pass from: "disk" to read the file instead'
|
|
45103
|
+
const refused = await this.noEditorYet(answerable, [
|
|
45104
|
+
`${tool} ${op} was asked of the editor; from: "disk" reads the file instead`
|
|
44874
45105
|
]);
|
|
45106
|
+
if (refused !== null) {
|
|
45107
|
+
return refused;
|
|
45108
|
+
}
|
|
44875
45109
|
}
|
|
44876
45110
|
const { op: _dropped, ...asks } = answerable;
|
|
44877
45111
|
const contained = this.containProjectFiles(asks);
|
|
@@ -44956,11 +45190,11 @@ class GodotServer {
|
|
|
44956
45190
|
case "script_info":
|
|
44957
45191
|
switch (op) {
|
|
44958
45192
|
case "symbols":
|
|
44959
|
-
return await this.
|
|
45193
|
+
return await this.askScriptInfo("lsp_get_symbols", "Symbols", args);
|
|
44960
45194
|
case "completion":
|
|
44961
|
-
return await this.
|
|
45195
|
+
return await this.askScriptInfo("lsp_get_completions", "Completion", args);
|
|
44962
45196
|
default:
|
|
44963
|
-
return await this.
|
|
45197
|
+
return await this.askScriptInfo("lsp_get_hover", "Hover", args);
|
|
44964
45198
|
}
|
|
44965
45199
|
case "script_diagnostics":
|
|
44966
45200
|
return await this.handleScriptDiagnostics(args);
|
|
@@ -45137,7 +45371,7 @@ class GodotServer {
|
|
|
45137
45371
|
if (path === undefined) {
|
|
45138
45372
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
45139
45373
|
}
|
|
45140
|
-
const file =
|
|
45374
|
+
const file = join27(path, "project.godot");
|
|
45141
45375
|
if (!existsSync20(file)) {
|
|
45142
45376
|
return {
|
|
45143
45377
|
ok: false,
|
|
@@ -45824,18 +46058,18 @@ class GodotServer {
|
|
|
45824
46058
|
if (!engine.ok) {
|
|
45825
46059
|
return engine.response;
|
|
45826
46060
|
}
|
|
45827
|
-
const folder =
|
|
46061
|
+
const folder = dirname20(output.absolutePath);
|
|
45828
46062
|
try {
|
|
45829
|
-
|
|
46063
|
+
mkdirSync16(folder, { recursive: true });
|
|
45830
46064
|
} catch (error) {
|
|
45831
|
-
return this.createErrorResponse(`Cannot create ${
|
|
46065
|
+
return this.createErrorResponse(`Cannot create ${dirname20(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
45832
46066
|
}
|
|
45833
46067
|
const debug = readBoolean(args, "debug") ?? false;
|
|
45834
46068
|
const exportLogs = scratchDirectory("export");
|
|
45835
46069
|
const exportArgs = [
|
|
45836
46070
|
"--headless",
|
|
45837
46071
|
"--log-file",
|
|
45838
|
-
|
|
46072
|
+
join27(exportLogs, "engine.log"),
|
|
45839
46073
|
"--path",
|
|
45840
46074
|
project.value.path,
|
|
45841
46075
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -45843,7 +46077,7 @@ class GodotServer {
|
|
|
45843
46077
|
output.absolutePath
|
|
45844
46078
|
];
|
|
45845
46079
|
this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
|
|
45846
|
-
const writtenAt = () => existsSync20(output.absolutePath) ?
|
|
46080
|
+
const writtenAt = () => existsSync20(output.absolutePath) ? statSync10(output.absolutePath).mtimeMs : null;
|
|
45847
46081
|
const before = writtenAt();
|
|
45848
46082
|
let ending;
|
|
45849
46083
|
try {
|
|
@@ -45899,10 +46133,18 @@ class GodotServer {
|
|
|
45899
46133
|
if (!paths.ok) {
|
|
45900
46134
|
return this.createErrorResponse(paths.reason, PATH_SOLUTIONS);
|
|
45901
46135
|
}
|
|
45902
|
-
const
|
|
46136
|
+
const flags = this.engineArguments(args);
|
|
46137
|
+
if (!flags.ok) {
|
|
46138
|
+
return flags.response;
|
|
46139
|
+
}
|
|
46140
|
+
const movie = movieRefusal(flags.value, [project.value.path], true);
|
|
46141
|
+
if (movie !== null) {
|
|
46142
|
+
return this.createErrorResponse(movie);
|
|
46143
|
+
}
|
|
46144
|
+
const missing = paths.value.filter((path) => !existsSync20(join27(project.value.path, path.slice("res://".length))));
|
|
45903
46145
|
const asked = paths.value.filter((path) => !missing.includes(path));
|
|
45904
46146
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
45905
|
-
const noRunner = !existsSync20(
|
|
46147
|
+
const noRunner = !existsSync20(join27(project.value.path, runner));
|
|
45906
46148
|
if (asked.length === 0) {
|
|
45907
46149
|
const verdict = `nothing at ${missing.join(" and ")}`;
|
|
45908
46150
|
const andNoRunner = noRunner ? `, and gdUnit4 is not installed in this project: no ${runner}` : "";
|
|
@@ -45946,6 +46188,7 @@ class GodotServer {
|
|
|
45946
46188
|
"--headless",
|
|
45947
46189
|
"--path",
|
|
45948
46190
|
project.value.path,
|
|
46191
|
+
...flags.value,
|
|
45949
46192
|
"-s",
|
|
45950
46193
|
`res://${runner}`,
|
|
45951
46194
|
"--ignoreHeadlessMode",
|
|
@@ -45992,24 +46235,28 @@ class GodotServer {
|
|
|
45992
46235
|
await untilGone([run.process.pid]);
|
|
45993
46236
|
}
|
|
45994
46237
|
await run.settled;
|
|
45995
|
-
const reportsDir =
|
|
46238
|
+
const reportsDir = join27(project.value.path, ".godot", ...ours.split("/"));
|
|
45996
46239
|
let report = null;
|
|
45997
46240
|
let reportProblem = null;
|
|
45998
46241
|
try {
|
|
45999
46242
|
const written = existsSync20(reportsDir) ? readdirSync11(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
46000
46243
|
const newest = written.at(-1);
|
|
46001
46244
|
if (newest !== undefined) {
|
|
46002
|
-
report = parseJUnit(readFileSync23(
|
|
46245
|
+
report = parseJUnit(readFileSync23(join27(reportsDir, newest, "results.xml"), "utf8"));
|
|
46003
46246
|
}
|
|
46004
46247
|
} catch (error) {
|
|
46005
46248
|
reportProblem = errorMessage(error);
|
|
46006
46249
|
} finally {
|
|
46007
46250
|
discard(reportsDir, userData);
|
|
46008
46251
|
try {
|
|
46009
|
-
rmdirSync2(
|
|
46252
|
+
rmdirSync2(dirname20(reportsDir));
|
|
46010
46253
|
} catch {}
|
|
46011
46254
|
}
|
|
46012
|
-
const engineEntries = foldedRepeats(run.log.select({
|
|
46255
|
+
const engineEntries = foldedRepeats(run.log.select({
|
|
46256
|
+
severity: flags.value.includes("--verbose") ? "info" : "warning",
|
|
46257
|
+
sinceLastCall: false,
|
|
46258
|
+
limit: Number.POSITIVE_INFINITY
|
|
46259
|
+
}).entries.map(aboveTheRunner));
|
|
46013
46260
|
const engineEntriesShown = engineEntries.slice(-MOST_ENGINE_ENTRIES);
|
|
46014
46261
|
const verdicts = {
|
|
46015
46262
|
0: "passed",
|
|
@@ -46120,6 +46367,7 @@ class GodotServer {
|
|
|
46120
46367
|
const notRun = report.suites.reduce((sum, suite) => sum + Math.max(0, suite.discovered - suite.tests), 0);
|
|
46121
46368
|
const uncaughtFolded = foldedRepeats(uncaught.map(aboveTheRunner));
|
|
46122
46369
|
const listedAsUncaught = new Set(uncaughtFolded.map((entry) => entry.index));
|
|
46370
|
+
const notApplied = ignoredYetRan(report, ignored);
|
|
46123
46371
|
return this.jsonTextResponse({
|
|
46124
46372
|
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && uncaught.length === 0 && missing.length === 0,
|
|
46125
46373
|
verdict,
|
|
@@ -46138,11 +46386,13 @@ class GodotServer {
|
|
|
46138
46386
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
46139
46387
|
orphans: orphans.total > 0 ? orphans.total : undefined,
|
|
46140
46388
|
notRun: notRun > 0 ? notRun : undefined,
|
|
46389
|
+
ignoreNotApplied: notApplied.length > 0 ? notApplied : undefined,
|
|
46141
46390
|
...printedLines,
|
|
46142
46391
|
savesNote,
|
|
46143
46392
|
...scanned,
|
|
46144
46393
|
note: [
|
|
46145
46394
|
missingNote,
|
|
46395
|
+
notApplied.length > 0 ? "An ignore entry named a case that ran all the same: gdUnit4 leaves a case out only as suite_name:test_name, and reads an entry with no colon as a suite name or a piece of a suite path. ignoreNotApplied gives the form for each case that ran." : undefined,
|
|
46146
46396
|
uncaughtNote(uncaught),
|
|
46147
46397
|
ranAs.startsWith("warnings") && warnings.length === 0 ? "gdUnit4 exits 101 for orphan nodes when nothing failed, and this run printed no count of them: orphan reporting may be off in the project settings." : notRunNote(notRun, readBoolean(args, "failFast") === true)
|
|
46148
46398
|
].filter((part) => part !== undefined).join(" ") || undefined,
|
|
@@ -46231,31 +46481,9 @@ class GodotServer {
|
|
|
46231
46481
|
async diagnosticsOf(args) {
|
|
46232
46482
|
const answer = await this.handleLSP("lsp_get_diagnostics", args);
|
|
46233
46483
|
const payload = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
|
|
46234
|
-
|
|
46235
|
-
|
|
46236
|
-
|
|
46237
|
-
return { refusal: this.createErrorResponse(reason), perScript: true };
|
|
46238
|
-
}
|
|
46239
|
-
if (payload["stillReading"] === true) {
|
|
46240
|
-
return { refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`), perScript: false };
|
|
46241
|
-
}
|
|
46242
|
-
if (typeof payload["serves"] === "string") {
|
|
46243
|
-
return {
|
|
46244
|
-
refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
|
|
46245
|
-
"editor_status names the editor on the bridge and the port it says it serves",
|
|
46246
|
-
"editor_launch opens this project's own editor, on a language server port of its own",
|
|
46247
|
-
"GDHARNESS_LSP_PORT points this server at the right one"
|
|
46248
|
-
]),
|
|
46249
|
-
perScript: false
|
|
46250
|
-
};
|
|
46251
|
-
}
|
|
46252
|
-
return {
|
|
46253
|
-
refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
|
|
46254
|
-
`Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
|
|
46255
|
-
"GDHARNESS_LSP_PORT points this server at another one"
|
|
46256
|
-
]),
|
|
46257
|
-
perScript: payload["aboutThisFile"] === true
|
|
46258
|
-
};
|
|
46484
|
+
const refused = this.languageServerRefusal(payload, "Diagnostics");
|
|
46485
|
+
if (refused !== null) {
|
|
46486
|
+
return refused;
|
|
46259
46487
|
}
|
|
46260
46488
|
const diagnostics = readArray(payload, "diagnostics") ?? [];
|
|
46261
46489
|
const errors = diagnostics.filter((entry) => asParams(entry)["severity"] === "error").length;
|
|
@@ -46362,6 +46590,40 @@ class GodotServer {
|
|
|
46362
46590
|
async handleLSP(toolName, args) {
|
|
46363
46591
|
return handleLSPTool(await this.lsp(), toolName, args);
|
|
46364
46592
|
}
|
|
46593
|
+
async askScriptInfo(toolName, asked, args) {
|
|
46594
|
+
const answer = await this.handleLSP(toolName, args);
|
|
46595
|
+
const refused = this.languageServerRefusal(asParams(JSON.parse(answer.content[0]?.text ?? "{}")), asked);
|
|
46596
|
+
return refused === null ? answer : refused.refusal;
|
|
46597
|
+
}
|
|
46598
|
+
languageServerRefusal(payload, asked) {
|
|
46599
|
+
if (payload["error"] === undefined) {
|
|
46600
|
+
return null;
|
|
46601
|
+
}
|
|
46602
|
+
const reason = typeof payload["error"] === "string" ? payload["error"] : JSON.stringify(payload["error"]);
|
|
46603
|
+
if (payload["refusedArguments"] === true) {
|
|
46604
|
+
return { refusal: this.createErrorResponse(reason), perScript: true };
|
|
46605
|
+
}
|
|
46606
|
+
if (payload["stillReading"] === true) {
|
|
46607
|
+
return { refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`), perScript: false };
|
|
46608
|
+
}
|
|
46609
|
+
if (typeof payload["serves"] === "string") {
|
|
46610
|
+
return {
|
|
46611
|
+
refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`, [
|
|
46612
|
+
"editor_status names the editor on the bridge and the port it says it serves",
|
|
46613
|
+
"editor_launch opens this project's own editor, on a language server port of its own",
|
|
46614
|
+
"GDHARNESS_LSP_PORT points this server at the right one"
|
|
46615
|
+
]),
|
|
46616
|
+
perScript: false
|
|
46617
|
+
};
|
|
46618
|
+
}
|
|
46619
|
+
return {
|
|
46620
|
+
refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`, [
|
|
46621
|
+
`Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
|
|
46622
|
+
"GDHARNESS_LSP_PORT points this server at another one"
|
|
46623
|
+
]),
|
|
46624
|
+
perScript: payload["aboutThisFile"] === true
|
|
46625
|
+
};
|
|
46626
|
+
}
|
|
46365
46627
|
async lsp() {
|
|
46366
46628
|
const port = this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT);
|
|
46367
46629
|
if (this.lspClient !== null && this.lspClient.port !== port) {
|
|
@@ -46462,7 +46724,7 @@ class GodotServer {
|
|
|
46462
46724
|
return;
|
|
46463
46725
|
}
|
|
46464
46726
|
try {
|
|
46465
|
-
|
|
46727
|
+
appendFileSync2(run.transcript, line.endsWith(`
|
|
46466
46728
|
`) ? line : `${line}
|
|
46467
46729
|
`);
|
|
46468
46730
|
} catch (error) {
|
|
@@ -46673,7 +46935,8 @@ class GodotServer {
|
|
|
46673
46935
|
serverSpeaks: RUNTIME_PROTOCOL,
|
|
46674
46936
|
behind: game.protocol > RUNTIME_PROTOCOL ? "server" : "addon"
|
|
46675
46937
|
}))
|
|
46676
|
-
}
|
|
46938
|
+
},
|
|
46939
|
+
server: { pid: process.pid, version: SERVER_VERSION, lifecycleLog: lifecycleLogPath() }
|
|
46677
46940
|
});
|
|
46678
46941
|
}
|
|
46679
46942
|
async editorServersListening() {
|
|
@@ -46705,7 +46968,7 @@ class GodotServer {
|
|
|
46705
46968
|
return null;
|
|
46706
46969
|
}
|
|
46707
46970
|
try {
|
|
46708
|
-
return settingKeys(readFileSync23(
|
|
46971
|
+
return settingKeys(readFileSync23(join27(projectPath, "project.godot"), "utf8"));
|
|
46709
46972
|
} catch {
|
|
46710
46973
|
return null;
|
|
46711
46974
|
}
|
|
@@ -46761,6 +47024,10 @@ class GodotServer {
|
|
|
46761
47024
|
async handleCloseEditor() {
|
|
46762
47025
|
const before = this.godotBridge.getStatus();
|
|
46763
47026
|
if (!before.connected) {
|
|
47027
|
+
const launched = this.launchedEditor;
|
|
47028
|
+
if (launched !== null && await this.launchedEditorIsUp() && !this.godotBridge.isConnected()) {
|
|
47029
|
+
return await this.stopTheLaunchedEditor(launched);
|
|
47030
|
+
}
|
|
46764
47031
|
return this.createErrorResponse("No editor is connected, so there is nothing to close.", [
|
|
46765
47032
|
"editor_status says whether the bridge is up and what has reached it"
|
|
46766
47033
|
]);
|
|
@@ -46794,6 +47061,31 @@ class GodotServer {
|
|
|
46794
47061
|
tookMs: Date.now() - began
|
|
46795
47062
|
});
|
|
46796
47063
|
}
|
|
47064
|
+
async stopTheLaunchedEditor(launched) {
|
|
47065
|
+
const began = Date.now();
|
|
47066
|
+
try {
|
|
47067
|
+
process.kill(launched.pid);
|
|
47068
|
+
} catch (error) {
|
|
47069
|
+
if (alive(launched.pid)) {
|
|
47070
|
+
return this.createErrorResponse(`The editor this server opened, pid ${launched.pid}, has not connected, and stopping it failed: ${errorMessage(error)}`, ["editor_status says whether it has since connected"]);
|
|
47071
|
+
}
|
|
47072
|
+
}
|
|
47073
|
+
const stopWaitMs = 1e4;
|
|
47074
|
+
if (!await this.waitForBridge(() => !alive(launched.pid), began + stopWaitMs)) {
|
|
47075
|
+
return this.createErrorResponse(`The editor this server opened, pid ${launched.pid}, had not connected and was told to stop, and is still running after ${stopWaitMs / 1000}s.`, ["editor_status says whether it has since connected"]);
|
|
47076
|
+
}
|
|
47077
|
+
this.closedHere = { editorPid: launched.pid, at: new Date().toISOString() };
|
|
47078
|
+
this.launchedEditor = null;
|
|
47079
|
+
this.launchedFrom = null;
|
|
47080
|
+
return this.jsonTextResponse({
|
|
47081
|
+
closed: true,
|
|
47082
|
+
editorPid: launched.pid,
|
|
47083
|
+
projectPath: launched.projectPath,
|
|
47084
|
+
beforeConnecting: true,
|
|
47085
|
+
note: "It had not reached the bridge, so it was stopped rather than asked to save and quit, and held nothing of this server's to save.",
|
|
47086
|
+
tookMs: Date.now() - began
|
|
47087
|
+
});
|
|
47088
|
+
}
|
|
46797
47089
|
settleTheLaunchReading() {
|
|
46798
47090
|
const watched = this.launchedFrom;
|
|
46799
47091
|
const status = this.godotBridge.getStatus();
|
|
@@ -46929,13 +47221,9 @@ class GodotServer {
|
|
|
46929
47221
|
`editor_launch with projectPath ${this.ownProject} opens the editor this server serves`
|
|
46930
47222
|
]);
|
|
46931
47223
|
}
|
|
46932
|
-
const
|
|
46933
|
-
|
|
46934
|
-
|
|
46935
|
-
return this.createErrorResponse(`The gdharness editor addon is not ${installed ? "enabled" : "installed"} in ${projectPath}, so an editor opened on it would never reach this server.`, installed ? [
|
|
46936
|
-
"gdharness setup in the project enables it",
|
|
46937
|
-
"Or enable it under Project Settings, Plugins, in an editor opened by hand"
|
|
46938
|
-
] : ["gdharness setup in the project installs and enables it"]);
|
|
47224
|
+
const missing = editorAddonMissing(projectPath);
|
|
47225
|
+
if (missing !== null) {
|
|
47226
|
+
return this.createErrorResponse(`${missing.why}, so an editor opened on it would never reach this server.`, missing.remedies);
|
|
46939
47227
|
}
|
|
46940
47228
|
const owed = restartOwed(projectPath);
|
|
46941
47229
|
if (owed !== null && restartUnderway(owed)) {
|
|
@@ -46963,7 +47251,8 @@ class GodotServer {
|
|
|
46963
47251
|
writeEditorLogNote(projectPath, launched.pid);
|
|
46964
47252
|
this.launchedEditor = {
|
|
46965
47253
|
pid: launched.pid,
|
|
46966
|
-
startedAt: (await startTimesOf([launched.pid])).get(launched.pid) ?? null
|
|
47254
|
+
startedAt: (await startTimesOf([launched.pid])).get(launched.pid) ?? null,
|
|
47255
|
+
projectPath
|
|
46967
47256
|
};
|
|
46968
47257
|
this.closedHere = null;
|
|
46969
47258
|
return { pid: launched.pid, error: null };
|
|
@@ -47002,6 +47291,10 @@ class GodotServer {
|
|
|
47002
47291
|
if (!flags.ok) {
|
|
47003
47292
|
return flags.response;
|
|
47004
47293
|
}
|
|
47294
|
+
const movie = movieRefusal(flags.value, [project.value.path], op === "check" || resolveHeadless(args["headless"], { platform: process.platform, variables: process.env }));
|
|
47295
|
+
if (movie !== null) {
|
|
47296
|
+
return this.createErrorResponse(movie);
|
|
47297
|
+
}
|
|
47005
47298
|
const asked = this.runEnvironment(args);
|
|
47006
47299
|
if (!asked.ok) {
|
|
47007
47300
|
return asked.response;
|
|
@@ -47096,7 +47389,7 @@ class GodotServer {
|
|
|
47096
47389
|
}
|
|
47097
47390
|
async runtimeUp(projectPath, before, budget, withArgs) {
|
|
47098
47391
|
const budgetMs = budget.ms;
|
|
47099
|
-
if (!existsSync20(
|
|
47392
|
+
if (!existsSync20(join27(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
47100
47393
|
return runtimeVerdict(null, {
|
|
47101
47394
|
addon: false,
|
|
47102
47395
|
budgetMs,
|
|
@@ -47181,7 +47474,7 @@ class GodotServer {
|
|
|
47181
47474
|
}
|
|
47182
47475
|
let announcedAt;
|
|
47183
47476
|
try {
|
|
47184
|
-
announcedAt =
|
|
47477
|
+
announcedAt = statSync10(endpoint.file).mtimeMs;
|
|
47185
47478
|
} catch {
|
|
47186
47479
|
return;
|
|
47187
47480
|
}
|
|
@@ -47350,7 +47643,7 @@ class GodotServer {
|
|
|
47350
47643
|
return;
|
|
47351
47644
|
}
|
|
47352
47645
|
if (game.transcript !== null) {
|
|
47353
|
-
|
|
47646
|
+
appendFileSync2(game.transcript, reported);
|
|
47354
47647
|
} else {
|
|
47355
47648
|
game.log.append("stderr", reported);
|
|
47356
47649
|
}
|
|
@@ -48054,7 +48347,7 @@ class GodotServer {
|
|
|
48054
48347
|
}
|
|
48055
48348
|
const going = await this.runStillGoing(run);
|
|
48056
48349
|
const projectPath = run.projectPath;
|
|
48057
|
-
const projectGone = projectPath !== null && !existsSync20(
|
|
48350
|
+
const projectGone = projectPath !== null && !existsSync20(join27(projectPath, "project.godot"));
|
|
48058
48351
|
if (projectGone && callersName !== undefined) {
|
|
48059
48352
|
return this.createErrorResponse(`callersOf reads the project's scripts, and the run's project at ${projectPath} has no project.godot any more.`, ["editor_run profile without callersOf answers with the profile itself"]);
|
|
48060
48353
|
}
|
|
@@ -48225,7 +48518,11 @@ class GodotServer {
|
|
|
48225
48518
|
if (!greeted && launched === null && status.connected) {
|
|
48226
48519
|
return this.createErrorResponse("The editor has connected and has not yet said who it is, so whose console to read cannot be told yet.", ["Ask again in a moment; editor_status shows greeting until it has"]);
|
|
48227
48520
|
}
|
|
48228
|
-
const projectPath = status.projectPath ?? this.ownProject ?? undefined;
|
|
48521
|
+
const projectPath = status.projectPath ?? this.launchedEditor?.projectPath ?? this.ownProject ?? undefined;
|
|
48522
|
+
const elsewhere = this.anotherProjectNamed(args, projectPath);
|
|
48523
|
+
if (elsewhere !== null) {
|
|
48524
|
+
return elsewhere;
|
|
48525
|
+
}
|
|
48229
48526
|
if (projectPath === undefined || !greeted && launched === null) {
|
|
48230
48527
|
return this.createErrorResponse("No editor is connected, so there is no console to read.", [
|
|
48231
48528
|
"editor_status says whether one is connected and whether it may yet be",
|
|
@@ -48276,6 +48573,19 @@ class GodotServer {
|
|
|
48276
48573
|
omitted: selected.omitted === 0 ? undefined : selected.omitted
|
|
48277
48574
|
});
|
|
48278
48575
|
}
|
|
48576
|
+
anotherProjectNamed(args, ofWhatIsRead) {
|
|
48577
|
+
const named = readNonEmptyString(args, "projectPath");
|
|
48578
|
+
if (named === undefined || ofWhatIsRead === null || ofWhatIsRead === undefined || ofWhatIsRead === "") {
|
|
48579
|
+
return null;
|
|
48580
|
+
}
|
|
48581
|
+
if (isSameDirectory(named, ofWhatIsRead)) {
|
|
48582
|
+
return null;
|
|
48583
|
+
}
|
|
48584
|
+
return this.createErrorResponse(`editor_output reads what this server holds, which is of ${ofWhatIsRead}, and this call names ${named}.`, [
|
|
48585
|
+
"Another project's run is read through that project's own gdharness server",
|
|
48586
|
+
"editor_status names every game running on the machine under runtimes"
|
|
48587
|
+
]);
|
|
48588
|
+
}
|
|
48279
48589
|
async handleGetDebugOutput(args, waitedMs) {
|
|
48280
48590
|
if (readString(args, "op") === "editor") {
|
|
48281
48591
|
return this.handleEditorConsole(args);
|
|
@@ -48285,6 +48595,10 @@ class GodotServer {
|
|
|
48285
48595
|
if (!run) {
|
|
48286
48596
|
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
48287
48597
|
}
|
|
48598
|
+
const elsewhere = this.anotherProjectNamed(args, run.projectPath ?? this.godotBridge.getStatus().projectPath);
|
|
48599
|
+
if (elsewhere !== null) {
|
|
48600
|
+
return elsewhere;
|
|
48601
|
+
}
|
|
48288
48602
|
await this.tieThePlayedGame(run);
|
|
48289
48603
|
this.drainEditorOutput(run);
|
|
48290
48604
|
this.drainTranscript(run);
|
|
@@ -48321,7 +48635,7 @@ class GodotServer {
|
|
|
48321
48635
|
notes.push(endedWithoutACode({
|
|
48322
48636
|
throughEditor: run.throughEditor,
|
|
48323
48637
|
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
48324
|
-
couldAnnounce: run.projectPath !== null && existsSync20(
|
|
48638
|
+
couldAnnounce: run.projectPath !== null && existsSync20(join27(run.projectPath, RUNTIME_AUTOLOAD.path)),
|
|
48325
48639
|
endedHere: typeof run.endedHere === "string",
|
|
48326
48640
|
pickedUp: run.pickedUp === true
|
|
48327
48641
|
}));
|
|
@@ -49101,15 +49415,47 @@ class GodotServer {
|
|
|
49101
49415
|
const holds = held["classes"].map(String);
|
|
49102
49416
|
return { unseen: unseenByEditor(projectPath, holds), stillHeld: heldButGone(projectPath, holds) };
|
|
49103
49417
|
}
|
|
49418
|
+
async noEditorYet(args, instead) {
|
|
49419
|
+
const project = readNonEmptyString(args, "projectPath") ?? this.ownProject;
|
|
49420
|
+
const missing = project === null ? null : editorAddonMissing(project);
|
|
49421
|
+
if (missing !== null) {
|
|
49422
|
+
return this.createErrorResponse(`Editor not connected, and no editor of this project can connect: ${missing.why}.`, [...instead, ...missing.remedies, "editor_launch then opens an editor that connects"]);
|
|
49423
|
+
}
|
|
49424
|
+
const launched = this.launchedEditor;
|
|
49425
|
+
if (launched !== null && await this.launchedEditorIsUp()) {
|
|
49426
|
+
const began = Date.now();
|
|
49427
|
+
await this.waitForBridge(() => hasSaidWhoItIs(this.godotBridge.getStatus()) || !alive(launched.pid), began + editorRestartTimeoutMs());
|
|
49428
|
+
if (this.godotBridge.isConnected()) {
|
|
49429
|
+
return null;
|
|
49430
|
+
}
|
|
49431
|
+
const opened = `the editor this server opened on ${launched.projectPath}, pid ${launched.pid}`;
|
|
49432
|
+
if (alive(launched.pid)) {
|
|
49433
|
+
return this.createErrorResponse(`Editor not connected: ${opened}, is still running and has not reached the bridge in the ${String(Math.round((Date.now() - began) / 1000))}s this call waited. An editor dials in once it has imported the project and loaded its plugins, and the first open of a large project imports for minutes.`, [
|
|
49434
|
+
...instead,
|
|
49435
|
+
"editor_status names it under awaitingLaunchedEditor until it has connected",
|
|
49436
|
+
"editor_output op editor shows what it is printing meanwhile"
|
|
49437
|
+
]);
|
|
49438
|
+
}
|
|
49439
|
+
return this.createErrorResponse(`Editor not connected: ${opened}, exited without reaching the bridge.`, [
|
|
49440
|
+
...instead,
|
|
49441
|
+
"editor_output op editor shows what it printed before it went",
|
|
49442
|
+
"editor_launch open starts another"
|
|
49443
|
+
]);
|
|
49444
|
+
}
|
|
49445
|
+
return this.createErrorResponse("Editor not connected: no Godot editor with the gdharness editor addon has reached this server.", [
|
|
49446
|
+
...instead,
|
|
49447
|
+
"Open the project in the editor with editor_launch, or by hand",
|
|
49448
|
+
"Enable the addon under Project > Project Settings > Plugins",
|
|
49449
|
+
"editor_status says whether the bridge is up and what it is listening on"
|
|
49450
|
+
]);
|
|
49451
|
+
}
|
|
49104
49452
|
async handleViaBridge(toolName, args, askedAs) {
|
|
49105
49453
|
if (!this.godotBridge.isConnected()) {
|
|
49106
|
-
const
|
|
49107
|
-
|
|
49108
|
-
|
|
49109
|
-
|
|
49110
|
-
|
|
49111
|
-
"editor_status says whether the bridge is up and what it is listening on"
|
|
49112
|
-
]);
|
|
49454
|
+
const alternative = askedAs !== undefined && Object.hasOwn(EDITOR_FREE_INSTEAD, askedAs) ? EDITOR_FREE_INSTEAD[askedAs] : undefined;
|
|
49455
|
+
const refused = await this.noEditorYet(args, alternative === undefined ? [] : [alternative]);
|
|
49456
|
+
if (refused !== null) {
|
|
49457
|
+
return refused;
|
|
49458
|
+
}
|
|
49113
49459
|
}
|
|
49114
49460
|
const open = this.godotBridge.getStatus().projectPath;
|
|
49115
49461
|
const meant = readNonEmptyString(args, "projectPath");
|
|
@@ -49182,7 +49528,7 @@ class GodotServer {
|
|
|
49182
49528
|
async ownRunOnItsWay(projectPath, budgetMs) {
|
|
49183
49529
|
const run = await this.currentRun();
|
|
49184
49530
|
const project = projectPath ?? run?.projectPath ?? null;
|
|
49185
|
-
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync20(
|
|
49531
|
+
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync20(join27(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
|
|
49186
49532
|
return null;
|
|
49187
49533
|
}
|
|
49188
49534
|
const already = runtimesAnnounced().running;
|
|
@@ -49191,7 +49537,7 @@ class GodotServer {
|
|
|
49191
49537
|
return !run.announcedBefore.has(one.pid);
|
|
49192
49538
|
}
|
|
49193
49539
|
try {
|
|
49194
|
-
return
|
|
49540
|
+
return statSync10(one.file).mtimeMs >= run.startedAt;
|
|
49195
49541
|
} catch {
|
|
49196
49542
|
return false;
|
|
49197
49543
|
}
|
|
@@ -49267,7 +49613,7 @@ class GodotServer {
|
|
|
49267
49613
|
}
|
|
49268
49614
|
}
|
|
49269
49615
|
const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
|
|
49270
|
-
const screenshotPath = screenshotDir ?
|
|
49616
|
+
const screenshotPath = screenshotDir ? join27(screenshotDir, "capture.png") : null;
|
|
49271
49617
|
const staleNote = this.runtimeStaleNote(choice.endpoint);
|
|
49272
49618
|
const stale = staleNote === undefined ? {} : { addonIsStale: true, staleNote };
|
|
49273
49619
|
const refused = (message) => this.createErrorResponse(staleNote === undefined ? message : `${message}
|
|
@@ -49418,7 +49764,7 @@ async function runGodotServer() {
|
|
|
49418
49764
|
const server = new GodotServer;
|
|
49419
49765
|
await server.run();
|
|
49420
49766
|
}
|
|
49421
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, EDITOR_FREE_INSTEAD, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
|
|
49767
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, EDITOR_FREE_INSTEAD, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
|
|
49422
49768
|
var init_server = __esm(() => {
|
|
49423
49769
|
init_dist();
|
|
49424
49770
|
init_stdio();
|
|
@@ -49447,6 +49793,7 @@ var init_server = __esm(() => {
|
|
|
49447
49793
|
init_junit();
|
|
49448
49794
|
init_keeper_channel();
|
|
49449
49795
|
init_launch();
|
|
49796
|
+
init_lifecycle_log();
|
|
49450
49797
|
init_lsp_client();
|
|
49451
49798
|
init_outside();
|
|
49452
49799
|
init_paths();
|
|
@@ -49472,7 +49819,7 @@ var init_server = __esm(() => {
|
|
|
49472
49819
|
init_tool_reference();
|
|
49473
49820
|
init_update_check();
|
|
49474
49821
|
run4 = promisify5(execFile5);
|
|
49475
|
-
__dirname2 =
|
|
49822
|
+
__dirname2 = dirname20(fileURLToPath5(import.meta.url));
|
|
49476
49823
|
CONSOLE_WRAPPER = /_console(\.exe)?$/i;
|
|
49477
49824
|
LONGEST_BOOT_NOTED_MS = 5 * 60000;
|
|
49478
49825
|
VALUE_NAMES = {
|
|
@@ -49531,7 +49878,7 @@ init_bridge_announce();
|
|
|
49531
49878
|
init_errors();
|
|
49532
49879
|
init_godot_path();
|
|
49533
49880
|
import { existsSync as existsSync21 } from "node:fs";
|
|
49534
|
-
import { join as
|
|
49881
|
+
import { join as join28, resolve as resolve8 } from "node:path";
|
|
49535
49882
|
|
|
49536
49883
|
// src/harnesses.ts
|
|
49537
49884
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
@@ -50692,7 +51039,9 @@ emptily: no game running, a game with no debugger behind it, a game running rath
|
|
|
50692
51039
|
control outside a headless viewport. Read the refusal instead of retrying the call.
|
|
50693
51040
|
|
|
50694
51041
|
Every answer is read back from the engine after the change rather than echoed from the request, and
|
|
50695
|
-
whatever the engine said on stderr comes back under \`engine_messages\`.
|
|
51042
|
+
whatever the engine said on stderr comes back under \`engine_messages\`. A headless engine runs the
|
|
51043
|
+
project's autoloads after the operation has answered, and what it reports from then until it exits
|
|
51044
|
+
comes back separately, under \`engine_messages_after_answer\`: none of it changes the answer.
|
|
50696
51045
|
|
|
50697
51046
|
## More
|
|
50698
51047
|
|
|
@@ -50794,7 +51143,7 @@ class UsageError extends Refusal {
|
|
|
50794
51143
|
function projectArgument(at) {
|
|
50795
51144
|
const named = args.slice(at).find((value) => !value.startsWith("--"));
|
|
50796
51145
|
const projectPath = resolve8(named ?? ".");
|
|
50797
|
-
if (!existsSync21(
|
|
51146
|
+
if (!existsSync21(join28(projectPath, "project.godot"))) {
|
|
50798
51147
|
throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
|
|
50799
51148
|
}
|
|
50800
51149
|
return projectPath;
|
|
@@ -50839,7 +51188,7 @@ async function registerRuntime(godot, projectPath) {
|
|
|
50839
51188
|
return;
|
|
50840
51189
|
}
|
|
50841
51190
|
console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
|
|
50842
|
-
if (!existsSync21(
|
|
51191
|
+
if (!existsSync21(join28(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
|
|
50843
51192
|
console.log(` that file is not in this project, so it boots with a missing script: restore it, or point the entry at res://${RUNTIME_AUTOLOAD.path}`);
|
|
50844
51193
|
}
|
|
50845
51194
|
}
|