gdharness 1.2.18 → 1.2.20
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 +169 -27
- package/build/index.js +162 -28
- package/build/keeper.js +94 -32
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -642,6 +642,36 @@ function resolveSilent(requested, variables) {
|
|
|
642
642
|
const said = envValue("GDHARNESS_SILENT", variables)?.toLowerCase();
|
|
643
643
|
return said !== undefined && said !== "0" && said !== "false";
|
|
644
644
|
}
|
|
645
|
+
function engineFlags(given) {
|
|
646
|
+
const offered = Object.keys(ENGINE_FLAGS).join(", ");
|
|
647
|
+
for (let at = 0;at < given.length; at += 1) {
|
|
648
|
+
const flag = given[at] ?? "";
|
|
649
|
+
const takes = Object.hasOwn(ENGINE_FLAGS, flag) ? ENGINE_FLAGS[flag] : undefined;
|
|
650
|
+
if (takes === undefined) {
|
|
651
|
+
const setBy = Object.hasOwn(SET_BY_ARGUMENT, flag) ? SET_BY_ARGUMENT[flag] : undefined;
|
|
652
|
+
return {
|
|
653
|
+
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
|
+
};
|
|
656
|
+
}
|
|
657
|
+
if (takes === "none") {
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
const value = given[at + 1];
|
|
661
|
+
const fits = value !== undefined && (takes === "number" ? /^\d+(?:\.\d+)?$/.test(value) : /^\d+x\d+$/.test(value));
|
|
662
|
+
if (!fits) {
|
|
663
|
+
return {
|
|
664
|
+
ok: false,
|
|
665
|
+
reason: `${flag} takes ${takes === "number" ? "a number" : "a size such as 1280x720"} as the next entry, and ${value === undefined ? "nothing followed it" : `${JSON.stringify(value)} is not one`}.`
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
at += 1;
|
|
669
|
+
}
|
|
670
|
+
return { ok: true };
|
|
671
|
+
}
|
|
672
|
+
function engineFlagsAmongGameArgs(args) {
|
|
673
|
+
return args.filter((one) => Object.hasOwn(ENGINE_FLAGS, one) || Object.hasOwn(SET_BY_ARGUMENT, one));
|
|
674
|
+
}
|
|
645
675
|
function runArguments(options) {
|
|
646
676
|
const args = options.headless ? ["--headless", "--path", options.projectPath] : ["--path", options.projectPath];
|
|
647
677
|
if (options.silent === true && !options.headless) {
|
|
@@ -650,6 +680,9 @@ function runArguments(options) {
|
|
|
650
680
|
if (options.quitAfter !== undefined && options.quitAfter !== null) {
|
|
651
681
|
args.push("--quit-after", String(options.quitAfter));
|
|
652
682
|
}
|
|
683
|
+
if (options.engineArgs !== undefined) {
|
|
684
|
+
args.push(...options.engineArgs);
|
|
685
|
+
}
|
|
653
686
|
if (options.scene !== null) {
|
|
654
687
|
args.push(`res://${options.scene}`);
|
|
655
688
|
}
|
|
@@ -692,7 +725,35 @@ function environmentFor(options, variables = process.env) {
|
|
|
692
725
|
const base = options.savesIn === undefined ? { ...variables } : userDataIn(options.savesIn, variables);
|
|
693
726
|
return { ...base, ...options.env, GDHARNESS_RUNTIME_DIR: options.runtimeDirectory };
|
|
694
727
|
}
|
|
695
|
-
var 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.";
|
|
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 init_launch = __esm(() => {
|
|
730
|
+
ENGINE_FLAGS = Object.freeze({
|
|
731
|
+
"--verbose": "none",
|
|
732
|
+
"--quiet": "none",
|
|
733
|
+
"--debug-collisions": "none",
|
|
734
|
+
"--debug-paths": "none",
|
|
735
|
+
"--debug-navigation": "none",
|
|
736
|
+
"--debug-avoidance": "none",
|
|
737
|
+
"--debug-stringnames": "none",
|
|
738
|
+
"--debug-canvas-item-redraw": "none",
|
|
739
|
+
"--debug-mute-audio": "none",
|
|
740
|
+
"--print-fps": "none",
|
|
741
|
+
"--gpu-profile": "none",
|
|
742
|
+
"--gpu-validation": "none",
|
|
743
|
+
"--gpu-abort": "none",
|
|
744
|
+
"--disable-vsync": "none",
|
|
745
|
+
"--frame-delay": "number",
|
|
746
|
+
"--time-scale": "number",
|
|
747
|
+
"--fixed-fps": "number",
|
|
748
|
+
"--max-fps": "number",
|
|
749
|
+
"--resolution": "size"
|
|
750
|
+
});
|
|
751
|
+
SET_BY_ARGUMENT = Object.freeze({
|
|
752
|
+
"--headless": "headless",
|
|
753
|
+
"--audio-driver": "silent",
|
|
754
|
+
"--quit-after": "frames, on editor_run check"
|
|
755
|
+
});
|
|
756
|
+
});
|
|
696
757
|
|
|
697
758
|
// src/godot-path.ts
|
|
698
759
|
import { execFile as execFile2 } from "node:child_process";
|
|
@@ -769,6 +830,7 @@ var run2;
|
|
|
769
830
|
var init_godot_path = __esm(() => {
|
|
770
831
|
init_detection();
|
|
771
832
|
init_errors();
|
|
833
|
+
init_launch();
|
|
772
834
|
run2 = promisify2(execFile2);
|
|
773
835
|
});
|
|
774
836
|
|
|
@@ -10840,6 +10902,7 @@ function annotationsOf(spec) {
|
|
|
10840
10902
|
}
|
|
10841
10903
|
var PROJECT_INFO_SECTIONS, PROJECT_PATH, RUNNING_PROJECT_PATH, RUNNING_PID, SCENE_PATH, SCRIPT_PATH, RESOURCE_PATH, NODE_PATH, PROPERTIES, XY, ANIMATION_TRACK, SCRIPT_MODIFICATIONS, INPUT_EVENTS, TILESET_SOURCES, TILEMAP_CELLS, COLOUR, TOOL_SPECS, SPECS_BY_NAME;
|
|
10842
10904
|
var init_tool_definitions = __esm(() => {
|
|
10905
|
+
init_launch();
|
|
10843
10906
|
PROJECT_INFO_SECTIONS = [
|
|
10844
10907
|
"autoloads",
|
|
10845
10908
|
"plugins",
|
|
@@ -11758,7 +11821,13 @@ var init_tool_definitions = __esm(() => {
|
|
|
11758
11821
|
type: "array",
|
|
11759
11822
|
items: { type: "string" },
|
|
11760
11823
|
ops: ["start", "check"],
|
|
11761
|
-
description: `The game's own arguments, what OS.get_cmdline_user_args() answers, such as ["--level=2"]. The separator is added here. A run with any is started by this server rather than by the editor, which fixes the game's command line when it opens the project, so the debug_* tools do not answer for it.`
|
|
11824
|
+
description: `The game's own arguments, what OS.get_cmdline_user_args() answers, such as ["--level=2"]. The separator is added here, so a flag the engine reads goes to the game instead, and the answer says so under argsNote. A run with any is started by this server rather than by the editor, which fixes the game's command line when it opens the project, so the debug_* tools do not answer for it.`
|
|
11825
|
+
},
|
|
11826
|
+
engineArgs: {
|
|
11827
|
+
type: "array",
|
|
11828
|
+
items: { type: "string" },
|
|
11829
|
+
ops: ["start", "check"],
|
|
11830
|
+
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.`
|
|
11762
11831
|
},
|
|
11763
11832
|
headless: { type: "boolean", ops: ["start"], description: "start: force a window or no window." },
|
|
11764
11833
|
visible: {
|
|
@@ -11855,7 +11924,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
11855
11924
|
requires: []
|
|
11856
11925
|
}
|
|
11857
11926
|
},
|
|
11858
|
-
answer: "stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked.",
|
|
11927
|
+
answer: "stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. arguments is the engine command line the check ran, as a start's is. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked.",
|
|
11859
11928
|
defaultOperation: "start"
|
|
11860
11929
|
},
|
|
11861
11930
|
{
|
|
@@ -34734,6 +34803,7 @@ function runtimeRequest(endpoint, command, params, timeoutMs, keepListening = fa
|
|
|
34734
34803
|
var RUNTIME_PROTOCOL = 2, ANNOUNCEMENT_PATTERN, ERROR_REPORT_PATTERN, CONSOLE_BOUNDARY = "[gdharness] runtime listening on ", ERROR_REPORT_KEEP_MS, GONE_WINDOW_MS = 120000, GONE_KEPT = 32, gone, STARTED_AFTER_ANNOUNCING_MS = 2000, CONFIRMED_FOR_MS = 60000, JUDGED_AFTER_MS = 60000, verdicts, judging = null, ENDED_WITHIN_MS = 3000, LOOK_EVERY_MS = 50, ANNOUNCE_BUDGET_MS = 5000, nextRequestId = 1, LATE_ANSWER_MS, LATE_KEPT = 50, lateAnswers;
|
|
34735
34804
|
var init_runtime_client = __esm(() => {
|
|
34736
34805
|
init_alive();
|
|
34806
|
+
init_launch();
|
|
34737
34807
|
init_paths();
|
|
34738
34808
|
init_process_children();
|
|
34739
34809
|
ANNOUNCEMENT_PATTERN = /^runtime-(\d+)\.json$/;
|
|
@@ -40746,6 +40816,7 @@ var init_lsp_client = __esm(() => {
|
|
|
40746
40816
|
init_answer_json();
|
|
40747
40817
|
init_errors();
|
|
40748
40818
|
init_framing();
|
|
40819
|
+
init_launch();
|
|
40749
40820
|
init_paths();
|
|
40750
40821
|
init_ports();
|
|
40751
40822
|
LSPTimeout = class LSPTimeout extends Error {
|
|
@@ -42409,19 +42480,41 @@ var init_restart_note = __esm(() => {
|
|
|
42409
42480
|
init_errors();
|
|
42410
42481
|
});
|
|
42411
42482
|
|
|
42483
|
+
// src/whole-file.ts
|
|
42484
|
+
import { renameSync as renameSync2, rmSync as rmSync12, writeFileSync as writeFileSync16 } from "node:fs";
|
|
42485
|
+
function writeWhole(path, text) {
|
|
42486
|
+
const partial = `${path}.${process.pid}.partial`;
|
|
42487
|
+
try {
|
|
42488
|
+
writeFileSync16(partial, text, "utf8");
|
|
42489
|
+
} catch {
|
|
42490
|
+
return false;
|
|
42491
|
+
}
|
|
42492
|
+
const until = Date.now() + HELD_FILE_MS;
|
|
42493
|
+
for (;; ) {
|
|
42494
|
+
try {
|
|
42495
|
+
renameSync2(partial, path);
|
|
42496
|
+
return true;
|
|
42497
|
+
} catch (error) {
|
|
42498
|
+
const code = error.code ?? "";
|
|
42499
|
+
if (!HELD_BY_A_READER.has(code) || Date.now() >= until) {
|
|
42500
|
+
try {
|
|
42501
|
+
rmSync12(partial, { force: true });
|
|
42502
|
+
} catch {}
|
|
42503
|
+
return false;
|
|
42504
|
+
}
|
|
42505
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, HELD_FILE_RETRY_MS);
|
|
42506
|
+
}
|
|
42507
|
+
}
|
|
42508
|
+
}
|
|
42509
|
+
var HELD_FILE_MS = 1000, HELD_FILE_RETRY_MS = 10, HELD_BY_A_READER;
|
|
42510
|
+
var init_whole_file = __esm(() => {
|
|
42511
|
+
HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
42512
|
+
});
|
|
42513
|
+
|
|
42412
42514
|
// src/run-record.ts
|
|
42413
42515
|
import { execFile as execFile4 } from "node:child_process";
|
|
42414
42516
|
import { createHash as createHash4 } from "node:crypto";
|
|
42415
|
-
import {
|
|
42416
|
-
mkdirSync as mkdirSync14,
|
|
42417
|
-
openSync as openSync2,
|
|
42418
|
-
readdirSync as readdirSync10,
|
|
42419
|
-
readFileSync as readFileSync21,
|
|
42420
|
-
realpathSync as realpathSync6,
|
|
42421
|
-
rmSync as rmSync12,
|
|
42422
|
-
statSync as statSync8,
|
|
42423
|
-
writeFileSync as writeFileSync16
|
|
42424
|
-
} from "node:fs";
|
|
42517
|
+
import { mkdirSync as mkdirSync14, openSync as openSync2, readdirSync as readdirSync10, readFileSync as readFileSync21, realpathSync as realpathSync6, rmSync as rmSync13, statSync as statSync8 } from "node:fs";
|
|
42425
42518
|
import { basename as basename6, dirname as dirname18, join as join24, resolve as resolve7 } from "node:path";
|
|
42426
42519
|
import { promisify as promisify4 } from "node:util";
|
|
42427
42520
|
function runsDirectory() {
|
|
@@ -42457,7 +42550,7 @@ function latestRunPath(servedBy, directory = runtimeDirectory()) {
|
|
|
42457
42550
|
function writeLatestRun(servedBy, latest) {
|
|
42458
42551
|
try {
|
|
42459
42552
|
mkdirSync14(runsDirectory(), { recursive: true });
|
|
42460
|
-
|
|
42553
|
+
writeWhole(latestRunPath(servedBy), JSON.stringify(latest, null, 2));
|
|
42461
42554
|
} catch {}
|
|
42462
42555
|
}
|
|
42463
42556
|
function readLatestRun(servedBy, directory = runtimeDirectory()) {
|
|
@@ -42491,7 +42584,10 @@ function editorNotePath(projectPath, directory = runtimeDirectory()) {
|
|
|
42491
42584
|
}
|
|
42492
42585
|
function writeEditorRunNote(note) {
|
|
42493
42586
|
mkdirSync14(runsDirectory(), { recursive: true });
|
|
42494
|
-
|
|
42587
|
+
const path = editorNotePath(note.projectPath);
|
|
42588
|
+
if (!writeWhole(path, JSON.stringify(note, null, 2))) {
|
|
42589
|
+
throw new Error(`the editor run note could not be written at ${path}`);
|
|
42590
|
+
}
|
|
42495
42591
|
}
|
|
42496
42592
|
function readEditorRunNote(projectPath) {
|
|
42497
42593
|
for (const directory of runtimeDirectories()) {
|
|
@@ -42762,7 +42858,7 @@ function clearRunRecord(projectPath) {
|
|
|
42762
42858
|
const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
|
|
42763
42859
|
for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
|
|
42764
42860
|
try {
|
|
42765
|
-
|
|
42861
|
+
rmSync13(path, { force: true });
|
|
42766
42862
|
} catch {}
|
|
42767
42863
|
}
|
|
42768
42864
|
}
|
|
@@ -42787,7 +42883,8 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
42787
42883
|
return;
|
|
42788
42884
|
}
|
|
42789
42885
|
for (const entry of entries) {
|
|
42790
|
-
|
|
42886
|
+
const transcript = entry.startsWith("run-") && (entry.endsWith(".log") || entry.endsWith(".profile.json"));
|
|
42887
|
+
if (!transcript && !entry.endsWith(".partial")) {
|
|
42791
42888
|
continue;
|
|
42792
42889
|
}
|
|
42793
42890
|
const path = join24(directory, entry);
|
|
@@ -42795,7 +42892,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
42795
42892
|
continue;
|
|
42796
42893
|
}
|
|
42797
42894
|
try {
|
|
42798
|
-
|
|
42895
|
+
rmSync13(path);
|
|
42799
42896
|
} catch {}
|
|
42800
42897
|
}
|
|
42801
42898
|
}
|
|
@@ -42805,6 +42902,7 @@ var init_run_record = __esm(() => {
|
|
|
42805
42902
|
init_paths();
|
|
42806
42903
|
init_process_children();
|
|
42807
42904
|
init_runtime_client();
|
|
42905
|
+
init_whole_file();
|
|
42808
42906
|
init_windows_ask();
|
|
42809
42907
|
execFileAsync = promisify4(execFile4);
|
|
42810
42908
|
});
|
|
@@ -42915,7 +43013,7 @@ var init_godot_variant = __esm(() => {
|
|
|
42915
43013
|
});
|
|
42916
43014
|
|
|
42917
43015
|
// src/script-profile.ts
|
|
42918
|
-
import { readFileSync as readFileSync22
|
|
43016
|
+
import { readFileSync as readFileSync22 } from "node:fs";
|
|
42919
43017
|
function readProfileFile(path) {
|
|
42920
43018
|
try {
|
|
42921
43019
|
const parsed = JSON.parse(readFileSync22(path, "utf8"));
|
|
@@ -43069,6 +43167,7 @@ var init_script_profile = __esm(() => {
|
|
|
43069
43167
|
init_gdscript_source();
|
|
43070
43168
|
init_godot_variant();
|
|
43071
43169
|
init_resources();
|
|
43170
|
+
init_whole_file();
|
|
43072
43171
|
});
|
|
43073
43172
|
|
|
43074
43173
|
// src/server-instructions.ts
|
|
@@ -43618,6 +43717,14 @@ function scanWaitAnswer(scanned, started = "game") {
|
|
|
43618
43717
|
scanNote: !scanned.stillScanning ? undefined : started === "game" ? `${still} this game started during the scan and may have read a class cache being rewritten. If ` + 'editor_output shows "Could not find type" for a class that is declared, editor_rescan ' + "waits for the scan to finish, and a start after it reads the finished cache." : `${still} the engine answering this started during the scan and may have read a class cache being ` + "rewritten, and a class cache it wrote can be written over when the scan finishes. If " + 'engine_messages shows "Could not find type" for a class that is declared, editor_rescan waits ' + "for the scan to finish, and the same call after it reads the finished cache."
|
|
43619
43718
|
};
|
|
43620
43719
|
}
|
|
43720
|
+
function engineFlagsNote(args) {
|
|
43721
|
+
const among = engineFlagsAmongGameArgs(args);
|
|
43722
|
+
if (among.length === 0) {
|
|
43723
|
+
return;
|
|
43724
|
+
}
|
|
43725
|
+
const named = among.join(", ");
|
|
43726
|
+
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.`;
|
|
43727
|
+
}
|
|
43621
43728
|
function opElsewhere(tool, requested) {
|
|
43622
43729
|
const ops = Object.hasOwn(OPS_ELSEWHERE, tool) ? OPS_ELSEWHERE[tool] : undefined;
|
|
43623
43730
|
const where = ops !== undefined && typeof requested === "string" && Object.hasOwn(ops, requested) ? ops[requested] : undefined;
|
|
@@ -43999,9 +44106,9 @@ function callersAnswer(totals, asked) {
|
|
|
43999
44106
|
note: found.sites.length === 0 ? `No script in the project names ${asked.name} in code, outside its declaration: a call made through a string, such as call("name"), is not found by name.` : found.property ? `${byName} ${PROPERTY_PLACES}` : byName
|
|
44000
44107
|
};
|
|
44001
44108
|
}
|
|
44002
|
-
function foldedEntriesAnswer(log) {
|
|
44109
|
+
function foldedEntriesAnswer(log, floor = "warning") {
|
|
44003
44110
|
const problems = log.selectFolded({
|
|
44004
|
-
severity:
|
|
44111
|
+
severity: floor,
|
|
44005
44112
|
sinceLastCall: false,
|
|
44006
44113
|
limit: MOST_ENGINE_ENTRIES
|
|
44007
44114
|
});
|
|
@@ -45026,6 +45133,25 @@ class GodotServer {
|
|
|
45026
45133
|
}
|
|
45027
45134
|
return { ok: true, value: given };
|
|
45028
45135
|
}
|
|
45136
|
+
engineArguments(args) {
|
|
45137
|
+
if (args["engineArgs"] === undefined) {
|
|
45138
|
+
return { ok: true, value: [] };
|
|
45139
|
+
}
|
|
45140
|
+
const given = readStringArray(args, "engineArgs");
|
|
45141
|
+
if (given === undefined) {
|
|
45142
|
+
return {
|
|
45143
|
+
ok: false,
|
|
45144
|
+
response: this.createErrorResponse("engineArgs must be a list of strings.", [
|
|
45145
|
+
'Pass engine flags, such as ["--verbose"] or ["--fixed-fps", "60"]'
|
|
45146
|
+
])
|
|
45147
|
+
};
|
|
45148
|
+
}
|
|
45149
|
+
const checked = engineFlags(given);
|
|
45150
|
+
if (!checked.ok) {
|
|
45151
|
+
return { ok: false, response: this.createErrorResponse(checked.reason) };
|
|
45152
|
+
}
|
|
45153
|
+
return { ok: true, value: given };
|
|
45154
|
+
}
|
|
45029
45155
|
runEnvironment(args) {
|
|
45030
45156
|
const savesIn = readNonEmptyString(args, "savesIn");
|
|
45031
45157
|
if (savesIn !== undefined && !isAbsolute3(savesIn)) {
|
|
@@ -46842,6 +46968,10 @@ class GodotServer {
|
|
|
46842
46968
|
if (!given.ok) {
|
|
46843
46969
|
return given.response;
|
|
46844
46970
|
}
|
|
46971
|
+
const flags = this.engineArguments(args);
|
|
46972
|
+
if (!flags.ok) {
|
|
46973
|
+
return flags.response;
|
|
46974
|
+
}
|
|
46845
46975
|
const asked = this.runEnvironment(args);
|
|
46846
46976
|
if (!asked.ok) {
|
|
46847
46977
|
return asked.response;
|
|
@@ -46863,7 +46993,7 @@ class GodotServer {
|
|
|
46863
46993
|
}
|
|
46864
46994
|
const sceneArgument = sceneToRun?.ok ? sceneToRun.relativePath : null;
|
|
46865
46995
|
if (op === "check") {
|
|
46866
|
-
return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value, scanned, asked.value);
|
|
46996
|
+
return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value, scanned, asked.value, flags.value);
|
|
46867
46997
|
}
|
|
46868
46998
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
46869
46999
|
let ended = null;
|
|
@@ -46890,7 +47020,7 @@ class GodotServer {
|
|
|
46890
47020
|
const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
|
|
46891
47021
|
const silent = resolveSilent(args["silent"], process.env) && !headless;
|
|
46892
47022
|
const profiled = readBoolean(args, "profile") === true;
|
|
46893
|
-
if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && !needsItsOwnProcess(asked.value) && !silent) {
|
|
47023
|
+
if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && flags.value.length === 0 && !needsItsOwnProcess(asked.value) && !silent) {
|
|
46894
47024
|
return await this.playThroughEditor(sceneArgument, refreshed.value, scanned, project.value.path, alreadyPlaying, runtimeWaitMs, ended, profiled);
|
|
46895
47025
|
}
|
|
46896
47026
|
const cmdArgs = runArguments({
|
|
@@ -46898,6 +47028,7 @@ class GodotServer {
|
|
|
46898
47028
|
headless,
|
|
46899
47029
|
scene: sceneArgument,
|
|
46900
47030
|
userArgs: given.value,
|
|
47031
|
+
engineArgs: flags.value,
|
|
46901
47032
|
silent
|
|
46902
47033
|
});
|
|
46903
47034
|
this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
@@ -46910,7 +47041,7 @@ class GodotServer {
|
|
|
46910
47041
|
started.announcedBefore = alreadyPlaying;
|
|
46911
47042
|
started.bootScene = runtimeWaitMs.scene;
|
|
46912
47043
|
this.activeProcess = started;
|
|
46913
|
-
const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && given.value.length > 0 ? " The editor cannot be handed arguments for a game it plays, so this one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && needsItsOwnProcess(asked.value) ? " The editor plays a game with its own environment, so a run given one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && silent ? " The editor cannot play a game silent, which takes an engine flag, so this one was " + `started here${args["silent"] === true ? "" : ", silent because GDHARNESS_SILENT says so"}: the debug_* tools answer only for a game the editor is playing, and silent: false plays it through the editor.` : "";
|
|
47044
|
+
const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && (given.value.length > 0 || flags.value.length > 0) ? " The editor cannot be handed arguments for a game it plays, so this one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && needsItsOwnProcess(asked.value) ? " The editor plays a game with its own environment, so a run given one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && silent ? " The editor cannot play a game silent, which takes an engine flag, so this one was " + `started here${args["silent"] === true ? "" : ", silent because GDHARNESS_SILENT says so"}: the debug_* tools answer only for a game the editor is playing, and silent: false plays it through the editor.` : "";
|
|
46914
47045
|
const endedForThis = endedToStartThis(ended);
|
|
46915
47046
|
const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
|
|
46916
47047
|
const alsoRunning = leftRunningNote(stranded);
|
|
@@ -46920,6 +47051,7 @@ class GodotServer {
|
|
|
46920
47051
|
alsoRunning: stranded.length === 0 ? undefined : stranded.map((one) => one.pid),
|
|
46921
47052
|
pid: started.pid,
|
|
46922
47053
|
arguments: cmdArgs,
|
|
47054
|
+
argsNote: engineFlagsNote(given.value),
|
|
46923
47055
|
transcript: started.transcript,
|
|
46924
47056
|
profile: profileOnStart(profiled, "gdharness", runtimeAddonRegistered(project.value.path)),
|
|
46925
47057
|
refreshedClasses: refreshed.value,
|
|
@@ -47822,10 +47954,17 @@ class GodotServer {
|
|
|
47822
47954
|
async currentRun() {
|
|
47823
47955
|
return this.activeProcess ?? await this.adoptRecordedRun();
|
|
47824
47956
|
}
|
|
47825
|
-
async checkBoot(godotPath, projectPath, scene, args, userArgs, scanned, asked = {}) {
|
|
47957
|
+
async checkBoot(godotPath, projectPath, scene, args, userArgs, scanned, asked = {}, engineArgs = []) {
|
|
47826
47958
|
const frames = readPositiveNumber(args, "frames") ?? 3;
|
|
47827
47959
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 60000;
|
|
47828
|
-
const cmdArgs = runArguments({
|
|
47960
|
+
const cmdArgs = runArguments({
|
|
47961
|
+
projectPath,
|
|
47962
|
+
headless: true,
|
|
47963
|
+
scene,
|
|
47964
|
+
quitAfter: frames,
|
|
47965
|
+
userArgs,
|
|
47966
|
+
engineArgs
|
|
47967
|
+
});
|
|
47829
47968
|
const boot = this.spawnGame(godotPath, cmdArgs, needsItsOwnProcess(asked) ? environmentFor({ ...asked, runtimeDirectory: runtimeDirectory() }) : undefined);
|
|
47830
47969
|
const hung = await new Promise((resolve) => {
|
|
47831
47970
|
const timer = setTimeout(() => {
|
|
@@ -47853,9 +47992,11 @@ class GodotServer {
|
|
|
47853
47992
|
frames,
|
|
47854
47993
|
errors,
|
|
47855
47994
|
warnings,
|
|
47995
|
+
arguments: cmdArgs,
|
|
47996
|
+
argsNote: engineFlagsNote(userArgs),
|
|
47856
47997
|
...scanWaitAnswer(scanned),
|
|
47857
47998
|
savesNote: asked.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
|
|
47858
|
-
...foldedEntriesAnswer(boot.log)
|
|
47999
|
+
...foldedEntriesAnswer(boot.log, engineArgs.includes("--verbose") ? "info" : "warning")
|
|
47859
48000
|
});
|
|
47860
48001
|
}
|
|
47861
48002
|
async handleRunProfile(args) {
|
|
@@ -49261,6 +49402,7 @@ var init_server = __esm(() => {
|
|
|
49261
49402
|
init_issues();
|
|
49262
49403
|
init_junit();
|
|
49263
49404
|
init_keeper_channel();
|
|
49405
|
+
init_launch();
|
|
49264
49406
|
init_lsp_client();
|
|
49265
49407
|
init_outside();
|
|
49266
49408
|
init_paths();
|
package/build/index.js
CHANGED
|
@@ -24913,6 +24913,62 @@ function resolveSilent(requested, variables) {
|
|
|
24913
24913
|
const said = envValue("GDHARNESS_SILENT", variables)?.toLowerCase();
|
|
24914
24914
|
return said !== undefined && said !== "0" && said !== "false";
|
|
24915
24915
|
}
|
|
24916
|
+
var ENGINE_FLAGS = Object.freeze({
|
|
24917
|
+
"--verbose": "none",
|
|
24918
|
+
"--quiet": "none",
|
|
24919
|
+
"--debug-collisions": "none",
|
|
24920
|
+
"--debug-paths": "none",
|
|
24921
|
+
"--debug-navigation": "none",
|
|
24922
|
+
"--debug-avoidance": "none",
|
|
24923
|
+
"--debug-stringnames": "none",
|
|
24924
|
+
"--debug-canvas-item-redraw": "none",
|
|
24925
|
+
"--debug-mute-audio": "none",
|
|
24926
|
+
"--print-fps": "none",
|
|
24927
|
+
"--gpu-profile": "none",
|
|
24928
|
+
"--gpu-validation": "none",
|
|
24929
|
+
"--gpu-abort": "none",
|
|
24930
|
+
"--disable-vsync": "none",
|
|
24931
|
+
"--frame-delay": "number",
|
|
24932
|
+
"--time-scale": "number",
|
|
24933
|
+
"--fixed-fps": "number",
|
|
24934
|
+
"--max-fps": "number",
|
|
24935
|
+
"--resolution": "size"
|
|
24936
|
+
});
|
|
24937
|
+
var SET_BY_ARGUMENT = Object.freeze({
|
|
24938
|
+
"--headless": "headless",
|
|
24939
|
+
"--audio-driver": "silent",
|
|
24940
|
+
"--quit-after": "frames, on editor_run check"
|
|
24941
|
+
});
|
|
24942
|
+
function engineFlags(given) {
|
|
24943
|
+
const offered = Object.keys(ENGINE_FLAGS).join(", ");
|
|
24944
|
+
for (let at = 0;at < given.length; at += 1) {
|
|
24945
|
+
const flag = given[at] ?? "";
|
|
24946
|
+
const takes = Object.hasOwn(ENGINE_FLAGS, flag) ? ENGINE_FLAGS[flag] : undefined;
|
|
24947
|
+
if (takes === undefined) {
|
|
24948
|
+
const setBy = Object.hasOwn(SET_BY_ARGUMENT, flag) ? SET_BY_ARGUMENT[flag] : undefined;
|
|
24949
|
+
return {
|
|
24950
|
+
ok: false,
|
|
24951
|
+
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"].`
|
|
24952
|
+
};
|
|
24953
|
+
}
|
|
24954
|
+
if (takes === "none") {
|
|
24955
|
+
continue;
|
|
24956
|
+
}
|
|
24957
|
+
const value = given[at + 1];
|
|
24958
|
+
const fits = value !== undefined && (takes === "number" ? /^\d+(?:\.\d+)?$/.test(value) : /^\d+x\d+$/.test(value));
|
|
24959
|
+
if (!fits) {
|
|
24960
|
+
return {
|
|
24961
|
+
ok: false,
|
|
24962
|
+
reason: `${flag} takes ${takes === "number" ? "a number" : "a size such as 1280x720"} as the next entry, and ${value === undefined ? "nothing followed it" : `${JSON.stringify(value)} is not one`}.`
|
|
24963
|
+
};
|
|
24964
|
+
}
|
|
24965
|
+
at += 1;
|
|
24966
|
+
}
|
|
24967
|
+
return { ok: true };
|
|
24968
|
+
}
|
|
24969
|
+
function engineFlagsAmongGameArgs(args) {
|
|
24970
|
+
return args.filter((one) => Object.hasOwn(ENGINE_FLAGS, one) || Object.hasOwn(SET_BY_ARGUMENT, one));
|
|
24971
|
+
}
|
|
24916
24972
|
function runArguments(options) {
|
|
24917
24973
|
const args = options.headless ? ["--headless", "--path", options.projectPath] : ["--path", options.projectPath];
|
|
24918
24974
|
if (options.silent === true && !options.headless) {
|
|
@@ -24921,6 +24977,9 @@ function runArguments(options) {
|
|
|
24921
24977
|
if (options.quitAfter !== undefined && options.quitAfter !== null) {
|
|
24922
24978
|
args.push("--quit-after", String(options.quitAfter));
|
|
24923
24979
|
}
|
|
24980
|
+
if (options.engineArgs !== undefined) {
|
|
24981
|
+
args.push(...options.engineArgs);
|
|
24982
|
+
}
|
|
24924
24983
|
if (options.scene !== null) {
|
|
24925
24984
|
args.push(`res://${options.scene}`);
|
|
24926
24985
|
}
|
|
@@ -33162,18 +33221,41 @@ function restartSettled(projectPath) {
|
|
|
33162
33221
|
// src/run-record.ts
|
|
33163
33222
|
import { execFile as execFile4 } from "node:child_process";
|
|
33164
33223
|
import { createHash as createHash3 } from "node:crypto";
|
|
33165
|
-
import {
|
|
33166
|
-
mkdirSync as mkdirSync11,
|
|
33167
|
-
openSync as openSync2,
|
|
33168
|
-
readdirSync as readdirSync7,
|
|
33169
|
-
readFileSync as readFileSync18,
|
|
33170
|
-
realpathSync as realpathSync6,
|
|
33171
|
-
rmSync as rmSync9,
|
|
33172
|
-
statSync as statSync6,
|
|
33173
|
-
writeFileSync as writeFileSync13
|
|
33174
|
-
} from "node:fs";
|
|
33224
|
+
import { mkdirSync as mkdirSync11, openSync as openSync2, readdirSync as readdirSync7, readFileSync as readFileSync18, realpathSync as realpathSync6, rmSync as rmSync10, statSync as statSync6 } from "node:fs";
|
|
33175
33225
|
import { basename as basename4, dirname as dirname13, join as join19, resolve as resolve6 } from "node:path";
|
|
33176
33226
|
import { promisify as promisify4 } from "node:util";
|
|
33227
|
+
|
|
33228
|
+
// src/whole-file.ts
|
|
33229
|
+
import { renameSync as renameSync2, rmSync as rmSync9, writeFileSync as writeFileSync13 } from "node:fs";
|
|
33230
|
+
var HELD_FILE_MS = 1000;
|
|
33231
|
+
var HELD_FILE_RETRY_MS = 10;
|
|
33232
|
+
var HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
33233
|
+
function writeWhole(path, text) {
|
|
33234
|
+
const partial = `${path}.${process.pid}.partial`;
|
|
33235
|
+
try {
|
|
33236
|
+
writeFileSync13(partial, text, "utf8");
|
|
33237
|
+
} catch {
|
|
33238
|
+
return false;
|
|
33239
|
+
}
|
|
33240
|
+
const until = Date.now() + HELD_FILE_MS;
|
|
33241
|
+
for (;; ) {
|
|
33242
|
+
try {
|
|
33243
|
+
renameSync2(partial, path);
|
|
33244
|
+
return true;
|
|
33245
|
+
} catch (error) {
|
|
33246
|
+
const code = error.code ?? "";
|
|
33247
|
+
if (!HELD_BY_A_READER.has(code) || Date.now() >= until) {
|
|
33248
|
+
try {
|
|
33249
|
+
rmSync9(partial, { force: true });
|
|
33250
|
+
} catch {}
|
|
33251
|
+
return false;
|
|
33252
|
+
}
|
|
33253
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, HELD_FILE_RETRY_MS);
|
|
33254
|
+
}
|
|
33255
|
+
}
|
|
33256
|
+
}
|
|
33257
|
+
|
|
33258
|
+
// src/run-record.ts
|
|
33177
33259
|
var execFileAsync = promisify4(execFile4);
|
|
33178
33260
|
function runsDirectory() {
|
|
33179
33261
|
return join19(runtimeDirectory(), "runs");
|
|
@@ -33208,7 +33290,7 @@ function latestRunPath(servedBy, directory = runtimeDirectory()) {
|
|
|
33208
33290
|
function writeLatestRun(servedBy, latest) {
|
|
33209
33291
|
try {
|
|
33210
33292
|
mkdirSync11(runsDirectory(), { recursive: true });
|
|
33211
|
-
|
|
33293
|
+
writeWhole(latestRunPath(servedBy), JSON.stringify(latest, null, 2));
|
|
33212
33294
|
} catch {}
|
|
33213
33295
|
}
|
|
33214
33296
|
function readLatestRun(servedBy, directory = runtimeDirectory()) {
|
|
@@ -33243,7 +33325,10 @@ function editorNotePath(projectPath, directory = runtimeDirectory()) {
|
|
|
33243
33325
|
}
|
|
33244
33326
|
function writeEditorRunNote(note) {
|
|
33245
33327
|
mkdirSync11(runsDirectory(), { recursive: true });
|
|
33246
|
-
|
|
33328
|
+
const path = editorNotePath(note.projectPath);
|
|
33329
|
+
if (!writeWhole(path, JSON.stringify(note, null, 2))) {
|
|
33330
|
+
throw new Error(`the editor run note could not be written at ${path}`);
|
|
33331
|
+
}
|
|
33247
33332
|
}
|
|
33248
33333
|
function readEditorRunNote(projectPath) {
|
|
33249
33334
|
for (const directory of runtimeDirectories()) {
|
|
@@ -33515,7 +33600,7 @@ function clearRunRecord(projectPath) {
|
|
|
33515
33600
|
const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
|
|
33516
33601
|
for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
|
|
33517
33602
|
try {
|
|
33518
|
-
|
|
33603
|
+
rmSync10(path, { force: true });
|
|
33519
33604
|
} catch {}
|
|
33520
33605
|
}
|
|
33521
33606
|
}
|
|
@@ -33540,7 +33625,8 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
33540
33625
|
return;
|
|
33541
33626
|
}
|
|
33542
33627
|
for (const entry of entries) {
|
|
33543
|
-
|
|
33628
|
+
const transcript = entry.startsWith("run-") && (entry.endsWith(".log") || entry.endsWith(".profile.json"));
|
|
33629
|
+
if (!transcript && !entry.endsWith(".partial")) {
|
|
33544
33630
|
continue;
|
|
33545
33631
|
}
|
|
33546
33632
|
const path = join19(directory, entry);
|
|
@@ -33548,7 +33634,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
33548
33634
|
continue;
|
|
33549
33635
|
}
|
|
33550
33636
|
try {
|
|
33551
|
-
|
|
33637
|
+
rmSync10(path);
|
|
33552
33638
|
} catch {}
|
|
33553
33639
|
}
|
|
33554
33640
|
}
|
|
@@ -33647,7 +33733,7 @@ function withoutListed(entries, errors) {
|
|
|
33647
33733
|
}
|
|
33648
33734
|
|
|
33649
33735
|
// src/script-profile.ts
|
|
33650
|
-
import { readFileSync as readFileSync19
|
|
33736
|
+
import { readFileSync as readFileSync19 } from "node:fs";
|
|
33651
33737
|
|
|
33652
33738
|
// src/godot-variant.ts
|
|
33653
33739
|
var FLAG_64 = 1 << 16;
|
|
@@ -34767,7 +34853,13 @@ var TOOL_SPECS = [
|
|
|
34767
34853
|
type: "array",
|
|
34768
34854
|
items: { type: "string" },
|
|
34769
34855
|
ops: ["start", "check"],
|
|
34770
|
-
description: `The game's own arguments, what OS.get_cmdline_user_args() answers, such as ["--level=2"]. The separator is added here. A run with any is started by this server rather than by the editor, which fixes the game's command line when it opens the project, so the debug_* tools do not answer for it.`
|
|
34856
|
+
description: `The game's own arguments, what OS.get_cmdline_user_args() answers, such as ["--level=2"]. The separator is added here, so a flag the engine reads goes to the game instead, and the answer says so under argsNote. A run with any is started by this server rather than by the editor, which fixes the game's command line when it opens the project, so the debug_* tools do not answer for it.`
|
|
34857
|
+
},
|
|
34858
|
+
engineArgs: {
|
|
34859
|
+
type: "array",
|
|
34860
|
+
items: { type: "string" },
|
|
34861
|
+
ops: ["start", "check"],
|
|
34862
|
+
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.`
|
|
34771
34863
|
},
|
|
34772
34864
|
headless: { type: "boolean", ops: ["start"], description: "start: force a window or no window." },
|
|
34773
34865
|
visible: {
|
|
@@ -34864,7 +34956,7 @@ var TOOL_SPECS = [
|
|
|
34864
34956
|
requires: []
|
|
34865
34957
|
}
|
|
34866
34958
|
},
|
|
34867
|
-
answer: "stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked.",
|
|
34959
|
+
answer: "stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. arguments is the engine command line the check ran, as a start's is. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked.",
|
|
34868
34960
|
defaultOperation: "start"
|
|
34869
34961
|
},
|
|
34870
34962
|
{
|
|
@@ -35558,9 +35650,9 @@ import {
|
|
|
35558
35650
|
readdirSync as readdirSync8,
|
|
35559
35651
|
readFileSync as readFileSync20,
|
|
35560
35652
|
rmdirSync,
|
|
35561
|
-
rmSync as
|
|
35653
|
+
rmSync as rmSync11,
|
|
35562
35654
|
statSync as statSync7,
|
|
35563
|
-
writeFileSync as
|
|
35655
|
+
writeFileSync as writeFileSync14
|
|
35564
35656
|
} from "node:fs";
|
|
35565
35657
|
import { dirname as dirname14, join as join21 } from "node:path";
|
|
35566
35658
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
@@ -36317,6 +36409,14 @@ var PLAYING_STATUS_MS = 1000;
|
|
|
36317
36409
|
var SCAN_START_MS = 2000;
|
|
36318
36410
|
var SCAN_SETTLED_MS = 500;
|
|
36319
36411
|
var REIMPORT_TIMEOUT_MS = 300000;
|
|
36412
|
+
function engineFlagsNote(args) {
|
|
36413
|
+
const among = engineFlagsAmongGameArgs(args);
|
|
36414
|
+
if (among.length === 0) {
|
|
36415
|
+
return;
|
|
36416
|
+
}
|
|
36417
|
+
const named = among.join(", ");
|
|
36418
|
+
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.`;
|
|
36419
|
+
}
|
|
36320
36420
|
var OPS_ELSEWHERE = {
|
|
36321
36421
|
runtime_invoke: { get: "runtime_inspect property", property: "runtime_inspect property" }
|
|
36322
36422
|
};
|
|
@@ -36747,9 +36847,9 @@ function callersAnswer(totals, asked) {
|
|
|
36747
36847
|
};
|
|
36748
36848
|
}
|
|
36749
36849
|
var MOST_ENGINE_ENTRIES = 200;
|
|
36750
|
-
function foldedEntriesAnswer(log) {
|
|
36850
|
+
function foldedEntriesAnswer(log, floor = "warning") {
|
|
36751
36851
|
const problems = log.selectFolded({
|
|
36752
|
-
severity:
|
|
36852
|
+
severity: floor,
|
|
36753
36853
|
sinceLastCall: false,
|
|
36754
36854
|
limit: MOST_ENGINE_ENTRIES
|
|
36755
36855
|
});
|
|
@@ -37777,6 +37877,25 @@ class GodotServer {
|
|
|
37777
37877
|
}
|
|
37778
37878
|
return { ok: true, value: given };
|
|
37779
37879
|
}
|
|
37880
|
+
engineArguments(args) {
|
|
37881
|
+
if (args["engineArgs"] === undefined) {
|
|
37882
|
+
return { ok: true, value: [] };
|
|
37883
|
+
}
|
|
37884
|
+
const given = readStringArray(args, "engineArgs");
|
|
37885
|
+
if (given === undefined) {
|
|
37886
|
+
return {
|
|
37887
|
+
ok: false,
|
|
37888
|
+
response: this.createErrorResponse("engineArgs must be a list of strings.", [
|
|
37889
|
+
'Pass engine flags, such as ["--verbose"] or ["--fixed-fps", "60"]'
|
|
37890
|
+
])
|
|
37891
|
+
};
|
|
37892
|
+
}
|
|
37893
|
+
const checked = engineFlags(given);
|
|
37894
|
+
if (!checked.ok) {
|
|
37895
|
+
return { ok: false, response: this.createErrorResponse(checked.reason) };
|
|
37896
|
+
}
|
|
37897
|
+
return { ok: true, value: given };
|
|
37898
|
+
}
|
|
37780
37899
|
runEnvironment(args) {
|
|
37781
37900
|
const savesIn = readNonEmptyString(args, "savesIn");
|
|
37782
37901
|
if (savesIn !== undefined && !isAbsolute3(savesIn)) {
|
|
@@ -39593,6 +39712,10 @@ class GodotServer {
|
|
|
39593
39712
|
if (!given.ok) {
|
|
39594
39713
|
return given.response;
|
|
39595
39714
|
}
|
|
39715
|
+
const flags = this.engineArguments(args);
|
|
39716
|
+
if (!flags.ok) {
|
|
39717
|
+
return flags.response;
|
|
39718
|
+
}
|
|
39596
39719
|
const asked = this.runEnvironment(args);
|
|
39597
39720
|
if (!asked.ok) {
|
|
39598
39721
|
return asked.response;
|
|
@@ -39614,7 +39737,7 @@ class GodotServer {
|
|
|
39614
39737
|
}
|
|
39615
39738
|
const sceneArgument = sceneToRun?.ok ? sceneToRun.relativePath : null;
|
|
39616
39739
|
if (op === "check") {
|
|
39617
|
-
return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value, scanned, asked.value);
|
|
39740
|
+
return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value, scanned, asked.value, flags.value);
|
|
39618
39741
|
}
|
|
39619
39742
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
39620
39743
|
let ended = null;
|
|
@@ -39641,7 +39764,7 @@ class GodotServer {
|
|
|
39641
39764
|
const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
|
|
39642
39765
|
const silent = resolveSilent(args["silent"], process.env) && !headless;
|
|
39643
39766
|
const profiled = readBoolean(args, "profile") === true;
|
|
39644
|
-
if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && !needsItsOwnProcess(asked.value) && !silent) {
|
|
39767
|
+
if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && flags.value.length === 0 && !needsItsOwnProcess(asked.value) && !silent) {
|
|
39645
39768
|
return await this.playThroughEditor(sceneArgument, refreshed.value, scanned, project.value.path, alreadyPlaying, runtimeWaitMs, ended, profiled);
|
|
39646
39769
|
}
|
|
39647
39770
|
const cmdArgs = runArguments({
|
|
@@ -39649,6 +39772,7 @@ class GodotServer {
|
|
|
39649
39772
|
headless,
|
|
39650
39773
|
scene: sceneArgument,
|
|
39651
39774
|
userArgs: given.value,
|
|
39775
|
+
engineArgs: flags.value,
|
|
39652
39776
|
silent
|
|
39653
39777
|
});
|
|
39654
39778
|
this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
@@ -39661,7 +39785,7 @@ class GodotServer {
|
|
|
39661
39785
|
started.announcedBefore = alreadyPlaying;
|
|
39662
39786
|
started.bootScene = runtimeWaitMs.scene;
|
|
39663
39787
|
this.activeProcess = started;
|
|
39664
|
-
const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && given.value.length > 0 ? " The editor cannot be handed arguments for a game it plays, so this one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && needsItsOwnProcess(asked.value) ? " The editor plays a game with its own environment, so a run given one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && silent ? " The editor cannot play a game silent, which takes an engine flag, so this one was " + `started here${args["silent"] === true ? "" : ", silent because GDHARNESS_SILENT says so"}: the debug_* tools answer only for a game the editor is playing, and silent: false plays it through the editor.` : "";
|
|
39788
|
+
const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && (given.value.length > 0 || flags.value.length > 0) ? " The editor cannot be handed arguments for a game it plays, so this one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && needsItsOwnProcess(asked.value) ? " The editor plays a game with its own environment, so a run given one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && silent ? " The editor cannot play a game silent, which takes an engine flag, so this one was " + `started here${args["silent"] === true ? "" : ", silent because GDHARNESS_SILENT says so"}: the debug_* tools answer only for a game the editor is playing, and silent: false plays it through the editor.` : "";
|
|
39665
39789
|
const endedForThis = endedToStartThis(ended);
|
|
39666
39790
|
const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
|
|
39667
39791
|
const alsoRunning = leftRunningNote(stranded);
|
|
@@ -39671,6 +39795,7 @@ class GodotServer {
|
|
|
39671
39795
|
alsoRunning: stranded.length === 0 ? undefined : stranded.map((one) => one.pid),
|
|
39672
39796
|
pid: started.pid,
|
|
39673
39797
|
arguments: cmdArgs,
|
|
39798
|
+
argsNote: engineFlagsNote(given.value),
|
|
39674
39799
|
transcript: started.transcript,
|
|
39675
39800
|
profile: profileOnStart(profiled, "gdharness", runtimeAddonRegistered(project.value.path)),
|
|
39676
39801
|
refreshedClasses: refreshed.value,
|
|
@@ -40573,10 +40698,17 @@ class GodotServer {
|
|
|
40573
40698
|
async currentRun() {
|
|
40574
40699
|
return this.activeProcess ?? await this.adoptRecordedRun();
|
|
40575
40700
|
}
|
|
40576
|
-
async checkBoot(godotPath, projectPath, scene, args, userArgs, scanned, asked = {}) {
|
|
40701
|
+
async checkBoot(godotPath, projectPath, scene, args, userArgs, scanned, asked = {}, engineArgs = []) {
|
|
40577
40702
|
const frames = readPositiveNumber(args, "frames") ?? 3;
|
|
40578
40703
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 60000;
|
|
40579
|
-
const cmdArgs = runArguments({
|
|
40704
|
+
const cmdArgs = runArguments({
|
|
40705
|
+
projectPath,
|
|
40706
|
+
headless: true,
|
|
40707
|
+
scene,
|
|
40708
|
+
quitAfter: frames,
|
|
40709
|
+
userArgs,
|
|
40710
|
+
engineArgs
|
|
40711
|
+
});
|
|
40580
40712
|
const boot = this.spawnGame(godotPath, cmdArgs, needsItsOwnProcess(asked) ? environmentFor({ ...asked, runtimeDirectory: runtimeDirectory() }) : undefined);
|
|
40581
40713
|
const hung = await new Promise((resolve) => {
|
|
40582
40714
|
const timer = setTimeout(() => {
|
|
@@ -40604,9 +40736,11 @@ class GodotServer {
|
|
|
40604
40736
|
frames,
|
|
40605
40737
|
errors,
|
|
40606
40738
|
warnings,
|
|
40739
|
+
arguments: cmdArgs,
|
|
40740
|
+
argsNote: engineFlagsNote(userArgs),
|
|
40607
40741
|
...scanWaitAnswer(scanned),
|
|
40608
40742
|
savesNote: asked.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
|
|
40609
|
-
...foldedEntriesAnswer(boot.log)
|
|
40743
|
+
...foldedEntriesAnswer(boot.log, engineArgs.includes("--verbose") ? "info" : "warning")
|
|
40610
40744
|
});
|
|
40611
40745
|
}
|
|
40612
40746
|
async handleRunProfile(args) {
|
package/build/keeper.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/keeper.ts
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
-
import { closeSync, existsSync as existsSync2, mkdtempSync as mkdtempSync3, openSync as openSync2, readFileSync as
|
|
5
|
+
import { closeSync, existsSync as existsSync2, mkdtempSync as mkdtempSync3, openSync as openSync2, readFileSync as readFileSync2, rmSync as rmSync5, writeFileSync as writeFileSync3 } from "node:fs";
|
|
6
6
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
7
7
|
import { join as join5 } from "node:path";
|
|
8
8
|
import { setTimeout as delay } from "node:timers/promises";
|
|
@@ -230,6 +230,14 @@ import { connect, createServer } from "node:net";
|
|
|
230
230
|
import { tmpdir } from "node:os";
|
|
231
231
|
import { join } from "node:path";
|
|
232
232
|
import process2 from "node:process";
|
|
233
|
+
function endedOrWhyNot(end) {
|
|
234
|
+
try {
|
|
235
|
+
return end();
|
|
236
|
+
} catch (error) {
|
|
237
|
+
return `could not end it: ${error instanceof Error ? error.message : String(error)}`.replaceAll(`
|
|
238
|
+
`, " ");
|
|
239
|
+
}
|
|
240
|
+
}
|
|
233
241
|
async function listenForAStop(end) {
|
|
234
242
|
let directory = null;
|
|
235
243
|
try {
|
|
@@ -244,7 +252,7 @@ async function listenForAStop(end) {
|
|
|
244
252
|
if (asked.includes(`
|
|
245
253
|
`)) {
|
|
246
254
|
socket.end(asked.startsWith(`stop
|
|
247
|
-
`) ? `${end
|
|
255
|
+
`) ? `${endedOrWhyNot(end)}
|
|
248
256
|
` : `unknown
|
|
249
257
|
`);
|
|
250
258
|
}
|
|
@@ -315,16 +323,7 @@ function readLaunched(printed) {
|
|
|
315
323
|
// src/run-record.ts
|
|
316
324
|
import { execFile as execFile2 } from "node:child_process";
|
|
317
325
|
import { createHash as createHash2 } from "node:crypto";
|
|
318
|
-
import {
|
|
319
|
-
mkdirSync,
|
|
320
|
-
openSync,
|
|
321
|
-
readdirSync,
|
|
322
|
-
readFileSync,
|
|
323
|
-
realpathSync,
|
|
324
|
-
rmSync as rmSync2,
|
|
325
|
-
statSync,
|
|
326
|
-
writeFileSync as writeFileSync2
|
|
327
|
-
} from "node:fs";
|
|
326
|
+
import { mkdirSync, openSync, readdirSync, readFileSync, realpathSync, rmSync as rmSync3, statSync } from "node:fs";
|
|
328
327
|
import { basename, dirname as dirname2, join as join3, resolve as resolve2 } from "node:path";
|
|
329
328
|
import { promisify as promisify2 } from "node:util";
|
|
330
329
|
|
|
@@ -364,6 +363,32 @@ function envValue(name, variables = process.env) {
|
|
|
364
363
|
const value = variables[name];
|
|
365
364
|
return value === undefined || value === "" ? undefined : value;
|
|
366
365
|
}
|
|
366
|
+
var ENGINE_FLAGS = Object.freeze({
|
|
367
|
+
"--verbose": "none",
|
|
368
|
+
"--quiet": "none",
|
|
369
|
+
"--debug-collisions": "none",
|
|
370
|
+
"--debug-paths": "none",
|
|
371
|
+
"--debug-navigation": "none",
|
|
372
|
+
"--debug-avoidance": "none",
|
|
373
|
+
"--debug-stringnames": "none",
|
|
374
|
+
"--debug-canvas-item-redraw": "none",
|
|
375
|
+
"--debug-mute-audio": "none",
|
|
376
|
+
"--print-fps": "none",
|
|
377
|
+
"--gpu-profile": "none",
|
|
378
|
+
"--gpu-validation": "none",
|
|
379
|
+
"--gpu-abort": "none",
|
|
380
|
+
"--disable-vsync": "none",
|
|
381
|
+
"--frame-delay": "number",
|
|
382
|
+
"--time-scale": "number",
|
|
383
|
+
"--fixed-fps": "number",
|
|
384
|
+
"--max-fps": "number",
|
|
385
|
+
"--resolution": "size"
|
|
386
|
+
});
|
|
387
|
+
var SET_BY_ARGUMENT = Object.freeze({
|
|
388
|
+
"--headless": "headless",
|
|
389
|
+
"--audio-driver": "silent",
|
|
390
|
+
"--quit-after": "frames, on editor_run check"
|
|
391
|
+
});
|
|
367
392
|
|
|
368
393
|
// src/runtime-client.ts
|
|
369
394
|
function runtimeDirectory(variables = process.env) {
|
|
@@ -379,6 +404,36 @@ var verdicts = new Map;
|
|
|
379
404
|
var LATE_ANSWER_MS = 30 * 60000;
|
|
380
405
|
var lateAnswers = new Map;
|
|
381
406
|
|
|
407
|
+
// src/whole-file.ts
|
|
408
|
+
import { renameSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
409
|
+
var HELD_FILE_MS = 1000;
|
|
410
|
+
var HELD_FILE_RETRY_MS = 10;
|
|
411
|
+
var HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
412
|
+
function writeWhole(path, text) {
|
|
413
|
+
const partial = `${path}.${process.pid}.partial`;
|
|
414
|
+
try {
|
|
415
|
+
writeFileSync2(partial, text, "utf8");
|
|
416
|
+
} catch {
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
const until = Date.now() + HELD_FILE_MS;
|
|
420
|
+
for (;; ) {
|
|
421
|
+
try {
|
|
422
|
+
renameSync(partial, path);
|
|
423
|
+
return true;
|
|
424
|
+
} catch (error) {
|
|
425
|
+
const code = error.code ?? "";
|
|
426
|
+
if (!HELD_BY_A_READER.has(code) || Date.now() >= until) {
|
|
427
|
+
try {
|
|
428
|
+
rmSync2(partial, { force: true });
|
|
429
|
+
} catch {}
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, HELD_FILE_RETRY_MS);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
382
437
|
// src/run-record.ts
|
|
383
438
|
var execFileAsync = promisify2(execFile2);
|
|
384
439
|
function runsDirectory() {
|
|
@@ -410,7 +465,10 @@ function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
|
410
465
|
}
|
|
411
466
|
function writeRunRecord(record) {
|
|
412
467
|
mkdirSync(runsDirectory(), { recursive: true });
|
|
413
|
-
|
|
468
|
+
const path = runRecordPath(record.projectPath);
|
|
469
|
+
if (!writeWhole(path, JSON.stringify(record, null, 2))) {
|
|
470
|
+
throw new Error(`the run note could not be written at ${path}`);
|
|
471
|
+
}
|
|
414
472
|
}
|
|
415
473
|
function recordRunEnded(projectPath, pid, ending) {
|
|
416
474
|
const path = runRecordPath(projectPath);
|
|
@@ -424,9 +482,7 @@ function recordRunEnded(projectPath, pid, ending) {
|
|
|
424
482
|
...ending.exitSignal === null ? {} : { exitSignal: ending.exitSignal },
|
|
425
483
|
endedAt: Date.now()
|
|
426
484
|
};
|
|
427
|
-
|
|
428
|
-
writeFileSync2(path, JSON.stringify(written, null, 2), "utf8");
|
|
429
|
-
} catch {}
|
|
485
|
+
writeWhole(path, JSON.stringify(written, null, 2));
|
|
430
486
|
}
|
|
431
487
|
function recordAt(path) {
|
|
432
488
|
let parsed;
|
|
@@ -464,7 +520,7 @@ function recordAt(path) {
|
|
|
464
520
|
}
|
|
465
521
|
|
|
466
522
|
// src/scratch.ts
|
|
467
|
-
import { existsSync, mkdtempSync as mkdtempSync2, readdirSync as readdirSync2, rmSync as
|
|
523
|
+
import { existsSync, mkdtempSync as mkdtempSync2, readdirSync as readdirSync2, rmSync as rmSync4 } from "node:fs";
|
|
468
524
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
469
525
|
import { join as join4 } from "node:path";
|
|
470
526
|
import process3 from "node:process";
|
|
@@ -497,7 +553,7 @@ function discard(...paths) {
|
|
|
497
553
|
return left;
|
|
498
554
|
}
|
|
499
555
|
function remove(path) {
|
|
500
|
-
|
|
556
|
+
rmSync4(path, SWEPT);
|
|
501
557
|
}
|
|
502
558
|
function tryAgain(path, attempt) {
|
|
503
559
|
if (attempt === 0) {
|
|
@@ -541,7 +597,6 @@ function lastTryOnExit() {
|
|
|
541
597
|
}
|
|
542
598
|
|
|
543
599
|
// src/script-profile.ts
|
|
544
|
-
import { readFileSync as readFileSync2, renameSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
545
600
|
import { createServer as createServer2 } from "node:net";
|
|
546
601
|
|
|
547
602
|
// src/gdscript-source.ts
|
|
@@ -1073,9 +1128,7 @@ function parseIniLikeValue(value) {
|
|
|
1073
1128
|
|
|
1074
1129
|
// src/script-profile.ts
|
|
1075
1130
|
function writeProfileFile(path, file) {
|
|
1076
|
-
|
|
1077
|
-
writeFileSync3(partial, JSON.stringify(file), "utf8");
|
|
1078
|
-
renameSync(partial, path);
|
|
1131
|
+
return writeWhole(path, JSON.stringify(file));
|
|
1079
1132
|
}
|
|
1080
1133
|
var QUEUE_SETTING = "limits/debugger/max_queued_messages";
|
|
1081
1134
|
var QUEUE_DEFAULT = 2048;
|
|
@@ -1315,12 +1368,12 @@ async function startThroughHelper(spec, output, stopFile) {
|
|
|
1315
1368
|
exited.now = true;
|
|
1316
1369
|
});
|
|
1317
1370
|
for (let waited = 0;waited < DESKTOP_START_MS; waited += 50) {
|
|
1318
|
-
const pid = existsSync2(pidFile) ? Number(
|
|
1371
|
+
const pid = existsSync2(pidFile) ? Number(readFileSync2(pidFile, "utf8").trim()) : Number.NaN;
|
|
1319
1372
|
if (Number.isInteger(pid) && pid > 0) {
|
|
1320
1373
|
return { child: started.child, pid };
|
|
1321
1374
|
}
|
|
1322
1375
|
if (exited.now) {
|
|
1323
|
-
const why = existsSync2(errorFile) ?
|
|
1376
|
+
const why = existsSync2(errorFile) ? readFileSync2(errorFile, "utf8").trim() : "it exited saying nothing";
|
|
1324
1377
|
return { error: `it could not be started ${where}: ${why}` };
|
|
1325
1378
|
}
|
|
1326
1379
|
await delay(50);
|
|
@@ -1332,18 +1385,19 @@ async function startThroughHelper(spec, output, stopFile) {
|
|
|
1332
1385
|
}
|
|
1333
1386
|
}
|
|
1334
1387
|
var PROFILE_WRITE_MS = 500;
|
|
1388
|
+
var LAST_WRITE_TRIES = 5;
|
|
1335
1389
|
async function profiledBy(path, projectPath) {
|
|
1336
1390
|
let latest = null;
|
|
1337
1391
|
let timer = null;
|
|
1338
1392
|
const write = () => {
|
|
1339
1393
|
timer = null;
|
|
1340
|
-
if (latest !== null) {
|
|
1341
|
-
|
|
1394
|
+
if (latest !== null && !writeProfileFile(path, { ...latest, connected: true })) {
|
|
1395
|
+
timer = setTimeout(write, PROFILE_WRITE_MS);
|
|
1342
1396
|
}
|
|
1343
1397
|
};
|
|
1344
1398
|
let projectGodot = null;
|
|
1345
1399
|
try {
|
|
1346
|
-
projectGodot =
|
|
1400
|
+
projectGodot = readFileSync2(join5(projectPath, "project.godot"), "utf8");
|
|
1347
1401
|
} catch {}
|
|
1348
1402
|
const listener = await ProfileListener.open((totals) => {
|
|
1349
1403
|
const first = latest === null;
|
|
@@ -1362,7 +1416,10 @@ async function profiledBy(path, projectPath) {
|
|
|
1362
1416
|
clearTimeout(timer);
|
|
1363
1417
|
}
|
|
1364
1418
|
latest = listener.totals() ?? latest;
|
|
1365
|
-
|
|
1419
|
+
let tries = 0;
|
|
1420
|
+
while (latest !== null && !writeProfileFile(path, { ...latest, connected: true }) && tries < LAST_WRITE_TRIES) {
|
|
1421
|
+
tries += 1;
|
|
1422
|
+
}
|
|
1366
1423
|
listener.close();
|
|
1367
1424
|
}
|
|
1368
1425
|
};
|
|
@@ -1427,6 +1484,11 @@ async function launch() {
|
|
|
1427
1484
|
process.exit(0);
|
|
1428
1485
|
});
|
|
1429
1486
|
}
|
|
1487
|
+
function writeNoteIfItCan(record) {
|
|
1488
|
+
try {
|
|
1489
|
+
writeRunRecord(record);
|
|
1490
|
+
} catch {}
|
|
1491
|
+
}
|
|
1430
1492
|
async function keep() {
|
|
1431
1493
|
const spec = await receivedSpec();
|
|
1432
1494
|
const run = spec.run;
|
|
@@ -1467,7 +1529,7 @@ async function keep() {
|
|
|
1467
1529
|
command: spec.command
|
|
1468
1530
|
};
|
|
1469
1531
|
if (record !== null) {
|
|
1470
|
-
|
|
1532
|
+
writeNoteIfItCan(record);
|
|
1471
1533
|
}
|
|
1472
1534
|
let listening = null;
|
|
1473
1535
|
child.once("exit", (code, signal) => {
|
|
@@ -1477,20 +1539,20 @@ async function keep() {
|
|
|
1477
1539
|
profiled?.finish();
|
|
1478
1540
|
listening?.close();
|
|
1479
1541
|
if (own !== null) {
|
|
1480
|
-
|
|
1542
|
+
rmSync5(own, { recursive: true, force: true });
|
|
1481
1543
|
}
|
|
1482
1544
|
process.exit(0);
|
|
1483
1545
|
});
|
|
1484
1546
|
if (record !== null) {
|
|
1485
1547
|
listening = await listenForAStop(() => {
|
|
1486
1548
|
if (stopFile !== "") {
|
|
1487
|
-
|
|
1549
|
+
writeFileSync3(stopFile, "");
|
|
1488
1550
|
return "signalled";
|
|
1489
1551
|
}
|
|
1490
1552
|
return child.kill() ? "signalled" : "gone";
|
|
1491
1553
|
});
|
|
1492
1554
|
if (listening !== null) {
|
|
1493
|
-
|
|
1555
|
+
writeNoteIfItCan({ ...record, keeper: listening.address });
|
|
1494
1556
|
}
|
|
1495
1557
|
}
|
|
1496
1558
|
process.stdout.write(`pid ${pid}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.20",
|
|
4
4
|
"mcpName": "io.github.Aureliolo/gdharness",
|
|
5
5
|
"description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
|
|
6
6
|
"type": "module",
|