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/index.js
CHANGED
|
@@ -10183,7 +10183,7 @@ function defectReport(where, error, context = {}) {
|
|
|
10183
10183
|
}
|
|
10184
10184
|
|
|
10185
10185
|
// src/server.ts
|
|
10186
|
-
import { execFile as execFile5, spawn } from "node:child_process";
|
|
10186
|
+
import { execFile as execFile5, spawn as spawn2 } from "node:child_process";
|
|
10187
10187
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
10188
10188
|
import {
|
|
10189
10189
|
appendFileSync,
|
|
@@ -10204,7 +10204,7 @@ import {
|
|
|
10204
10204
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
10205
10205
|
import { basename as basename3, dirname as dirname10, extname as extname2, isAbsolute as isAbsolute3, join as join16, normalize as normalize2, relative as relative3 } from "node:path";
|
|
10206
10206
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
10207
|
-
import { fileURLToPath as
|
|
10207
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
10208
10208
|
import { promisify as promisify5 } from "node:util";
|
|
10209
10209
|
|
|
10210
10210
|
// node_modules/zod/v3/helpers/util.js
|
|
@@ -28884,6 +28884,46 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
28884
28884
|
}
|
|
28885
28885
|
}
|
|
28886
28886
|
|
|
28887
|
+
// src/outside.ts
|
|
28888
|
+
import { spawn } from "node:child_process";
|
|
28889
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
28890
|
+
var KEEPER_SCRIPT = fileURLToPath2(new URL("./keeper.js", import.meta.url));
|
|
28891
|
+
function encodeSpec(spec) {
|
|
28892
|
+
return Buffer.from(JSON.stringify(spec), "utf8").toString("base64url");
|
|
28893
|
+
}
|
|
28894
|
+
function readLaunched(printed) {
|
|
28895
|
+
const line = printed.split(`
|
|
28896
|
+
`).find((one) => one.trim() !== "")?.trim() ?? "";
|
|
28897
|
+
const pid = /^pid (\d+)$/.exec(line);
|
|
28898
|
+
if (pid?.[1] !== undefined) {
|
|
28899
|
+
return { pid: Number(pid[1]) };
|
|
28900
|
+
}
|
|
28901
|
+
return { error: line.replace(/^error /, "") || "the launcher said nothing" };
|
|
28902
|
+
}
|
|
28903
|
+
async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = KEEPER_SCRIPT) {
|
|
28904
|
+
return await new Promise((resolve) => {
|
|
28905
|
+
const launcher = spawn(runtime, [keeper, "launch", encodeSpec(spec)], {
|
|
28906
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
28907
|
+
windowsHide: true
|
|
28908
|
+
});
|
|
28909
|
+
let printed = "";
|
|
28910
|
+
let complained = "";
|
|
28911
|
+
launcher.stdout.on("data", (chunk) => {
|
|
28912
|
+
printed += chunk.toString();
|
|
28913
|
+
});
|
|
28914
|
+
launcher.stderr.on("data", (chunk) => {
|
|
28915
|
+
complained += chunk.toString();
|
|
28916
|
+
});
|
|
28917
|
+
launcher.once("error", (error) => {
|
|
28918
|
+
resolve({ error: `the launcher did not start: ${error.message}` });
|
|
28919
|
+
});
|
|
28920
|
+
launcher.once("close", () => {
|
|
28921
|
+
const launched = readLaunched(printed);
|
|
28922
|
+
resolve("error" in launched && complained.trim() !== "" ? { error: `${launched.error}: ${complained.trim()}` } : launched);
|
|
28923
|
+
});
|
|
28924
|
+
});
|
|
28925
|
+
}
|
|
28926
|
+
|
|
28887
28927
|
// src/process-time.ts
|
|
28888
28928
|
import { execFile as execFile4 } from "node:child_process";
|
|
28889
28929
|
import { promisify as promisify4 } from "node:util";
|
|
@@ -29336,9 +29376,6 @@ import { basename as basename2, join as join14 } from "node:path";
|
|
|
29336
29376
|
function runsDirectory() {
|
|
29337
29377
|
return join14(runtimeDirectory(), "runs");
|
|
29338
29378
|
}
|
|
29339
|
-
function recordPath() {
|
|
29340
|
-
return join14(runsDirectory(), "run.json");
|
|
29341
|
-
}
|
|
29342
29379
|
function recordPaths() {
|
|
29343
29380
|
return runtimeDirectories().map((directory) => join14(directory, "runs", "run.json"));
|
|
29344
29381
|
}
|
|
@@ -29347,10 +29384,6 @@ function openTranscript(startedAt) {
|
|
|
29347
29384
|
const path = join14(runsDirectory(), `run-${startedAt}.log`);
|
|
29348
29385
|
return { path, fd: openSync2(path, "a") };
|
|
29349
29386
|
}
|
|
29350
|
-
function writeRunRecord(record) {
|
|
29351
|
-
mkdirSync8(runsDirectory(), { recursive: true });
|
|
29352
|
-
writeFileSync9(recordPath(), JSON.stringify(record, null, 2), "utf8");
|
|
29353
|
-
}
|
|
29354
29387
|
function editorNotePath() {
|
|
29355
29388
|
return join14(runsDirectory(), "editor-run.json");
|
|
29356
29389
|
}
|
|
@@ -29386,21 +29419,6 @@ function editorNoteAt(path) {
|
|
|
29386
29419
|
}
|
|
29387
29420
|
return { projectPath, transcript, startedAt };
|
|
29388
29421
|
}
|
|
29389
|
-
function recordRunEnded(pid, ending) {
|
|
29390
|
-
const path = recordPath();
|
|
29391
|
-
const record = recordAt(path);
|
|
29392
|
-
if (record === null || record.pid !== pid) {
|
|
29393
|
-
return;
|
|
29394
|
-
}
|
|
29395
|
-
const written = {
|
|
29396
|
-
...record,
|
|
29397
|
-
...ending.exitCode === null ? {} : { exitCode: ending.exitCode },
|
|
29398
|
-
...ending.exitSignal === null ? {} : { exitSignal: ending.exitSignal }
|
|
29399
|
-
};
|
|
29400
|
-
try {
|
|
29401
|
-
writeFileSync9(path, JSON.stringify(written, null, 2), "utf8");
|
|
29402
|
-
} catch {}
|
|
29403
|
-
}
|
|
29404
29422
|
function readRunRecord() {
|
|
29405
29423
|
for (const path of recordPaths()) {
|
|
29406
29424
|
const record = recordAt(path);
|
|
@@ -29635,7 +29653,7 @@ import {
|
|
|
29635
29653
|
writeFileSync as writeFileSync10
|
|
29636
29654
|
} from "node:fs";
|
|
29637
29655
|
import { dirname as dirname9, join as join15 } from "node:path";
|
|
29638
|
-
import { fileURLToPath as
|
|
29656
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
29639
29657
|
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
29640
29658
|
var EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
|
|
29641
29659
|
var RUNTIME_AUTOLOAD = {
|
|
@@ -29681,7 +29699,7 @@ function shippedEditorDigest() {
|
|
|
29681
29699
|
return shippedDigest ?? undefined;
|
|
29682
29700
|
}
|
|
29683
29701
|
function shippedAddonsDirectory() {
|
|
29684
|
-
return join15(dirname9(
|
|
29702
|
+
return join15(dirname9(fileURLToPath3(import.meta.url)), "godot", "addons");
|
|
29685
29703
|
}
|
|
29686
29704
|
function installedAddonVersion(projectPath) {
|
|
29687
29705
|
const marker = join15(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
@@ -30316,7 +30334,7 @@ var TOOL_SPECS = [
|
|
|
30316
30334
|
},
|
|
30317
30335
|
{
|
|
30318
30336
|
name: "editor_run",
|
|
30319
|
-
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.",
|
|
30337
|
+
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.",
|
|
30320
30338
|
parameters: {
|
|
30321
30339
|
projectPath: { ...PROJECT_PATH, ops: ["start", "check"] },
|
|
30322
30340
|
scene: {
|
|
@@ -30433,7 +30451,7 @@ var TOOL_SPECS = [
|
|
|
30433
30451
|
},
|
|
30434
30452
|
{
|
|
30435
30453
|
name: "editor_status",
|
|
30436
|
-
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.",
|
|
30454
|
+
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.",
|
|
30437
30455
|
parameters: {},
|
|
30438
30456
|
requires: []
|
|
30439
30457
|
},
|
|
@@ -30948,7 +30966,7 @@ function buildToolDefinitions() {
|
|
|
30948
30966
|
var UPDATE_NOTICE_EVERY = 500;
|
|
30949
30967
|
var FEEDBACK_NOTICE_EVERY = 250;
|
|
30950
30968
|
var run5 = promisify5(execFile5);
|
|
30951
|
-
var __dirname2 = dirname10(
|
|
30969
|
+
var __dirname2 = dirname10(fileURLToPath4(import.meta.url));
|
|
30952
30970
|
var EDITOR_RESTART_TIMEOUT_MS = 90000;
|
|
30953
30971
|
var HOLD_PING_MS = 2000;
|
|
30954
30972
|
var SLOWEST_FRAME_RATE = 20;
|
|
@@ -31019,7 +31037,7 @@ function stillRunning(run) {
|
|
|
31019
31037
|
if (typeof run.endedHere === "string") {
|
|
31020
31038
|
return false;
|
|
31021
31039
|
}
|
|
31022
|
-
if (!run.throughEditor && run.
|
|
31040
|
+
if (!run.throughEditor && run.pid !== null) {
|
|
31023
31041
|
return alive(run.pid);
|
|
31024
31042
|
}
|
|
31025
31043
|
if (run.announcedPid !== undefined && !alive(run.announcedPid)) {
|
|
@@ -31044,12 +31062,12 @@ function runIsUp(run, editorSays, now = Date.now()) {
|
|
|
31044
31062
|
return editorSays ?? stillRunning(run);
|
|
31045
31063
|
}
|
|
31046
31064
|
function noCodeWillCome(run) {
|
|
31047
|
-
return !exited(run)
|
|
31065
|
+
return !exited(run);
|
|
31048
31066
|
}
|
|
31049
31067
|
var STOP_WAIT_MS = 1e4;
|
|
31050
31068
|
var EXIT_REPORTED_WITHIN_MS = 2000;
|
|
31051
|
-
async function untilGone(
|
|
31052
|
-
const gone = () =>
|
|
31069
|
+
async function untilGone(pids, withinMs = STOP_WAIT_MS) {
|
|
31070
|
+
const gone = () => pids.every((pid) => !alive(pid));
|
|
31053
31071
|
const deadline = Date.now() + withinMs;
|
|
31054
31072
|
while (!gone() && Date.now() < deadline) {
|
|
31055
31073
|
await delay3(50);
|
|
@@ -31057,6 +31075,9 @@ async function untilGone(handle, pids, withinMs = STOP_WAIT_MS) {
|
|
|
31057
31075
|
return gone();
|
|
31058
31076
|
}
|
|
31059
31077
|
function endedWithoutACode(run) {
|
|
31078
|
+
if (!run.throughEditor && !run.pickedUp) {
|
|
31079
|
+
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.";
|
|
31080
|
+
}
|
|
31060
31081
|
if (!run.throughEditor) {
|
|
31061
31082
|
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.";
|
|
31062
31083
|
}
|
|
@@ -31263,6 +31284,34 @@ var SUCCESSOR_CHECK_MS = 1e4;
|
|
|
31263
31284
|
var HANDOVER_MS = SUCCESSOR_CHECK_MS + 5000;
|
|
31264
31285
|
var HANDOVER_POLL_MS = 250;
|
|
31265
31286
|
var CACHE_WRITE_MS = 2000;
|
|
31287
|
+
var SCAN_WAIT_MS = 30000;
|
|
31288
|
+
function scanWaitIn(payload) {
|
|
31289
|
+
const waited = readNumber(payload, "waitedForEditorScanMs");
|
|
31290
|
+
const note = readString(payload, "scanNote");
|
|
31291
|
+
return {
|
|
31292
|
+
...waited === undefined ? {} : { waitedForEditorScanMs: waited },
|
|
31293
|
+
...note === undefined ? {} : { scanNote: note }
|
|
31294
|
+
};
|
|
31295
|
+
}
|
|
31296
|
+
function withScanWait(outcome, scanned) {
|
|
31297
|
+
if (scanned.waitedMs === 0) {
|
|
31298
|
+
return outcome;
|
|
31299
|
+
}
|
|
31300
|
+
const said = scanWaitAnswer(scanned);
|
|
31301
|
+
if (outcome.ok) {
|
|
31302
|
+
return { ...outcome, payload: { ...outcome.payload, ...said } };
|
|
31303
|
+
}
|
|
31304
|
+
return said.scanNote === undefined ? outcome : { ...outcome, message: `${outcome.message} ${said.scanNote}` };
|
|
31305
|
+
}
|
|
31306
|
+
function scanWaitAnswer(scanned) {
|
|
31307
|
+
if (scanned.waitedMs === 0) {
|
|
31308
|
+
return {};
|
|
31309
|
+
}
|
|
31310
|
+
return {
|
|
31311
|
+
waitedForEditorScanMs: scanned.waitedMs,
|
|
31312
|
+
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
|
|
31313
|
+
};
|
|
31314
|
+
}
|
|
31266
31315
|
var PLAYING_STATUS_MS = 1000;
|
|
31267
31316
|
var SCAN_START_MS = 2000;
|
|
31268
31317
|
var WAIT_FOR_RUN_MS = 600000;
|
|
@@ -32230,6 +32279,47 @@ class GodotServer {
|
|
|
32230
32279
|
}
|
|
32231
32280
|
return { ok: true, value: stale };
|
|
32232
32281
|
}
|
|
32282
|
+
async untilTheEditorsScanIsWritten(projectPath) {
|
|
32283
|
+
const open = this.godotBridge.getStatus().projectPath;
|
|
32284
|
+
if (!this.godotBridge.isConnected() || open === undefined || !isSameDirectory(open, projectPath)) {
|
|
32285
|
+
return { waitedMs: 0, stillScanning: false };
|
|
32286
|
+
}
|
|
32287
|
+
const started = Date.now();
|
|
32288
|
+
let sawAScan = false;
|
|
32289
|
+
let scanEndedAt = null;
|
|
32290
|
+
for (;; ) {
|
|
32291
|
+
const asked = Date.now();
|
|
32292
|
+
let status;
|
|
32293
|
+
try {
|
|
32294
|
+
status = asParams(await this.godotBridge.invokeTool("scan_status", {}));
|
|
32295
|
+
} catch {
|
|
32296
|
+
return { waitedMs: Date.now() - started, stillScanning: false };
|
|
32297
|
+
}
|
|
32298
|
+
if (status["scanning"] === true || status["importing"] === true || status["pending"] === true) {
|
|
32299
|
+
sawAScan = true;
|
|
32300
|
+
if (Date.now() - started >= SCAN_WAIT_MS) {
|
|
32301
|
+
return { waitedMs: Date.now() - started, stillScanning: true };
|
|
32302
|
+
}
|
|
32303
|
+
await new Promise((settle) => setTimeout(settle, 100));
|
|
32304
|
+
continue;
|
|
32305
|
+
}
|
|
32306
|
+
const since = readNumber(status, "sinceScanFinishedMs");
|
|
32307
|
+
scanEndedAt = since !== undefined && since >= 0 ? asked - since : sawAScan ? asked : null;
|
|
32308
|
+
break;
|
|
32309
|
+
}
|
|
32310
|
+
let waitedForTheWrite = false;
|
|
32311
|
+
if (scanEndedAt !== null) {
|
|
32312
|
+
const writeDue = scanEndedAt + CACHE_WRITE_MS;
|
|
32313
|
+
while (Date.now() < writeDue && (cacheWrittenAt(projectPath) ?? 0) < scanEndedAt) {
|
|
32314
|
+
waitedForTheWrite = true;
|
|
32315
|
+
await new Promise((settle) => setTimeout(settle, 50));
|
|
32316
|
+
}
|
|
32317
|
+
}
|
|
32318
|
+
return {
|
|
32319
|
+
waitedMs: sawAScan || waitedForTheWrite ? Date.now() - started : 0,
|
|
32320
|
+
stillScanning: false
|
|
32321
|
+
};
|
|
32322
|
+
}
|
|
32233
32323
|
containProjectFiles(args) {
|
|
32234
32324
|
const projectPath = readString(args, "projectPath");
|
|
32235
32325
|
if (!projectPath) {
|
|
@@ -32290,14 +32380,15 @@ class GodotServer {
|
|
|
32290
32380
|
return engine.response;
|
|
32291
32381
|
}
|
|
32292
32382
|
const projectPath = project.value.path;
|
|
32383
|
+
const scanned = await this.untilTheEditorsScanIsWritten(projectPath);
|
|
32293
32384
|
const before = scriptsWithoutUid(projectPath);
|
|
32294
32385
|
const imported = await runImport(engine.value, projectPath);
|
|
32295
32386
|
if (!imported.ok) {
|
|
32296
|
-
return this.answer(imported);
|
|
32387
|
+
return this.answer(withScanWait(imported, scanned));
|
|
32297
32388
|
}
|
|
32298
32389
|
const after = scriptsWithoutUid(projectPath);
|
|
32299
32390
|
const given = before.filter((script) => !after.includes(script));
|
|
32300
|
-
return this.answer({
|
|
32391
|
+
return this.answer(withScanWait({
|
|
32301
32392
|
ok: true,
|
|
32302
32393
|
messages: imported.messages,
|
|
32303
32394
|
payload: {
|
|
@@ -32305,7 +32396,7 @@ class GodotServer {
|
|
|
32305
32396
|
stillWithoutUid: after,
|
|
32306
32397
|
note: uidsLeftNote(after.length)
|
|
32307
32398
|
}
|
|
32308
|
-
});
|
|
32399
|
+
}, scanned));
|
|
32309
32400
|
}
|
|
32310
32401
|
async operation(operation, params, projectPath) {
|
|
32311
32402
|
const engine = await this.engine();
|
|
@@ -32316,8 +32407,10 @@ class GodotServer {
|
|
|
32316
32407
|
messages: []
|
|
32317
32408
|
};
|
|
32318
32409
|
}
|
|
32410
|
+
const scanned = await this.untilTheEditorsScanIsWritten(projectPath);
|
|
32319
32411
|
this.logDebug(`Running ${operation} in ${projectPath}: ${JSON.stringify(params)}`);
|
|
32320
|
-
|
|
32412
|
+
const outcome = await runOperation({ godotPath: engine.value, script: this.operationsScript, debug: GODOT_DEBUG_MODE_DEFAULT }, operation, params, projectPath);
|
|
32413
|
+
return withScanWait(outcome, scanned);
|
|
32321
32414
|
}
|
|
32322
32415
|
answer(outcome) {
|
|
32323
32416
|
if (!outcome.ok) {
|
|
@@ -32499,6 +32592,7 @@ class GodotServer {
|
|
|
32499
32592
|
if (!classes.ok) {
|
|
32500
32593
|
return this.answer(classes);
|
|
32501
32594
|
}
|
|
32595
|
+
const scanned = scanWaitIn(classes.payload);
|
|
32502
32596
|
const ours = `gdharness-reports/${randomUUID2()}`;
|
|
32503
32597
|
const reports = `res://.godot/${ours}`;
|
|
32504
32598
|
const ignored = readStringArray(args, "ignore") ?? [];
|
|
@@ -32589,7 +32683,8 @@ class GodotServer {
|
|
|
32589
32683
|
hung,
|
|
32590
32684
|
arguments: cmdArgs,
|
|
32591
32685
|
entries: forAnswer(printed.slice(0, 60)),
|
|
32592
|
-
savesNote
|
|
32686
|
+
savesNote,
|
|
32687
|
+
...scanned
|
|
32593
32688
|
}, null, 2)
|
|
32594
32689
|
}
|
|
32595
32690
|
],
|
|
@@ -32624,6 +32719,7 @@ class GodotServer {
|
|
|
32624
32719
|
orphans: orphans.total > 0 ? orphans.total : undefined,
|
|
32625
32720
|
notRun: notRun > 0 ? notRun : undefined,
|
|
32626
32721
|
savesNote,
|
|
32722
|
+
...scanned,
|
|
32627
32723
|
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,
|
|
32628
32724
|
suites: unclean.map((suite) => ({
|
|
32629
32725
|
name: suite.name,
|
|
@@ -32781,7 +32877,12 @@ class GodotServer {
|
|
|
32781
32877
|
return anEditorIsStillComing(listeningSince, this.launchedEditorIsUp());
|
|
32782
32878
|
}
|
|
32783
32879
|
launchedEditorIsUp() {
|
|
32784
|
-
|
|
32880
|
+
const launched = this.launchedEditor;
|
|
32881
|
+
if (launched === null || !alive(launched.pid)) {
|
|
32882
|
+
return false;
|
|
32883
|
+
}
|
|
32884
|
+
const now = startTimesOf([launched.pid]).get(launched.pid);
|
|
32885
|
+
return launched.startedAt === null || now === undefined || now === launched.startedAt;
|
|
32785
32886
|
}
|
|
32786
32887
|
dap() {
|
|
32787
32888
|
const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
|
|
@@ -32922,6 +33023,28 @@ class GodotServer {
|
|
|
32922
33023
|
return null;
|
|
32923
33024
|
}
|
|
32924
33025
|
}
|
|
33026
|
+
async editorScanState() {
|
|
33027
|
+
if (!this.godotBridge.getStatus().connected) {
|
|
33028
|
+
return null;
|
|
33029
|
+
}
|
|
33030
|
+
try {
|
|
33031
|
+
const answer = await Promise.race([
|
|
33032
|
+
this.godotBridge.invokeTool("scan_status", {}),
|
|
33033
|
+
delay3(PLAYING_STATUS_MS).then(() => null)
|
|
33034
|
+
]);
|
|
33035
|
+
if (answer === null) {
|
|
33036
|
+
return null;
|
|
33037
|
+
}
|
|
33038
|
+
const since = readNumber(asParams(answer), "sinceScanFinishedMs");
|
|
33039
|
+
return {
|
|
33040
|
+
scanning: readBoolean(asParams(answer), "scanning") ?? false,
|
|
33041
|
+
importing: readBoolean(asParams(answer), "importing") ?? false,
|
|
33042
|
+
finishedMsAgo: since === undefined || since < 0 ? null : since
|
|
33043
|
+
};
|
|
33044
|
+
} catch {
|
|
33045
|
+
return null;
|
|
33046
|
+
}
|
|
33047
|
+
}
|
|
32925
33048
|
async handleEditorStatus() {
|
|
32926
33049
|
const located = await this.locator.find();
|
|
32927
33050
|
const announced = runtimesAnnounced();
|
|
@@ -32952,11 +33075,16 @@ class GodotServer {
|
|
|
32952
33075
|
hold: current === null ? {} : await this.holdOf(current, active)
|
|
32953
33076
|
};
|
|
32954
33077
|
})();
|
|
32955
|
-
const [games, { playing, active, hold }] = await Promise.all([
|
|
33078
|
+
const [games, { playing, active, hold }, scan] = await Promise.all([
|
|
33079
|
+
pinged,
|
|
33080
|
+
asked,
|
|
33081
|
+
this.editorScanState()
|
|
33082
|
+
]);
|
|
32956
33083
|
return this.jsonTextResponse({
|
|
32957
33084
|
editor: {
|
|
32958
33085
|
...this.getEditorStatusPayload(),
|
|
32959
|
-
debugPort: playing?.debugPort ?? this.godotBridge.getStatus().debugPort
|
|
33086
|
+
debugPort: playing?.debugPort ?? this.godotBridge.getStatus().debugPort,
|
|
33087
|
+
scan
|
|
32960
33088
|
},
|
|
32961
33089
|
godot: {
|
|
32962
33090
|
path: located.ok ? located.path : null,
|
|
@@ -33115,9 +33243,9 @@ class GodotServer {
|
|
|
33115
33243
|
}
|
|
33116
33244
|
async openAnEditor(engine, projectPath, ports) {
|
|
33117
33245
|
clearEditorLog(projectPath);
|
|
33118
|
-
const
|
|
33119
|
-
|
|
33120
|
-
|
|
33246
|
+
const launched = await launchOutsideTheTree({
|
|
33247
|
+
command: engine,
|
|
33248
|
+
args: editorArguments(projectPath, ports, editorLogPath(projectPath)),
|
|
33121
33249
|
env: {
|
|
33122
33250
|
...process.env,
|
|
33123
33251
|
GDHARNESS_RUNTIME_DIR: runtimeDirectory(),
|
|
@@ -33126,23 +33254,15 @@ class GodotServer {
|
|
|
33126
33254
|
[OPENED_BY_A_SERVER]: "1"
|
|
33127
33255
|
}
|
|
33128
33256
|
});
|
|
33129
|
-
if (
|
|
33130
|
-
|
|
33257
|
+
if ("error" in launched) {
|
|
33258
|
+
return { pid: null, error: launched.error };
|
|
33131
33259
|
}
|
|
33132
|
-
|
|
33133
|
-
|
|
33134
|
-
|
|
33135
|
-
|
|
33136
|
-
|
|
33137
|
-
|
|
33138
|
-
});
|
|
33139
|
-
});
|
|
33140
|
-
if (started !== null) {
|
|
33141
|
-
return { pid: null, error: started };
|
|
33142
|
-
}
|
|
33143
|
-
editor.unref();
|
|
33144
|
-
this.launchedEditor = editor.pid === undefined ? null : editor;
|
|
33145
|
-
return { pid: editor.pid ?? null, error: null };
|
|
33260
|
+
writeEditorLogNote(projectPath, launched.pid);
|
|
33261
|
+
this.launchedEditor = {
|
|
33262
|
+
pid: launched.pid,
|
|
33263
|
+
startedAt: startTimesOf([launched.pid]).get(launched.pid) ?? null
|
|
33264
|
+
};
|
|
33265
|
+
return { pid: launched.pid, error: null };
|
|
33146
33266
|
}
|
|
33147
33267
|
async portsForAnEditor() {
|
|
33148
33268
|
const lsp = portFromEnvOrNull("GDHARNESS_LSP_PORT") ?? await freePort(DEFAULT_LSP_PORT);
|
|
@@ -33177,13 +33297,14 @@ class GodotServer {
|
|
|
33177
33297
|
if (!engine.ok) {
|
|
33178
33298
|
return engine.response;
|
|
33179
33299
|
}
|
|
33300
|
+
const scanned = await this.untilTheEditorsScanIsWritten(project.value.path);
|
|
33180
33301
|
const refreshed = await this.refreshStaleClasses(project.value.path);
|
|
33181
33302
|
if (!refreshed.ok) {
|
|
33182
33303
|
return refreshed.response;
|
|
33183
33304
|
}
|
|
33184
33305
|
const sceneArgument = sceneToRun?.ok ? sceneToRun.relativePath : null;
|
|
33185
33306
|
if (op === "check") {
|
|
33186
|
-
return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value, asked.value);
|
|
33307
|
+
return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value, scanned, asked.value);
|
|
33187
33308
|
}
|
|
33188
33309
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
33189
33310
|
let ended = null;
|
|
@@ -33202,7 +33323,7 @@ class GodotServer {
|
|
|
33202
33323
|
const runtimeWaitMs = this.announceBudget(args, project.value.path);
|
|
33203
33324
|
const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
|
|
33204
33325
|
if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && !needsItsOwnProcess(asked.value)) {
|
|
33205
|
-
return await this.playThroughEditor(sceneArgument, refreshed.value, project.value.path, alreadyPlaying, runtimeWaitMs, ended);
|
|
33326
|
+
return await this.playThroughEditor(sceneArgument, refreshed.value, scanned, project.value.path, alreadyPlaying, runtimeWaitMs, ended);
|
|
33206
33327
|
}
|
|
33207
33328
|
const cmdArgs = runArguments({
|
|
33208
33329
|
projectPath: project.value.path,
|
|
@@ -33211,7 +33332,12 @@ class GodotServer {
|
|
|
33211
33332
|
userArgs: given.value
|
|
33212
33333
|
});
|
|
33213
33334
|
this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
33214
|
-
const started = this.
|
|
33335
|
+
const started = await this.launchKeptGame(engine.value, cmdArgs, project.value.path, needsItsOwnProcess(asked.value) ? environmentFor({ ...asked.value, runtimeDirectory: runtimeDirectory() }) : undefined);
|
|
33336
|
+
if ("error" in started) {
|
|
33337
|
+
return this.createErrorResponse(`The game could not be started: ${started.error}`, [
|
|
33338
|
+
"editor_status says which Godot this server runs and whether it answers"
|
|
33339
|
+
]);
|
|
33340
|
+
}
|
|
33215
33341
|
started.announcedBefore = alreadyPlaying;
|
|
33216
33342
|
this.activeProcess = started;
|
|
33217
33343
|
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." : "";
|
|
@@ -33223,10 +33349,11 @@ class GodotServer {
|
|
|
33223
33349
|
started: true,
|
|
33224
33350
|
through: "gdharness",
|
|
33225
33351
|
alsoRunning: stranded.length === 0 ? undefined : stranded.map((one) => one.pid),
|
|
33226
|
-
pid: started.
|
|
33352
|
+
pid: started.pid,
|
|
33227
33353
|
arguments: cmdArgs,
|
|
33228
33354
|
transcript: started.transcript,
|
|
33229
33355
|
refreshedClasses: refreshed.value,
|
|
33356
|
+
...scanWaitAnswer(scanned),
|
|
33230
33357
|
savesNote: asked.value.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
|
|
33231
33358
|
endedPreviousRun: endedPreviousRun(ended),
|
|
33232
33359
|
runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
|
|
@@ -33308,7 +33435,7 @@ class GodotServer {
|
|
|
33308
33435
|
writeBootNote(run.projectPath, took);
|
|
33309
33436
|
}
|
|
33310
33437
|
}
|
|
33311
|
-
async playThroughEditor(scene, refreshedClasses, projectPath, alreadyPlaying, runtimeWaitMs, ended) {
|
|
33438
|
+
async playThroughEditor(scene, refreshedClasses, scanned, projectPath, alreadyPlaying, runtimeWaitMs, ended) {
|
|
33312
33439
|
const log = new GameLog;
|
|
33313
33440
|
try {
|
|
33314
33441
|
await this.dap().connect();
|
|
@@ -33337,7 +33464,6 @@ class GodotServer {
|
|
|
33337
33464
|
closeSync2(transcript.fd);
|
|
33338
33465
|
const playAnswer = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
|
|
33339
33466
|
const played = {
|
|
33340
|
-
process: null,
|
|
33341
33467
|
pid: null,
|
|
33342
33468
|
log,
|
|
33343
33469
|
transcript: transcript.path,
|
|
@@ -33359,6 +33485,7 @@ class GodotServer {
|
|
|
33359
33485
|
through: "editor",
|
|
33360
33486
|
scene: scene === null ? "the main scene" : `res://${scene}`,
|
|
33361
33487
|
refreshedClasses,
|
|
33488
|
+
...scanWaitAnswer(scanned),
|
|
33362
33489
|
debugPort: readNumber(playAnswer, "debugPort"),
|
|
33363
33490
|
endedPreviousRun: endedPreviousRun(ended),
|
|
33364
33491
|
breakpoints: breakpoints.applied.length === 0 ? undefined : this.breakpointsAsProjectSpells(projectPath, breakpoints.applied),
|
|
@@ -33435,8 +33562,14 @@ class GodotServer {
|
|
|
33435
33562
|
}
|
|
33436
33563
|
async endActiveGame(reason, going) {
|
|
33437
33564
|
const running = this.activeProcess;
|
|
33438
|
-
const recorded = running
|
|
33439
|
-
|
|
33565
|
+
const recorded = running !== null && !running.throughEditor ? readRunRecord() : null;
|
|
33566
|
+
const clearTheRecord = () => {
|
|
33567
|
+
clearRunRecord((record) => this.couldBeOurs(record.projectPath));
|
|
33568
|
+
};
|
|
33569
|
+
const aKeeperHoldsIt = running !== null && going && !running.throughEditor && running.pid !== null;
|
|
33570
|
+
if (!aKeeperHoldsIt) {
|
|
33571
|
+
clearTheRecord();
|
|
33572
|
+
}
|
|
33440
33573
|
if (!running || !going) {
|
|
33441
33574
|
return true;
|
|
33442
33575
|
}
|
|
@@ -33444,16 +33577,13 @@ class GodotServer {
|
|
|
33444
33577
|
const announced = this.announcedPidOf(running);
|
|
33445
33578
|
if (running.throughEditor) {
|
|
33446
33579
|
await this.handleViaBridge("stop_playing", {});
|
|
33447
|
-
return await untilGone(
|
|
33448
|
-
}
|
|
33449
|
-
if (running.process !== null) {
|
|
33450
|
-
running.process.kill();
|
|
33451
|
-
return await untilGone(running.process, announced === undefined ? [] : [announced]);
|
|
33580
|
+
return await untilGone(announced === undefined ? [] : [announced]);
|
|
33452
33581
|
}
|
|
33453
33582
|
if (running.pid === null) {
|
|
33454
33583
|
return true;
|
|
33455
33584
|
}
|
|
33456
33585
|
if (recorded === null || recorded.pid !== running.pid || !stillTheRecordedRun(recorded)) {
|
|
33586
|
+
clearTheRecord();
|
|
33457
33587
|
running.endedHere = null;
|
|
33458
33588
|
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.`);
|
|
33459
33589
|
return true;
|
|
@@ -33466,12 +33596,20 @@ class GodotServer {
|
|
|
33466
33596
|
} catch {
|
|
33467
33597
|
landed = false;
|
|
33468
33598
|
}
|
|
33469
|
-
const gone = await untilGone(
|
|
33599
|
+
const gone = await untilGone([running.pid]);
|
|
33600
|
+
if (gone) {
|
|
33601
|
+
await this.untilTheExitIsRecorded(running);
|
|
33602
|
+
}
|
|
33603
|
+
if (landed && gone) {
|
|
33604
|
+
running.exitCode = null;
|
|
33605
|
+
running.exitSignal = "SIGTERM";
|
|
33606
|
+
}
|
|
33607
|
+
clearTheRecord();
|
|
33470
33608
|
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}.`);
|
|
33471
33609
|
return gone;
|
|
33472
33610
|
}
|
|
33473
33611
|
spawnGame(godotPath, cmdArgs, env) {
|
|
33474
|
-
const child =
|
|
33612
|
+
const child = spawn2(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
|
|
33475
33613
|
const cancelled = callSignal();
|
|
33476
33614
|
if (cancelled !== undefined) {
|
|
33477
33615
|
const end = () => {
|
|
@@ -33523,25 +33661,23 @@ class GodotServer {
|
|
|
33523
33661
|
});
|
|
33524
33662
|
return started;
|
|
33525
33663
|
}
|
|
33526
|
-
|
|
33664
|
+
async launchKeptGame(godotPath, cmdArgs, projectPath, env) {
|
|
33527
33665
|
const startedAt = Date.now();
|
|
33528
33666
|
const transcript = openTranscript(startedAt);
|
|
33529
|
-
|
|
33530
|
-
|
|
33531
|
-
|
|
33532
|
-
|
|
33533
|
-
|
|
33534
|
-
|
|
33535
|
-
|
|
33536
|
-
|
|
33537
|
-
|
|
33667
|
+
closeSync2(transcript.fd);
|
|
33668
|
+
const launched = await launchOutsideTheTree({
|
|
33669
|
+
command: godotPath,
|
|
33670
|
+
args: cmdArgs,
|
|
33671
|
+
...env === undefined ? {} : { env },
|
|
33672
|
+
run: { transcript: transcript.path, startedAt, projectPath }
|
|
33673
|
+
});
|
|
33674
|
+
if ("error" in launched) {
|
|
33675
|
+
return launched;
|
|
33538
33676
|
}
|
|
33539
|
-
|
|
33540
|
-
|
|
33541
|
-
|
|
33542
|
-
|
|
33543
|
-
pid: child.pid ?? null,
|
|
33544
|
-
log,
|
|
33677
|
+
sweepTranscripts();
|
|
33678
|
+
return {
|
|
33679
|
+
pid: launched.pid,
|
|
33680
|
+
log: new GameLog,
|
|
33545
33681
|
transcript: transcript.path,
|
|
33546
33682
|
readOffset: 0,
|
|
33547
33683
|
projectPath,
|
|
@@ -33551,34 +33687,6 @@ class GodotServer {
|
|
|
33551
33687
|
throughEditor: false,
|
|
33552
33688
|
brokeOn: null
|
|
33553
33689
|
};
|
|
33554
|
-
child.on("exit", (code, signal) => {
|
|
33555
|
-
this.logDebug(`Godot process exited with code ${code ?? "none"}, signal ${signal ?? "none"}`);
|
|
33556
|
-
this.drainTranscript(started);
|
|
33557
|
-
log.finish();
|
|
33558
|
-
started.exitCode = code;
|
|
33559
|
-
started.exitSignal = code === null ? signal : null;
|
|
33560
|
-
if (started.pid !== null) {
|
|
33561
|
-
recordRunEnded(started.pid, { exitCode: started.exitCode, exitSignal: started.exitSignal });
|
|
33562
|
-
}
|
|
33563
|
-
});
|
|
33564
|
-
child.on("error", (err) => {
|
|
33565
|
-
console.error("Failed to start Godot process:", err);
|
|
33566
|
-
log.append("stderr", `${err.message}
|
|
33567
|
-
`);
|
|
33568
|
-
started.exitCode = -1;
|
|
33569
|
-
});
|
|
33570
|
-
if (started.pid !== null) {
|
|
33571
|
-
writeRunRecord({
|
|
33572
|
-
pid: started.pid,
|
|
33573
|
-
transcript: transcript.path,
|
|
33574
|
-
startedAt,
|
|
33575
|
-
projectPath,
|
|
33576
|
-
arguments: cmdArgs,
|
|
33577
|
-
command: godotPath
|
|
33578
|
-
});
|
|
33579
|
-
}
|
|
33580
|
-
sweepTranscripts();
|
|
33581
|
-
return started;
|
|
33582
33690
|
}
|
|
33583
33691
|
drainTranscript(game) {
|
|
33584
33692
|
if (game.transcript === null) {
|
|
@@ -33634,7 +33742,6 @@ class GodotServer {
|
|
|
33634
33742
|
const left = readEditorRunNote();
|
|
33635
33743
|
const note = left !== null && project !== null && isSameDirectory(left.projectPath, project) ? left : null;
|
|
33636
33744
|
const picked = {
|
|
33637
|
-
process: null,
|
|
33638
33745
|
pid: null,
|
|
33639
33746
|
log: new GameLog,
|
|
33640
33747
|
transcript: note?.transcript ?? null,
|
|
@@ -33771,7 +33878,6 @@ class GodotServer {
|
|
|
33771
33878
|
return null;
|
|
33772
33879
|
}
|
|
33773
33880
|
const adopted = {
|
|
33774
|
-
process: null,
|
|
33775
33881
|
pid: record.pid,
|
|
33776
33882
|
log: new GameLog,
|
|
33777
33883
|
transcript: record.transcript,
|
|
@@ -33781,7 +33887,8 @@ class GodotServer {
|
|
|
33781
33887
|
exitCode: null,
|
|
33782
33888
|
exitSignal: null,
|
|
33783
33889
|
throughEditor: false,
|
|
33784
|
-
brokeOn: null
|
|
33890
|
+
brokeOn: null,
|
|
33891
|
+
pickedUp: true
|
|
33785
33892
|
};
|
|
33786
33893
|
this.drainTranscript(adopted);
|
|
33787
33894
|
if (!alive(record.pid) || !couldStillBeTheRecordedRun(record)) {
|
|
@@ -33862,7 +33969,7 @@ class GodotServer {
|
|
|
33862
33969
|
currentRun() {
|
|
33863
33970
|
return this.activeProcess ?? this.adoptRecordedRun();
|
|
33864
33971
|
}
|
|
33865
|
-
async checkBoot(godotPath, projectPath, scene, args, userArgs, asked = {}) {
|
|
33972
|
+
async checkBoot(godotPath, projectPath, scene, args, userArgs, scanned, asked = {}) {
|
|
33866
33973
|
const frames = readPositiveNumber(args, "frames") ?? 3;
|
|
33867
33974
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 60000;
|
|
33868
33975
|
const cmdArgs = runArguments({ projectPath, headless: true, scene, quitAfter: frames, userArgs });
|
|
@@ -33892,6 +33999,7 @@ class GodotServer {
|
|
|
33892
33999
|
frames,
|
|
33893
34000
|
errors,
|
|
33894
34001
|
warnings,
|
|
34002
|
+
...scanWaitAnswer(scanned),
|
|
33895
34003
|
savesNote: asked.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
|
|
33896
34004
|
entries: forAnswer(boot.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
33897
34005
|
});
|
|
@@ -33915,8 +34023,8 @@ class GodotServer {
|
|
|
33915
34023
|
async runStillGoing(run, editorSays) {
|
|
33916
34024
|
if (!run.throughEditor) {
|
|
33917
34025
|
const going = stillRunning(run);
|
|
33918
|
-
if (!going && run.
|
|
33919
|
-
|
|
34026
|
+
if (!going && run.pid !== null && !exited(run) && run.endedUnwatched !== true && typeof run.endedHere !== "string" && !await this.untilTheExitIsRecorded(run)) {
|
|
34027
|
+
run.endedUnwatched = true;
|
|
33920
34028
|
}
|
|
33921
34029
|
return going;
|
|
33922
34030
|
}
|
|
@@ -33933,6 +34041,21 @@ class GodotServer {
|
|
|
33933
34041
|
}
|
|
33934
34042
|
return runIsUp(run, said);
|
|
33935
34043
|
}
|
|
34044
|
+
async untilTheExitIsRecorded(run) {
|
|
34045
|
+
const until = Date.now() + EXIT_REPORTED_WITHIN_MS;
|
|
34046
|
+
for (;; ) {
|
|
34047
|
+
const record = readRunRecord();
|
|
34048
|
+
if (record !== null && record.pid === run.pid && (record.exitCode !== undefined || record.exitSignal !== undefined)) {
|
|
34049
|
+
run.exitCode = record.exitCode ?? null;
|
|
34050
|
+
run.exitSignal = record.exitSignal ?? null;
|
|
34051
|
+
return true;
|
|
34052
|
+
}
|
|
34053
|
+
if (Date.now() >= until) {
|
|
34054
|
+
return false;
|
|
34055
|
+
}
|
|
34056
|
+
await delay3(50);
|
|
34057
|
+
}
|
|
34058
|
+
}
|
|
33936
34059
|
handleEditorConsole(args) {
|
|
33937
34060
|
const status = this.godotBridge.getStatus();
|
|
33938
34061
|
const projectPath = status.projectPath ?? this.ownProject ?? undefined;
|
|
@@ -34034,7 +34157,8 @@ class GodotServer {
|
|
|
34034
34157
|
throughEditor: run.throughEditor,
|
|
34035
34158
|
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
34036
34159
|
couldAnnounce: run.projectPath !== null && existsSync10(join16(run.projectPath, RUNTIME_AUTOLOAD.path)),
|
|
34037
|
-
endedHere: typeof run.endedHere === "string"
|
|
34160
|
+
endedHere: typeof run.endedHere === "string",
|
|
34161
|
+
pickedUp: run.pickedUp === true
|
|
34038
34162
|
}));
|
|
34039
34163
|
}
|
|
34040
34164
|
if (run.pickedUpPlaying === true) {
|
|
@@ -34060,7 +34184,7 @@ class GodotServer {
|
|
|
34060
34184
|
...hold,
|
|
34061
34185
|
endedUnwatched: run.endedUnwatched === true ? true : undefined,
|
|
34062
34186
|
endedBy: stillRunning(run) ? undefined : run.endedHere ?? null,
|
|
34063
|
-
project:
|
|
34187
|
+
project: run.throughEditor ? undefined : run.projectPath ?? undefined,
|
|
34064
34188
|
transcript: run.transcript ?? undefined,
|
|
34065
34189
|
startedAt: new Date(run.startedAt).toISOString(),
|
|
34066
34190
|
elapsedMs: Date.now() - run.startedAt,
|
|
@@ -34275,6 +34399,10 @@ class GodotServer {
|
|
|
34275
34399
|
return now === null ? [] : lost.filter((name) => now.has(name));
|
|
34276
34400
|
}
|
|
34277
34401
|
async rebuildClassCache(projectPath) {
|
|
34402
|
+
const scanned = await this.untilTheEditorsScanIsWritten(projectPath);
|
|
34403
|
+
return withScanWait(await this.rebuildClassCacheFromDisk(projectPath), scanned);
|
|
34404
|
+
}
|
|
34405
|
+
async rebuildClassCacheFromDisk(projectPath) {
|
|
34278
34406
|
const before = cachedClasses(projectPath);
|
|
34279
34407
|
const writtenBefore = cacheWrittenAt(projectPath);
|
|
34280
34408
|
const noted = readClassNote(projectPath);
|