gdharness 1.0.28 → 1.0.30
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
CHANGED
|
@@ -16309,7 +16309,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16309
16309
|
},
|
|
16310
16310
|
{
|
|
16311
16311
|
name: "editor_run",
|
|
16312
|
-
description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. args hands the game its own flags, the ones it reads with OS.get_cmdline_user_args(), and a run carrying any is started by this server rather than by the editor. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice. When it is not listening, mayYetAnnounce says whether that is final: false is a runtime that is not coming, true is a game still on its way up, which editor_status will see and runtimeWaitMs waits longer for. A start this server made also answers with transcript, the file both the run's streams are written to, so a watch on it can be armed off the start rather than off a second call.",
|
|
16312
|
+
description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. args hands the game its own flags, the ones it reads with OS.get_cmdline_user_args(), and a run carrying any is started by this server rather than by the editor. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. start and check both wait first for a scan the open editor is running and for the class cache it writes just after, since a game booted in between resolves no global class, as does every other engine this server starts on the project; waitedForEditorScanMs says how long, and scanNote says when a scan ran past thirty seconds and the game started during it. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice. When it is not listening, mayYetAnnounce says whether that is final: false is a runtime that is not coming, true is a game still on its way up, which editor_status will see and runtimeWaitMs waits longer for. A start this server made also answers with transcript, the file both the run's streams are written to, so a watch on it can be armed off the start rather than off a second call.",
|
|
16313
16313
|
parameters: {
|
|
16314
16314
|
projectPath: { ...PROJECT_PATH, ops: ["start", "check"] },
|
|
16315
16315
|
scene: {
|
|
@@ -16426,7 +16426,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16426
16426
|
},
|
|
16427
16427
|
{
|
|
16428
16428
|
name: "editor_status",
|
|
16429
|
-
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: 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. 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. 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. 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. 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.",
|
|
16429
|
+
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: 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. 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. 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. 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. 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.",
|
|
16430
16430
|
parameters: {},
|
|
16431
16431
|
requires: []
|
|
16432
16432
|
},
|
|
@@ -37730,6 +37730,49 @@ var init_lsp_client = __esm(() => {
|
|
|
37730
37730
|
init_ports();
|
|
37731
37731
|
});
|
|
37732
37732
|
|
|
37733
|
+
// src/outside.ts
|
|
37734
|
+
import { spawn } from "node:child_process";
|
|
37735
|
+
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
37736
|
+
function encodeSpec(spec) {
|
|
37737
|
+
return Buffer.from(JSON.stringify(spec), "utf8").toString("base64url");
|
|
37738
|
+
}
|
|
37739
|
+
function readLaunched(printed) {
|
|
37740
|
+
const line = printed.split(`
|
|
37741
|
+
`).find((one) => one.trim() !== "")?.trim() ?? "";
|
|
37742
|
+
const pid = /^pid (\d+)$/.exec(line);
|
|
37743
|
+
if (pid?.[1] !== undefined) {
|
|
37744
|
+
return { pid: Number(pid[1]) };
|
|
37745
|
+
}
|
|
37746
|
+
return { error: line.replace(/^error /, "") || "the launcher said nothing" };
|
|
37747
|
+
}
|
|
37748
|
+
async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = KEEPER_SCRIPT) {
|
|
37749
|
+
return await new Promise((resolve) => {
|
|
37750
|
+
const launcher = spawn(runtime, [keeper, "launch", encodeSpec(spec)], {
|
|
37751
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
37752
|
+
windowsHide: true
|
|
37753
|
+
});
|
|
37754
|
+
let printed = "";
|
|
37755
|
+
let complained = "";
|
|
37756
|
+
launcher.stdout.on("data", (chunk) => {
|
|
37757
|
+
printed += chunk.toString();
|
|
37758
|
+
});
|
|
37759
|
+
launcher.stderr.on("data", (chunk) => {
|
|
37760
|
+
complained += chunk.toString();
|
|
37761
|
+
});
|
|
37762
|
+
launcher.once("error", (error) => {
|
|
37763
|
+
resolve({ error: `the launcher did not start: ${error.message}` });
|
|
37764
|
+
});
|
|
37765
|
+
launcher.once("close", () => {
|
|
37766
|
+
const launched = readLaunched(printed);
|
|
37767
|
+
resolve("error" in launched && complained.trim() !== "" ? { error: `${launched.error}: ${complained.trim()}` } : launched);
|
|
37768
|
+
});
|
|
37769
|
+
});
|
|
37770
|
+
}
|
|
37771
|
+
var KEEPER_SCRIPT;
|
|
37772
|
+
var init_outside = __esm(() => {
|
|
37773
|
+
KEEPER_SCRIPT = fileURLToPath5(new URL("./keeper.js", import.meta.url));
|
|
37774
|
+
});
|
|
37775
|
+
|
|
37733
37776
|
// src/process-time.ts
|
|
37734
37777
|
import { execFile as execFile6 } from "node:child_process";
|
|
37735
37778
|
import { promisify as promisify6 } from "node:util";
|
|
@@ -37949,9 +37992,6 @@ import { basename as basename3, join as join22 } from "node:path";
|
|
|
37949
37992
|
function runsDirectory() {
|
|
37950
37993
|
return join22(runtimeDirectory(), "runs");
|
|
37951
37994
|
}
|
|
37952
|
-
function recordPath() {
|
|
37953
|
-
return join22(runsDirectory(), "run.json");
|
|
37954
|
-
}
|
|
37955
37995
|
function recordPaths() {
|
|
37956
37996
|
return runtimeDirectories().map((directory) => join22(directory, "runs", "run.json"));
|
|
37957
37997
|
}
|
|
@@ -37960,10 +38000,6 @@ function openTranscript(startedAt) {
|
|
|
37960
38000
|
const path = join22(runsDirectory(), `run-${startedAt}.log`);
|
|
37961
38001
|
return { path, fd: openSync2(path, "a") };
|
|
37962
38002
|
}
|
|
37963
|
-
function writeRunRecord(record) {
|
|
37964
|
-
mkdirSync13(runsDirectory(), { recursive: true });
|
|
37965
|
-
writeFileSync15(recordPath(), JSON.stringify(record, null, 2), "utf8");
|
|
37966
|
-
}
|
|
37967
38003
|
function editorNotePath() {
|
|
37968
38004
|
return join22(runsDirectory(), "editor-run.json");
|
|
37969
38005
|
}
|
|
@@ -37999,21 +38035,6 @@ function editorNoteAt(path) {
|
|
|
37999
38035
|
}
|
|
38000
38036
|
return { projectPath, transcript, startedAt };
|
|
38001
38037
|
}
|
|
38002
|
-
function recordRunEnded(pid, ending) {
|
|
38003
|
-
const path = recordPath();
|
|
38004
|
-
const record = recordAt(path);
|
|
38005
|
-
if (record === null || record.pid !== pid) {
|
|
38006
|
-
return;
|
|
38007
|
-
}
|
|
38008
|
-
const written = {
|
|
38009
|
-
...record,
|
|
38010
|
-
...ending.exitCode === null ? {} : { exitCode: ending.exitCode },
|
|
38011
|
-
...ending.exitSignal === null ? {} : { exitSignal: ending.exitSignal }
|
|
38012
|
-
};
|
|
38013
|
-
try {
|
|
38014
|
-
writeFileSync15(path, JSON.stringify(written, null, 2), "utf8");
|
|
38015
|
-
} catch {}
|
|
38016
|
-
}
|
|
38017
38038
|
function readRunRecord() {
|
|
38018
38039
|
for (const path of recordPaths()) {
|
|
38019
38040
|
const record = recordAt(path);
|
|
@@ -38240,7 +38261,7 @@ var init_run_record = __esm(() => {
|
|
|
38240
38261
|
});
|
|
38241
38262
|
|
|
38242
38263
|
// src/server.ts
|
|
38243
|
-
import { execFile as execFile7, spawn } from "node:child_process";
|
|
38264
|
+
import { execFile as execFile7, spawn as spawn2 } from "node:child_process";
|
|
38244
38265
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
38245
38266
|
import {
|
|
38246
38267
|
appendFileSync,
|
|
@@ -38261,7 +38282,7 @@ import {
|
|
|
38261
38282
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
38262
38283
|
import { basename as basename4, dirname as dirname16, extname as extname2, isAbsolute as isAbsolute3, join as join23, normalize as normalize3, relative as relative3 } from "node:path";
|
|
38263
38284
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
38264
|
-
import { fileURLToPath as
|
|
38285
|
+
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
38265
38286
|
import { promisify as promisify7 } from "node:util";
|
|
38266
38287
|
function captureDestinationRefusal(outputPath, projects) {
|
|
38267
38288
|
if (/^(res|user):\/\//.test(outputPath)) {
|
|
@@ -38330,7 +38351,7 @@ function stillRunning(run) {
|
|
|
38330
38351
|
if (typeof run.endedHere === "string") {
|
|
38331
38352
|
return false;
|
|
38332
38353
|
}
|
|
38333
|
-
if (!run.throughEditor && run.
|
|
38354
|
+
if (!run.throughEditor && run.pid !== null) {
|
|
38334
38355
|
return alive(run.pid);
|
|
38335
38356
|
}
|
|
38336
38357
|
if (run.announcedPid !== undefined && !alive(run.announcedPid)) {
|
|
@@ -38354,10 +38375,10 @@ function runIsUp(run, editorSays, now = Date.now()) {
|
|
|
38354
38375
|
return editorSays ?? stillRunning(run);
|
|
38355
38376
|
}
|
|
38356
38377
|
function noCodeWillCome(run) {
|
|
38357
|
-
return !exited(run)
|
|
38378
|
+
return !exited(run);
|
|
38358
38379
|
}
|
|
38359
|
-
async function untilGone(
|
|
38360
|
-
const gone = () =>
|
|
38380
|
+
async function untilGone(pids, withinMs = STOP_WAIT_MS) {
|
|
38381
|
+
const gone = () => pids.every((pid) => !alive(pid));
|
|
38361
38382
|
const deadline = Date.now() + withinMs;
|
|
38362
38383
|
while (!gone() && Date.now() < deadline) {
|
|
38363
38384
|
await delay3(50);
|
|
@@ -38365,6 +38386,9 @@ async function untilGone(handle, pids, withinMs = STOP_WAIT_MS) {
|
|
|
38365
38386
|
return gone();
|
|
38366
38387
|
}
|
|
38367
38388
|
function endedWithoutACode(run) {
|
|
38389
|
+
if (!run.throughEditor && !run.pickedUp) {
|
|
38390
|
+
return "This run was started here, and the keeper holding it wrote no exit code into its note: the keeper was ended before the game, or another run replaced the note. Everything it printed is below, read back from its transcript.";
|
|
38391
|
+
}
|
|
38368
38392
|
if (!run.throughEditor) {
|
|
38369
38393
|
return run.endedHere ? "This run was started by another server, so nothing here held it and its exit code was never collected. Everything it printed is below, read back from its transcript." : "This run was started by another server, the one this server replaced or one running beside it, and it ended with nothing here waiting on it, so its exit code was never collected. Everything it printed is below, read back from its transcript.";
|
|
38370
38394
|
}
|
|
@@ -38557,6 +38581,33 @@ function outsideTheirRanges(spec, args) {
|
|
|
38557
38581
|
}
|
|
38558
38582
|
return complaints;
|
|
38559
38583
|
}
|
|
38584
|
+
function scanWaitIn(payload) {
|
|
38585
|
+
const waited = readNumber(payload, "waitedForEditorScanMs");
|
|
38586
|
+
const note = readString(payload, "scanNote");
|
|
38587
|
+
return {
|
|
38588
|
+
...waited === undefined ? {} : { waitedForEditorScanMs: waited },
|
|
38589
|
+
...note === undefined ? {} : { scanNote: note }
|
|
38590
|
+
};
|
|
38591
|
+
}
|
|
38592
|
+
function withScanWait(outcome, scanned) {
|
|
38593
|
+
if (scanned.waitedMs === 0) {
|
|
38594
|
+
return outcome;
|
|
38595
|
+
}
|
|
38596
|
+
const said = scanWaitAnswer(scanned);
|
|
38597
|
+
if (outcome.ok) {
|
|
38598
|
+
return { ...outcome, payload: { ...outcome.payload, ...said } };
|
|
38599
|
+
}
|
|
38600
|
+
return said.scanNote === undefined ? outcome : { ...outcome, message: `${outcome.message} ${said.scanNote}` };
|
|
38601
|
+
}
|
|
38602
|
+
function scanWaitAnswer(scanned) {
|
|
38603
|
+
if (scanned.waitedMs === 0) {
|
|
38604
|
+
return {};
|
|
38605
|
+
}
|
|
38606
|
+
return {
|
|
38607
|
+
waitedForEditorScanMs: scanned.waitedMs,
|
|
38608
|
+
scanNote: scanned.stillScanning ? `The editor was still scanning the project after ${SCAN_WAIT_MS / 1000} seconds, so 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." : undefined
|
|
38609
|
+
};
|
|
38610
|
+
}
|
|
38560
38611
|
function escapingPropertyValue(properties) {
|
|
38561
38612
|
if (typeof properties !== "object" || properties === null) {
|
|
38562
38613
|
return;
|
|
@@ -39491,6 +39542,47 @@ class GodotServer {
|
|
|
39491
39542
|
}
|
|
39492
39543
|
return { ok: true, value: stale };
|
|
39493
39544
|
}
|
|
39545
|
+
async untilTheEditorsScanIsWritten(projectPath) {
|
|
39546
|
+
const open = this.godotBridge.getStatus().projectPath;
|
|
39547
|
+
if (!this.godotBridge.isConnected() || open === undefined || !isSameDirectory(open, projectPath)) {
|
|
39548
|
+
return { waitedMs: 0, stillScanning: false };
|
|
39549
|
+
}
|
|
39550
|
+
const started = Date.now();
|
|
39551
|
+
let sawAScan = false;
|
|
39552
|
+
let scanEndedAt = null;
|
|
39553
|
+
for (;; ) {
|
|
39554
|
+
const asked = Date.now();
|
|
39555
|
+
let status;
|
|
39556
|
+
try {
|
|
39557
|
+
status = asParams(await this.godotBridge.invokeTool("scan_status", {}));
|
|
39558
|
+
} catch {
|
|
39559
|
+
return { waitedMs: Date.now() - started, stillScanning: false };
|
|
39560
|
+
}
|
|
39561
|
+
if (status["scanning"] === true || status["importing"] === true || status["pending"] === true) {
|
|
39562
|
+
sawAScan = true;
|
|
39563
|
+
if (Date.now() - started >= SCAN_WAIT_MS) {
|
|
39564
|
+
return { waitedMs: Date.now() - started, stillScanning: true };
|
|
39565
|
+
}
|
|
39566
|
+
await new Promise((settle) => setTimeout(settle, 100));
|
|
39567
|
+
continue;
|
|
39568
|
+
}
|
|
39569
|
+
const since = readNumber(status, "sinceScanFinishedMs");
|
|
39570
|
+
scanEndedAt = since !== undefined && since >= 0 ? asked - since : sawAScan ? asked : null;
|
|
39571
|
+
break;
|
|
39572
|
+
}
|
|
39573
|
+
let waitedForTheWrite = false;
|
|
39574
|
+
if (scanEndedAt !== null) {
|
|
39575
|
+
const writeDue = scanEndedAt + CACHE_WRITE_MS;
|
|
39576
|
+
while (Date.now() < writeDue && (cacheWrittenAt(projectPath) ?? 0) < scanEndedAt) {
|
|
39577
|
+
waitedForTheWrite = true;
|
|
39578
|
+
await new Promise((settle) => setTimeout(settle, 50));
|
|
39579
|
+
}
|
|
39580
|
+
}
|
|
39581
|
+
return {
|
|
39582
|
+
waitedMs: sawAScan || waitedForTheWrite ? Date.now() - started : 0,
|
|
39583
|
+
stillScanning: false
|
|
39584
|
+
};
|
|
39585
|
+
}
|
|
39494
39586
|
containProjectFiles(args) {
|
|
39495
39587
|
const projectPath = readString(args, "projectPath");
|
|
39496
39588
|
if (!projectPath) {
|
|
@@ -39551,14 +39643,15 @@ class GodotServer {
|
|
|
39551
39643
|
return engine.response;
|
|
39552
39644
|
}
|
|
39553
39645
|
const projectPath = project.value.path;
|
|
39646
|
+
const scanned = await this.untilTheEditorsScanIsWritten(projectPath);
|
|
39554
39647
|
const before = scriptsWithoutUid(projectPath);
|
|
39555
39648
|
const imported = await runImport(engine.value, projectPath);
|
|
39556
39649
|
if (!imported.ok) {
|
|
39557
|
-
return this.answer(imported);
|
|
39650
|
+
return this.answer(withScanWait(imported, scanned));
|
|
39558
39651
|
}
|
|
39559
39652
|
const after = scriptsWithoutUid(projectPath);
|
|
39560
39653
|
const given = before.filter((script) => !after.includes(script));
|
|
39561
|
-
return this.answer({
|
|
39654
|
+
return this.answer(withScanWait({
|
|
39562
39655
|
ok: true,
|
|
39563
39656
|
messages: imported.messages,
|
|
39564
39657
|
payload: {
|
|
@@ -39566,7 +39659,7 @@ class GodotServer {
|
|
|
39566
39659
|
stillWithoutUid: after,
|
|
39567
39660
|
note: uidsLeftNote(after.length)
|
|
39568
39661
|
}
|
|
39569
|
-
});
|
|
39662
|
+
}, scanned));
|
|
39570
39663
|
}
|
|
39571
39664
|
async operation(operation, params, projectPath) {
|
|
39572
39665
|
const engine = await this.engine();
|
|
@@ -39577,8 +39670,10 @@ class GodotServer {
|
|
|
39577
39670
|
messages: []
|
|
39578
39671
|
};
|
|
39579
39672
|
}
|
|
39673
|
+
const scanned = await this.untilTheEditorsScanIsWritten(projectPath);
|
|
39580
39674
|
this.logDebug(`Running ${operation} in ${projectPath}: ${JSON.stringify(params)}`);
|
|
39581
|
-
|
|
39675
|
+
const outcome = await runOperation2({ godotPath: engine.value, script: this.operationsScript, debug: GODOT_DEBUG_MODE_DEFAULT }, operation, params, projectPath);
|
|
39676
|
+
return withScanWait(outcome, scanned);
|
|
39582
39677
|
}
|
|
39583
39678
|
answer(outcome) {
|
|
39584
39679
|
if (!outcome.ok) {
|
|
@@ -39760,6 +39855,7 @@ class GodotServer {
|
|
|
39760
39855
|
if (!classes.ok) {
|
|
39761
39856
|
return this.answer(classes);
|
|
39762
39857
|
}
|
|
39858
|
+
const scanned = scanWaitIn(classes.payload);
|
|
39763
39859
|
const ours = `gdharness-reports/${randomUUID2()}`;
|
|
39764
39860
|
const reports = `res://.godot/${ours}`;
|
|
39765
39861
|
const ignored = readStringArray(args, "ignore") ?? [];
|
|
@@ -39850,7 +39946,8 @@ class GodotServer {
|
|
|
39850
39946
|
hung,
|
|
39851
39947
|
arguments: cmdArgs,
|
|
39852
39948
|
entries: forAnswer(printed.slice(0, 60)),
|
|
39853
|
-
savesNote
|
|
39949
|
+
savesNote,
|
|
39950
|
+
...scanned
|
|
39854
39951
|
}, null, 2)
|
|
39855
39952
|
}
|
|
39856
39953
|
],
|
|
@@ -39885,6 +39982,7 @@ class GodotServer {
|
|
|
39885
39982
|
orphans: orphans.total > 0 ? orphans.total : undefined,
|
|
39886
39983
|
notRun: notRun > 0 ? notRun : undefined,
|
|
39887
39984
|
savesNote,
|
|
39985
|
+
...scanned,
|
|
39888
39986
|
note: verdict.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." : notRun > 0 ? `This run stopped at the first failure in each suite it failed in, so ${notRun} case${notRun === 1 ? "" : "s"} never ran and count as neither passed nor failed. Leave failFast out to run every case.` : undefined,
|
|
39889
39987
|
suites: unclean.map((suite) => ({
|
|
39890
39988
|
name: suite.name,
|
|
@@ -40042,7 +40140,12 @@ class GodotServer {
|
|
|
40042
40140
|
return anEditorIsStillComing(listeningSince, this.launchedEditorIsUp());
|
|
40043
40141
|
}
|
|
40044
40142
|
launchedEditorIsUp() {
|
|
40045
|
-
|
|
40143
|
+
const launched = this.launchedEditor;
|
|
40144
|
+
if (launched === null || !alive(launched.pid)) {
|
|
40145
|
+
return false;
|
|
40146
|
+
}
|
|
40147
|
+
const now = startTimesOf([launched.pid]).get(launched.pid);
|
|
40148
|
+
return launched.startedAt === null || now === undefined || now === launched.startedAt;
|
|
40046
40149
|
}
|
|
40047
40150
|
dap() {
|
|
40048
40151
|
const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
|
|
@@ -40183,6 +40286,28 @@ class GodotServer {
|
|
|
40183
40286
|
return null;
|
|
40184
40287
|
}
|
|
40185
40288
|
}
|
|
40289
|
+
async editorScanState() {
|
|
40290
|
+
if (!this.godotBridge.getStatus().connected) {
|
|
40291
|
+
return null;
|
|
40292
|
+
}
|
|
40293
|
+
try {
|
|
40294
|
+
const answer = await Promise.race([
|
|
40295
|
+
this.godotBridge.invokeTool("scan_status", {}),
|
|
40296
|
+
delay3(PLAYING_STATUS_MS).then(() => null)
|
|
40297
|
+
]);
|
|
40298
|
+
if (answer === null) {
|
|
40299
|
+
return null;
|
|
40300
|
+
}
|
|
40301
|
+
const since = readNumber(asParams(answer), "sinceScanFinishedMs");
|
|
40302
|
+
return {
|
|
40303
|
+
scanning: readBoolean(asParams(answer), "scanning") ?? false,
|
|
40304
|
+
importing: readBoolean(asParams(answer), "importing") ?? false,
|
|
40305
|
+
finishedMsAgo: since === undefined || since < 0 ? null : since
|
|
40306
|
+
};
|
|
40307
|
+
} catch {
|
|
40308
|
+
return null;
|
|
40309
|
+
}
|
|
40310
|
+
}
|
|
40186
40311
|
async handleEditorStatus() {
|
|
40187
40312
|
const located = await this.locator.find();
|
|
40188
40313
|
const announced = runtimesAnnounced();
|
|
@@ -40213,11 +40338,16 @@ class GodotServer {
|
|
|
40213
40338
|
hold: current === null ? {} : await this.holdOf(current, active)
|
|
40214
40339
|
};
|
|
40215
40340
|
})();
|
|
40216
|
-
const [games, { playing, active, hold }] = await Promise.all([
|
|
40341
|
+
const [games, { playing, active, hold }, scan] = await Promise.all([
|
|
40342
|
+
pinged,
|
|
40343
|
+
asked,
|
|
40344
|
+
this.editorScanState()
|
|
40345
|
+
]);
|
|
40217
40346
|
return this.jsonTextResponse({
|
|
40218
40347
|
editor: {
|
|
40219
40348
|
...this.getEditorStatusPayload(),
|
|
40220
|
-
debugPort: playing?.debugPort ?? this.godotBridge.getStatus().debugPort
|
|
40349
|
+
debugPort: playing?.debugPort ?? this.godotBridge.getStatus().debugPort,
|
|
40350
|
+
scan
|
|
40221
40351
|
},
|
|
40222
40352
|
godot: {
|
|
40223
40353
|
path: located.ok ? located.path : null,
|
|
@@ -40376,9 +40506,9 @@ class GodotServer {
|
|
|
40376
40506
|
}
|
|
40377
40507
|
async openAnEditor(engine, projectPath, ports) {
|
|
40378
40508
|
clearEditorLog(projectPath);
|
|
40379
|
-
const
|
|
40380
|
-
|
|
40381
|
-
|
|
40509
|
+
const launched = await launchOutsideTheTree({
|
|
40510
|
+
command: engine,
|
|
40511
|
+
args: editorArguments(projectPath, ports, editorLogPath(projectPath)),
|
|
40382
40512
|
env: {
|
|
40383
40513
|
...process.env,
|
|
40384
40514
|
GDHARNESS_RUNTIME_DIR: runtimeDirectory(),
|
|
@@ -40387,23 +40517,15 @@ class GodotServer {
|
|
|
40387
40517
|
[OPENED_BY_A_SERVER]: "1"
|
|
40388
40518
|
}
|
|
40389
40519
|
});
|
|
40390
|
-
if (
|
|
40391
|
-
|
|
40520
|
+
if ("error" in launched) {
|
|
40521
|
+
return { pid: null, error: launched.error };
|
|
40392
40522
|
}
|
|
40393
|
-
|
|
40394
|
-
|
|
40395
|
-
|
|
40396
|
-
|
|
40397
|
-
|
|
40398
|
-
|
|
40399
|
-
});
|
|
40400
|
-
});
|
|
40401
|
-
if (started !== null) {
|
|
40402
|
-
return { pid: null, error: started };
|
|
40403
|
-
}
|
|
40404
|
-
editor.unref();
|
|
40405
|
-
this.launchedEditor = editor.pid === undefined ? null : editor;
|
|
40406
|
-
return { pid: editor.pid ?? null, error: null };
|
|
40523
|
+
writeEditorLogNote(projectPath, launched.pid);
|
|
40524
|
+
this.launchedEditor = {
|
|
40525
|
+
pid: launched.pid,
|
|
40526
|
+
startedAt: startTimesOf([launched.pid]).get(launched.pid) ?? null
|
|
40527
|
+
};
|
|
40528
|
+
return { pid: launched.pid, error: null };
|
|
40407
40529
|
}
|
|
40408
40530
|
async portsForAnEditor() {
|
|
40409
40531
|
const lsp = portFromEnvOrNull("GDHARNESS_LSP_PORT") ?? await freePort(DEFAULT_LSP_PORT);
|
|
@@ -40438,13 +40560,14 @@ class GodotServer {
|
|
|
40438
40560
|
if (!engine.ok) {
|
|
40439
40561
|
return engine.response;
|
|
40440
40562
|
}
|
|
40563
|
+
const scanned = await this.untilTheEditorsScanIsWritten(project.value.path);
|
|
40441
40564
|
const refreshed = await this.refreshStaleClasses(project.value.path);
|
|
40442
40565
|
if (!refreshed.ok) {
|
|
40443
40566
|
return refreshed.response;
|
|
40444
40567
|
}
|
|
40445
40568
|
const sceneArgument = sceneToRun?.ok ? sceneToRun.relativePath : null;
|
|
40446
40569
|
if (op === "check") {
|
|
40447
|
-
return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value, asked.value);
|
|
40570
|
+
return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value, scanned, asked.value);
|
|
40448
40571
|
}
|
|
40449
40572
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
40450
40573
|
let ended = null;
|
|
@@ -40463,7 +40586,7 @@ class GodotServer {
|
|
|
40463
40586
|
const runtimeWaitMs = this.announceBudget(args, project.value.path);
|
|
40464
40587
|
const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
|
|
40465
40588
|
if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && !needsItsOwnProcess(asked.value)) {
|
|
40466
|
-
return await this.playThroughEditor(sceneArgument, refreshed.value, project.value.path, alreadyPlaying, runtimeWaitMs, ended);
|
|
40589
|
+
return await this.playThroughEditor(sceneArgument, refreshed.value, scanned, project.value.path, alreadyPlaying, runtimeWaitMs, ended);
|
|
40467
40590
|
}
|
|
40468
40591
|
const cmdArgs = runArguments({
|
|
40469
40592
|
projectPath: project.value.path,
|
|
@@ -40472,7 +40595,12 @@ class GodotServer {
|
|
|
40472
40595
|
userArgs: given.value
|
|
40473
40596
|
});
|
|
40474
40597
|
this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
40475
|
-
const started = this.
|
|
40598
|
+
const started = await this.launchKeptGame(engine.value, cmdArgs, project.value.path, needsItsOwnProcess(asked.value) ? environmentFor({ ...asked.value, runtimeDirectory: runtimeDirectory() }) : undefined);
|
|
40599
|
+
if ("error" in started) {
|
|
40600
|
+
return this.createErrorResponse(`The game could not be started: ${started.error}`, [
|
|
40601
|
+
"editor_status says which Godot this server runs and whether it answers"
|
|
40602
|
+
]);
|
|
40603
|
+
}
|
|
40476
40604
|
started.announcedBefore = alreadyPlaying;
|
|
40477
40605
|
this.activeProcess = started;
|
|
40478
40606
|
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." : "";
|
|
@@ -40484,10 +40612,11 @@ class GodotServer {
|
|
|
40484
40612
|
started: true,
|
|
40485
40613
|
through: "gdharness",
|
|
40486
40614
|
alsoRunning: stranded.length === 0 ? undefined : stranded.map((one) => one.pid),
|
|
40487
|
-
pid: started.
|
|
40615
|
+
pid: started.pid,
|
|
40488
40616
|
arguments: cmdArgs,
|
|
40489
40617
|
transcript: started.transcript,
|
|
40490
40618
|
refreshedClasses: refreshed.value,
|
|
40619
|
+
...scanWaitAnswer(scanned),
|
|
40491
40620
|
savesNote: asked.value.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
|
|
40492
40621
|
endedPreviousRun: endedPreviousRun(ended),
|
|
40493
40622
|
runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
|
|
@@ -40569,7 +40698,7 @@ class GodotServer {
|
|
|
40569
40698
|
writeBootNote(run.projectPath, took);
|
|
40570
40699
|
}
|
|
40571
40700
|
}
|
|
40572
|
-
async playThroughEditor(scene, refreshedClasses, projectPath, alreadyPlaying, runtimeWaitMs, ended) {
|
|
40701
|
+
async playThroughEditor(scene, refreshedClasses, scanned, projectPath, alreadyPlaying, runtimeWaitMs, ended) {
|
|
40573
40702
|
const log = new GameLog;
|
|
40574
40703
|
try {
|
|
40575
40704
|
await this.dap().connect();
|
|
@@ -40598,7 +40727,6 @@ class GodotServer {
|
|
|
40598
40727
|
closeSync2(transcript.fd);
|
|
40599
40728
|
const playAnswer = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
|
|
40600
40729
|
const played = {
|
|
40601
|
-
process: null,
|
|
40602
40730
|
pid: null,
|
|
40603
40731
|
log,
|
|
40604
40732
|
transcript: transcript.path,
|
|
@@ -40620,6 +40748,7 @@ class GodotServer {
|
|
|
40620
40748
|
through: "editor",
|
|
40621
40749
|
scene: scene === null ? "the main scene" : `res://${scene}`,
|
|
40622
40750
|
refreshedClasses,
|
|
40751
|
+
...scanWaitAnswer(scanned),
|
|
40623
40752
|
debugPort: readNumber(playAnswer, "debugPort"),
|
|
40624
40753
|
endedPreviousRun: endedPreviousRun(ended),
|
|
40625
40754
|
breakpoints: breakpoints.applied.length === 0 ? undefined : this.breakpointsAsProjectSpells(projectPath, breakpoints.applied),
|
|
@@ -40696,8 +40825,14 @@ class GodotServer {
|
|
|
40696
40825
|
}
|
|
40697
40826
|
async endActiveGame(reason, going) {
|
|
40698
40827
|
const running = this.activeProcess;
|
|
40699
|
-
const recorded = running
|
|
40700
|
-
|
|
40828
|
+
const recorded = running !== null && !running.throughEditor ? readRunRecord() : null;
|
|
40829
|
+
const clearTheRecord = () => {
|
|
40830
|
+
clearRunRecord((record) => this.couldBeOurs(record.projectPath));
|
|
40831
|
+
};
|
|
40832
|
+
const aKeeperHoldsIt = running !== null && going && !running.throughEditor && running.pid !== null;
|
|
40833
|
+
if (!aKeeperHoldsIt) {
|
|
40834
|
+
clearTheRecord();
|
|
40835
|
+
}
|
|
40701
40836
|
if (!running || !going) {
|
|
40702
40837
|
return true;
|
|
40703
40838
|
}
|
|
@@ -40705,16 +40840,13 @@ class GodotServer {
|
|
|
40705
40840
|
const announced = this.announcedPidOf(running);
|
|
40706
40841
|
if (running.throughEditor) {
|
|
40707
40842
|
await this.handleViaBridge("stop_playing", {});
|
|
40708
|
-
return await untilGone(
|
|
40709
|
-
}
|
|
40710
|
-
if (running.process !== null) {
|
|
40711
|
-
running.process.kill();
|
|
40712
|
-
return await untilGone(running.process, announced === undefined ? [] : [announced]);
|
|
40843
|
+
return await untilGone(announced === undefined ? [] : [announced]);
|
|
40713
40844
|
}
|
|
40714
40845
|
if (running.pid === null) {
|
|
40715
40846
|
return true;
|
|
40716
40847
|
}
|
|
40717
40848
|
if (recorded === null || recorded.pid !== running.pid || !stillTheRecordedRun(recorded)) {
|
|
40849
|
+
clearTheRecord();
|
|
40718
40850
|
running.endedHere = null;
|
|
40719
40851
|
running.log.record("warning", `This run was not ended here: pid ${running.pid} no longer answers as the run that was recorded, so nothing was signalled. If that process is still the game, end it yourself; if it is not, it belongs to something else.`);
|
|
40720
40852
|
return true;
|
|
@@ -40727,12 +40859,20 @@ class GodotServer {
|
|
|
40727
40859
|
} catch {
|
|
40728
40860
|
landed = false;
|
|
40729
40861
|
}
|
|
40730
|
-
const gone = await untilGone(
|
|
40862
|
+
const gone = await untilGone([running.pid]);
|
|
40863
|
+
if (gone) {
|
|
40864
|
+
await this.untilTheExitIsRecorded(running);
|
|
40865
|
+
}
|
|
40866
|
+
if (landed && gone) {
|
|
40867
|
+
running.exitCode = null;
|
|
40868
|
+
running.exitSignal = "SIGTERM";
|
|
40869
|
+
}
|
|
40870
|
+
clearTheRecord();
|
|
40731
40871
|
running.log.record("info", landed ? `gdharness ended pid ${running.pid}, which the operating system described as ${described}.` : `gdharness signalled pid ${running.pid} and it was already gone; the operating system had described it as ${described}.`);
|
|
40732
40872
|
return gone;
|
|
40733
40873
|
}
|
|
40734
40874
|
spawnGame(godotPath, cmdArgs, env) {
|
|
40735
|
-
const child =
|
|
40875
|
+
const child = spawn2(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
|
|
40736
40876
|
const cancelled = callSignal();
|
|
40737
40877
|
if (cancelled !== undefined) {
|
|
40738
40878
|
const end = () => {
|
|
@@ -40784,25 +40924,23 @@ class GodotServer {
|
|
|
40784
40924
|
});
|
|
40785
40925
|
return started;
|
|
40786
40926
|
}
|
|
40787
|
-
|
|
40927
|
+
async launchKeptGame(godotPath, cmdArgs, projectPath, env) {
|
|
40788
40928
|
const startedAt = Date.now();
|
|
40789
40929
|
const transcript = openTranscript(startedAt);
|
|
40790
|
-
|
|
40791
|
-
|
|
40792
|
-
|
|
40793
|
-
|
|
40794
|
-
|
|
40795
|
-
|
|
40796
|
-
|
|
40797
|
-
|
|
40798
|
-
|
|
40930
|
+
closeSync2(transcript.fd);
|
|
40931
|
+
const launched = await launchOutsideTheTree({
|
|
40932
|
+
command: godotPath,
|
|
40933
|
+
args: cmdArgs,
|
|
40934
|
+
...env === undefined ? {} : { env },
|
|
40935
|
+
run: { transcript: transcript.path, startedAt, projectPath }
|
|
40936
|
+
});
|
|
40937
|
+
if ("error" in launched) {
|
|
40938
|
+
return launched;
|
|
40799
40939
|
}
|
|
40800
|
-
|
|
40801
|
-
|
|
40802
|
-
|
|
40803
|
-
|
|
40804
|
-
pid: child.pid ?? null,
|
|
40805
|
-
log,
|
|
40940
|
+
sweepTranscripts();
|
|
40941
|
+
return {
|
|
40942
|
+
pid: launched.pid,
|
|
40943
|
+
log: new GameLog,
|
|
40806
40944
|
transcript: transcript.path,
|
|
40807
40945
|
readOffset: 0,
|
|
40808
40946
|
projectPath,
|
|
@@ -40812,34 +40950,6 @@ class GodotServer {
|
|
|
40812
40950
|
throughEditor: false,
|
|
40813
40951
|
brokeOn: null
|
|
40814
40952
|
};
|
|
40815
|
-
child.on("exit", (code, signal) => {
|
|
40816
|
-
this.logDebug(`Godot process exited with code ${code ?? "none"}, signal ${signal ?? "none"}`);
|
|
40817
|
-
this.drainTranscript(started);
|
|
40818
|
-
log.finish();
|
|
40819
|
-
started.exitCode = code;
|
|
40820
|
-
started.exitSignal = code === null ? signal : null;
|
|
40821
|
-
if (started.pid !== null) {
|
|
40822
|
-
recordRunEnded(started.pid, { exitCode: started.exitCode, exitSignal: started.exitSignal });
|
|
40823
|
-
}
|
|
40824
|
-
});
|
|
40825
|
-
child.on("error", (err) => {
|
|
40826
|
-
console.error("Failed to start Godot process:", err);
|
|
40827
|
-
log.append("stderr", `${err.message}
|
|
40828
|
-
`);
|
|
40829
|
-
started.exitCode = -1;
|
|
40830
|
-
});
|
|
40831
|
-
if (started.pid !== null) {
|
|
40832
|
-
writeRunRecord({
|
|
40833
|
-
pid: started.pid,
|
|
40834
|
-
transcript: transcript.path,
|
|
40835
|
-
startedAt,
|
|
40836
|
-
projectPath,
|
|
40837
|
-
arguments: cmdArgs,
|
|
40838
|
-
command: godotPath
|
|
40839
|
-
});
|
|
40840
|
-
}
|
|
40841
|
-
sweepTranscripts();
|
|
40842
|
-
return started;
|
|
40843
40953
|
}
|
|
40844
40954
|
drainTranscript(game) {
|
|
40845
40955
|
if (game.transcript === null) {
|
|
@@ -40895,7 +41005,6 @@ class GodotServer {
|
|
|
40895
41005
|
const left = readEditorRunNote();
|
|
40896
41006
|
const note = left !== null && project !== null && isSameDirectory(left.projectPath, project) ? left : null;
|
|
40897
41007
|
const picked = {
|
|
40898
|
-
process: null,
|
|
40899
41008
|
pid: null,
|
|
40900
41009
|
log: new GameLog,
|
|
40901
41010
|
transcript: note?.transcript ?? null,
|
|
@@ -41032,7 +41141,6 @@ class GodotServer {
|
|
|
41032
41141
|
return null;
|
|
41033
41142
|
}
|
|
41034
41143
|
const adopted = {
|
|
41035
|
-
process: null,
|
|
41036
41144
|
pid: record.pid,
|
|
41037
41145
|
log: new GameLog,
|
|
41038
41146
|
transcript: record.transcript,
|
|
@@ -41042,7 +41150,8 @@ class GodotServer {
|
|
|
41042
41150
|
exitCode: null,
|
|
41043
41151
|
exitSignal: null,
|
|
41044
41152
|
throughEditor: false,
|
|
41045
|
-
brokeOn: null
|
|
41153
|
+
brokeOn: null,
|
|
41154
|
+
pickedUp: true
|
|
41046
41155
|
};
|
|
41047
41156
|
this.drainTranscript(adopted);
|
|
41048
41157
|
if (!alive(record.pid) || !couldStillBeTheRecordedRun(record)) {
|
|
@@ -41123,7 +41232,7 @@ class GodotServer {
|
|
|
41123
41232
|
currentRun() {
|
|
41124
41233
|
return this.activeProcess ?? this.adoptRecordedRun();
|
|
41125
41234
|
}
|
|
41126
|
-
async checkBoot(godotPath, projectPath, scene, args, userArgs, asked = {}) {
|
|
41235
|
+
async checkBoot(godotPath, projectPath, scene, args, userArgs, scanned, asked = {}) {
|
|
41127
41236
|
const frames = readPositiveNumber(args, "frames") ?? 3;
|
|
41128
41237
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 60000;
|
|
41129
41238
|
const cmdArgs = runArguments({ projectPath, headless: true, scene, quitAfter: frames, userArgs });
|
|
@@ -41153,6 +41262,7 @@ class GodotServer {
|
|
|
41153
41262
|
frames,
|
|
41154
41263
|
errors,
|
|
41155
41264
|
warnings,
|
|
41265
|
+
...scanWaitAnswer(scanned),
|
|
41156
41266
|
savesNote: asked.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
|
|
41157
41267
|
entries: forAnswer(boot.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
41158
41268
|
});
|
|
@@ -41176,8 +41286,8 @@ class GodotServer {
|
|
|
41176
41286
|
async runStillGoing(run, editorSays) {
|
|
41177
41287
|
if (!run.throughEditor) {
|
|
41178
41288
|
const going = stillRunning(run);
|
|
41179
|
-
if (!going && run.
|
|
41180
|
-
|
|
41289
|
+
if (!going && run.pid !== null && !exited(run) && run.endedUnwatched !== true && typeof run.endedHere !== "string" && !await this.untilTheExitIsRecorded(run)) {
|
|
41290
|
+
run.endedUnwatched = true;
|
|
41181
41291
|
}
|
|
41182
41292
|
return going;
|
|
41183
41293
|
}
|
|
@@ -41194,6 +41304,21 @@ class GodotServer {
|
|
|
41194
41304
|
}
|
|
41195
41305
|
return runIsUp(run, said);
|
|
41196
41306
|
}
|
|
41307
|
+
async untilTheExitIsRecorded(run) {
|
|
41308
|
+
const until = Date.now() + EXIT_REPORTED_WITHIN_MS;
|
|
41309
|
+
for (;; ) {
|
|
41310
|
+
const record = readRunRecord();
|
|
41311
|
+
if (record !== null && record.pid === run.pid && (record.exitCode !== undefined || record.exitSignal !== undefined)) {
|
|
41312
|
+
run.exitCode = record.exitCode ?? null;
|
|
41313
|
+
run.exitSignal = record.exitSignal ?? null;
|
|
41314
|
+
return true;
|
|
41315
|
+
}
|
|
41316
|
+
if (Date.now() >= until) {
|
|
41317
|
+
return false;
|
|
41318
|
+
}
|
|
41319
|
+
await delay3(50);
|
|
41320
|
+
}
|
|
41321
|
+
}
|
|
41197
41322
|
handleEditorConsole(args) {
|
|
41198
41323
|
const status = this.godotBridge.getStatus();
|
|
41199
41324
|
const projectPath = status.projectPath ?? this.ownProject ?? undefined;
|
|
@@ -41295,7 +41420,8 @@ class GodotServer {
|
|
|
41295
41420
|
throughEditor: run.throughEditor,
|
|
41296
41421
|
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
41297
41422
|
couldAnnounce: run.projectPath !== null && existsSync15(join23(run.projectPath, RUNTIME_AUTOLOAD2.path)),
|
|
41298
|
-
endedHere: typeof run.endedHere === "string"
|
|
41423
|
+
endedHere: typeof run.endedHere === "string",
|
|
41424
|
+
pickedUp: run.pickedUp === true
|
|
41299
41425
|
}));
|
|
41300
41426
|
}
|
|
41301
41427
|
if (run.pickedUpPlaying === true) {
|
|
@@ -41321,7 +41447,7 @@ class GodotServer {
|
|
|
41321
41447
|
...hold,
|
|
41322
41448
|
endedUnwatched: run.endedUnwatched === true ? true : undefined,
|
|
41323
41449
|
endedBy: stillRunning(run) ? undefined : run.endedHere ?? null,
|
|
41324
|
-
project:
|
|
41450
|
+
project: run.throughEditor ? undefined : run.projectPath ?? undefined,
|
|
41325
41451
|
transcript: run.transcript ?? undefined,
|
|
41326
41452
|
startedAt: new Date(run.startedAt).toISOString(),
|
|
41327
41453
|
elapsedMs: Date.now() - run.startedAt,
|
|
@@ -41536,6 +41662,10 @@ class GodotServer {
|
|
|
41536
41662
|
return now === null ? [] : lost.filter((name) => now.has(name));
|
|
41537
41663
|
}
|
|
41538
41664
|
async rebuildClassCache(projectPath) {
|
|
41665
|
+
const scanned = await this.untilTheEditorsScanIsWritten(projectPath);
|
|
41666
|
+
return withScanWait(await this.rebuildClassCacheFromDisk(projectPath), scanned);
|
|
41667
|
+
}
|
|
41668
|
+
async rebuildClassCacheFromDisk(projectPath) {
|
|
41539
41669
|
const before = cachedClasses(projectPath);
|
|
41540
41670
|
const writtenBefore = cacheWrittenAt(projectPath);
|
|
41541
41671
|
const noted = readClassNote(projectPath);
|
|
@@ -41924,7 +42054,7 @@ async function runGodotServer() {
|
|
|
41924
42054
|
const server = new GodotServer;
|
|
41925
42055
|
await server.run();
|
|
41926
42056
|
}
|
|
41927
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run7, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
42057
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run7, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, SCAN_WAIT_MS = 30000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
41928
42058
|
var init_server3 = __esm(() => {
|
|
41929
42059
|
init_mcp();
|
|
41930
42060
|
init_stdio2();
|
|
@@ -41947,6 +42077,7 @@ var init_server3 = __esm(() => {
|
|
|
41947
42077
|
init_issues();
|
|
41948
42078
|
init_junit();
|
|
41949
42079
|
init_lsp_client();
|
|
42080
|
+
init_outside();
|
|
41950
42081
|
init_paths();
|
|
41951
42082
|
init_ports();
|
|
41952
42083
|
init_process_children();
|
|
@@ -41962,7 +42093,7 @@ var init_server3 = __esm(() => {
|
|
|
41962
42093
|
init_tool_definitions();
|
|
41963
42094
|
init_update_check();
|
|
41964
42095
|
run7 = promisify7(execFile7);
|
|
41965
|
-
__dirname2 = dirname16(
|
|
42096
|
+
__dirname2 = dirname16(fileURLToPath6(import.meta.url));
|
|
41966
42097
|
CONSOLE_WRAPPER = /_console(\.exe)?$/i;
|
|
41967
42098
|
LONGEST_BOOT_NOTED_MS = 5 * 60000;
|
|
41968
42099
|
VALUE_NAMES = {
|
|
@@ -43697,6 +43828,13 @@ a class it cannot resolve would go out of that file with it. The answer names th
|
|
|
43697
43828
|
\`cacheRestored\`, so no fresh engine, CI run or clone inherits the short file. That is the same
|
|
43698
43829
|
class \`unseenByEditor\` names, and the loss is not a reason to refuse the scan.
|
|
43699
43830
|
|
|
43831
|
+
The editor also scans on its own, when files change under it, and it rewrites that file just after
|
|
43832
|
+
the scan ends. Every engine this server starts on the project (a run, a boot check, a test run, a
|
|
43833
|
+
headless operation) waits until a scan in progress and the write after it are done, and says how
|
|
43834
|
+
long under \`waitedForEditorScanMs\`. A \`scanNote\` means a scan was still going after thirty
|
|
43835
|
+
seconds and the engine started anyway: a "Could not find type" from that engine is the scan, not
|
|
43836
|
+
the script, and \`editor_rescan\` followed by the same call again is the cure.
|
|
43837
|
+
|
|
43700
43838
|
A diagnostic about a member rather than a type, \`Static function "x()" not found in base "Y"\`
|
|
43701
43839
|
about something the engine compiles, is a different thing and is no longer stale here: each ask
|
|
43702
43840
|
gives the document back when its answer arrives, so the next one is read off disk. If one turns
|