gdharness 1.0.29 → 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 +134 -112
- package/build/index.js +132 -114
- package/build/keeper.js +222 -0
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -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
|
}
|
|
@@ -40116,7 +40140,12 @@ class GodotServer {
|
|
|
40116
40140
|
return anEditorIsStillComing(listeningSince, this.launchedEditorIsUp());
|
|
40117
40141
|
}
|
|
40118
40142
|
launchedEditorIsUp() {
|
|
40119
|
-
|
|
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;
|
|
40120
40149
|
}
|
|
40121
40150
|
dap() {
|
|
40122
40151
|
const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
|
|
@@ -40477,9 +40506,9 @@ class GodotServer {
|
|
|
40477
40506
|
}
|
|
40478
40507
|
async openAnEditor(engine, projectPath, ports) {
|
|
40479
40508
|
clearEditorLog(projectPath);
|
|
40480
|
-
const
|
|
40481
|
-
|
|
40482
|
-
|
|
40509
|
+
const launched = await launchOutsideTheTree({
|
|
40510
|
+
command: engine,
|
|
40511
|
+
args: editorArguments(projectPath, ports, editorLogPath(projectPath)),
|
|
40483
40512
|
env: {
|
|
40484
40513
|
...process.env,
|
|
40485
40514
|
GDHARNESS_RUNTIME_DIR: runtimeDirectory(),
|
|
@@ -40488,23 +40517,15 @@ class GodotServer {
|
|
|
40488
40517
|
[OPENED_BY_A_SERVER]: "1"
|
|
40489
40518
|
}
|
|
40490
40519
|
});
|
|
40491
|
-
if (
|
|
40492
|
-
|
|
40520
|
+
if ("error" in launched) {
|
|
40521
|
+
return { pid: null, error: launched.error };
|
|
40493
40522
|
}
|
|
40494
|
-
|
|
40495
|
-
|
|
40496
|
-
|
|
40497
|
-
|
|
40498
|
-
|
|
40499
|
-
|
|
40500
|
-
});
|
|
40501
|
-
});
|
|
40502
|
-
if (started !== null) {
|
|
40503
|
-
return { pid: null, error: started };
|
|
40504
|
-
}
|
|
40505
|
-
editor.unref();
|
|
40506
|
-
this.launchedEditor = editor.pid === undefined ? null : editor;
|
|
40507
|
-
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 };
|
|
40508
40529
|
}
|
|
40509
40530
|
async portsForAnEditor() {
|
|
40510
40531
|
const lsp = portFromEnvOrNull("GDHARNESS_LSP_PORT") ?? await freePort(DEFAULT_LSP_PORT);
|
|
@@ -40574,7 +40595,12 @@ class GodotServer {
|
|
|
40574
40595
|
userArgs: given.value
|
|
40575
40596
|
});
|
|
40576
40597
|
this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
40577
|
-
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
|
+
}
|
|
40578
40604
|
started.announcedBefore = alreadyPlaying;
|
|
40579
40605
|
this.activeProcess = started;
|
|
40580
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." : "";
|
|
@@ -40586,7 +40612,7 @@ class GodotServer {
|
|
|
40586
40612
|
started: true,
|
|
40587
40613
|
through: "gdharness",
|
|
40588
40614
|
alsoRunning: stranded.length === 0 ? undefined : stranded.map((one) => one.pid),
|
|
40589
|
-
pid: started.
|
|
40615
|
+
pid: started.pid,
|
|
40590
40616
|
arguments: cmdArgs,
|
|
40591
40617
|
transcript: started.transcript,
|
|
40592
40618
|
refreshedClasses: refreshed.value,
|
|
@@ -40701,7 +40727,6 @@ class GodotServer {
|
|
|
40701
40727
|
closeSync2(transcript.fd);
|
|
40702
40728
|
const playAnswer = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
|
|
40703
40729
|
const played = {
|
|
40704
|
-
process: null,
|
|
40705
40730
|
pid: null,
|
|
40706
40731
|
log,
|
|
40707
40732
|
transcript: transcript.path,
|
|
@@ -40800,8 +40825,14 @@ class GodotServer {
|
|
|
40800
40825
|
}
|
|
40801
40826
|
async endActiveGame(reason, going) {
|
|
40802
40827
|
const running = this.activeProcess;
|
|
40803
|
-
const recorded = running
|
|
40804
|
-
|
|
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
|
+
}
|
|
40805
40836
|
if (!running || !going) {
|
|
40806
40837
|
return true;
|
|
40807
40838
|
}
|
|
@@ -40809,16 +40840,13 @@ class GodotServer {
|
|
|
40809
40840
|
const announced = this.announcedPidOf(running);
|
|
40810
40841
|
if (running.throughEditor) {
|
|
40811
40842
|
await this.handleViaBridge("stop_playing", {});
|
|
40812
|
-
return await untilGone(
|
|
40813
|
-
}
|
|
40814
|
-
if (running.process !== null) {
|
|
40815
|
-
running.process.kill();
|
|
40816
|
-
return await untilGone(running.process, announced === undefined ? [] : [announced]);
|
|
40843
|
+
return await untilGone(announced === undefined ? [] : [announced]);
|
|
40817
40844
|
}
|
|
40818
40845
|
if (running.pid === null) {
|
|
40819
40846
|
return true;
|
|
40820
40847
|
}
|
|
40821
40848
|
if (recorded === null || recorded.pid !== running.pid || !stillTheRecordedRun(recorded)) {
|
|
40849
|
+
clearTheRecord();
|
|
40822
40850
|
running.endedHere = null;
|
|
40823
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.`);
|
|
40824
40852
|
return true;
|
|
@@ -40831,12 +40859,20 @@ class GodotServer {
|
|
|
40831
40859
|
} catch {
|
|
40832
40860
|
landed = false;
|
|
40833
40861
|
}
|
|
40834
|
-
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();
|
|
40835
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}.`);
|
|
40836
40872
|
return gone;
|
|
40837
40873
|
}
|
|
40838
40874
|
spawnGame(godotPath, cmdArgs, env) {
|
|
40839
|
-
const child =
|
|
40875
|
+
const child = spawn2(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
|
|
40840
40876
|
const cancelled = callSignal();
|
|
40841
40877
|
if (cancelled !== undefined) {
|
|
40842
40878
|
const end = () => {
|
|
@@ -40888,25 +40924,23 @@ class GodotServer {
|
|
|
40888
40924
|
});
|
|
40889
40925
|
return started;
|
|
40890
40926
|
}
|
|
40891
|
-
|
|
40927
|
+
async launchKeptGame(godotPath, cmdArgs, projectPath, env) {
|
|
40892
40928
|
const startedAt = Date.now();
|
|
40893
40929
|
const transcript = openTranscript(startedAt);
|
|
40894
|
-
|
|
40895
|
-
|
|
40896
|
-
|
|
40897
|
-
|
|
40898
|
-
|
|
40899
|
-
|
|
40900
|
-
|
|
40901
|
-
|
|
40902
|
-
|
|
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;
|
|
40903
40939
|
}
|
|
40904
|
-
|
|
40905
|
-
|
|
40906
|
-
|
|
40907
|
-
|
|
40908
|
-
pid: child.pid ?? null,
|
|
40909
|
-
log,
|
|
40940
|
+
sweepTranscripts();
|
|
40941
|
+
return {
|
|
40942
|
+
pid: launched.pid,
|
|
40943
|
+
log: new GameLog,
|
|
40910
40944
|
transcript: transcript.path,
|
|
40911
40945
|
readOffset: 0,
|
|
40912
40946
|
projectPath,
|
|
@@ -40916,34 +40950,6 @@ class GodotServer {
|
|
|
40916
40950
|
throughEditor: false,
|
|
40917
40951
|
brokeOn: null
|
|
40918
40952
|
};
|
|
40919
|
-
child.on("exit", (code, signal) => {
|
|
40920
|
-
this.logDebug(`Godot process exited with code ${code ?? "none"}, signal ${signal ?? "none"}`);
|
|
40921
|
-
this.drainTranscript(started);
|
|
40922
|
-
log.finish();
|
|
40923
|
-
started.exitCode = code;
|
|
40924
|
-
started.exitSignal = code === null ? signal : null;
|
|
40925
|
-
if (started.pid !== null) {
|
|
40926
|
-
recordRunEnded(started.pid, { exitCode: started.exitCode, exitSignal: started.exitSignal });
|
|
40927
|
-
}
|
|
40928
|
-
});
|
|
40929
|
-
child.on("error", (err) => {
|
|
40930
|
-
console.error("Failed to start Godot process:", err);
|
|
40931
|
-
log.append("stderr", `${err.message}
|
|
40932
|
-
`);
|
|
40933
|
-
started.exitCode = -1;
|
|
40934
|
-
});
|
|
40935
|
-
if (started.pid !== null) {
|
|
40936
|
-
writeRunRecord({
|
|
40937
|
-
pid: started.pid,
|
|
40938
|
-
transcript: transcript.path,
|
|
40939
|
-
startedAt,
|
|
40940
|
-
projectPath,
|
|
40941
|
-
arguments: cmdArgs,
|
|
40942
|
-
command: godotPath
|
|
40943
|
-
});
|
|
40944
|
-
}
|
|
40945
|
-
sweepTranscripts();
|
|
40946
|
-
return started;
|
|
40947
40953
|
}
|
|
40948
40954
|
drainTranscript(game) {
|
|
40949
40955
|
if (game.transcript === null) {
|
|
@@ -40999,7 +41005,6 @@ class GodotServer {
|
|
|
40999
41005
|
const left = readEditorRunNote();
|
|
41000
41006
|
const note = left !== null && project !== null && isSameDirectory(left.projectPath, project) ? left : null;
|
|
41001
41007
|
const picked = {
|
|
41002
|
-
process: null,
|
|
41003
41008
|
pid: null,
|
|
41004
41009
|
log: new GameLog,
|
|
41005
41010
|
transcript: note?.transcript ?? null,
|
|
@@ -41136,7 +41141,6 @@ class GodotServer {
|
|
|
41136
41141
|
return null;
|
|
41137
41142
|
}
|
|
41138
41143
|
const adopted = {
|
|
41139
|
-
process: null,
|
|
41140
41144
|
pid: record.pid,
|
|
41141
41145
|
log: new GameLog,
|
|
41142
41146
|
transcript: record.transcript,
|
|
@@ -41146,7 +41150,8 @@ class GodotServer {
|
|
|
41146
41150
|
exitCode: null,
|
|
41147
41151
|
exitSignal: null,
|
|
41148
41152
|
throughEditor: false,
|
|
41149
|
-
brokeOn: null
|
|
41153
|
+
brokeOn: null,
|
|
41154
|
+
pickedUp: true
|
|
41150
41155
|
};
|
|
41151
41156
|
this.drainTranscript(adopted);
|
|
41152
41157
|
if (!alive(record.pid) || !couldStillBeTheRecordedRun(record)) {
|
|
@@ -41281,8 +41286,8 @@ class GodotServer {
|
|
|
41281
41286
|
async runStillGoing(run, editorSays) {
|
|
41282
41287
|
if (!run.throughEditor) {
|
|
41283
41288
|
const going = stillRunning(run);
|
|
41284
|
-
if (!going && run.
|
|
41285
|
-
|
|
41289
|
+
if (!going && run.pid !== null && !exited(run) && run.endedUnwatched !== true && typeof run.endedHere !== "string" && !await this.untilTheExitIsRecorded(run)) {
|
|
41290
|
+
run.endedUnwatched = true;
|
|
41286
41291
|
}
|
|
41287
41292
|
return going;
|
|
41288
41293
|
}
|
|
@@ -41299,6 +41304,21 @@ class GodotServer {
|
|
|
41299
41304
|
}
|
|
41300
41305
|
return runIsUp(run, said);
|
|
41301
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
|
+
}
|
|
41302
41322
|
handleEditorConsole(args) {
|
|
41303
41323
|
const status = this.godotBridge.getStatus();
|
|
41304
41324
|
const projectPath = status.projectPath ?? this.ownProject ?? undefined;
|
|
@@ -41400,7 +41420,8 @@ class GodotServer {
|
|
|
41400
41420
|
throughEditor: run.throughEditor,
|
|
41401
41421
|
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
41402
41422
|
couldAnnounce: run.projectPath !== null && existsSync15(join23(run.projectPath, RUNTIME_AUTOLOAD2.path)),
|
|
41403
|
-
endedHere: typeof run.endedHere === "string"
|
|
41423
|
+
endedHere: typeof run.endedHere === "string",
|
|
41424
|
+
pickedUp: run.pickedUp === true
|
|
41404
41425
|
}));
|
|
41405
41426
|
}
|
|
41406
41427
|
if (run.pickedUpPlaying === true) {
|
|
@@ -41426,7 +41447,7 @@ class GodotServer {
|
|
|
41426
41447
|
...hold,
|
|
41427
41448
|
endedUnwatched: run.endedUnwatched === true ? true : undefined,
|
|
41428
41449
|
endedBy: stillRunning(run) ? undefined : run.endedHere ?? null,
|
|
41429
|
-
project:
|
|
41450
|
+
project: run.throughEditor ? undefined : run.projectPath ?? undefined,
|
|
41430
41451
|
transcript: run.transcript ?? undefined,
|
|
41431
41452
|
startedAt: new Date(run.startedAt).toISOString(),
|
|
41432
41453
|
elapsedMs: Date.now() - run.startedAt,
|
|
@@ -42056,6 +42077,7 @@ var init_server3 = __esm(() => {
|
|
|
42056
42077
|
init_issues();
|
|
42057
42078
|
init_junit();
|
|
42058
42079
|
init_lsp_client();
|
|
42080
|
+
init_outside();
|
|
42059
42081
|
init_paths();
|
|
42060
42082
|
init_ports();
|
|
42061
42083
|
init_process_children();
|
|
@@ -42071,7 +42093,7 @@ var init_server3 = __esm(() => {
|
|
|
42071
42093
|
init_tool_definitions();
|
|
42072
42094
|
init_update_check();
|
|
42073
42095
|
run7 = promisify7(execFile7);
|
|
42074
|
-
__dirname2 = dirname16(
|
|
42096
|
+
__dirname2 = dirname16(fileURLToPath6(import.meta.url));
|
|
42075
42097
|
CONSOLE_WRAPPER = /_console(\.exe)?$/i;
|
|
42076
42098
|
LONGEST_BOOT_NOTED_MS = 5 * 60000;
|
|
42077
42099
|
VALUE_NAMES = {
|
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);
|
|
@@ -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
|
}
|
|
@@ -32856,7 +32877,12 @@ class GodotServer {
|
|
|
32856
32877
|
return anEditorIsStillComing(listeningSince, this.launchedEditorIsUp());
|
|
32857
32878
|
}
|
|
32858
32879
|
launchedEditorIsUp() {
|
|
32859
|
-
|
|
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;
|
|
32860
32886
|
}
|
|
32861
32887
|
dap() {
|
|
32862
32888
|
const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
|
|
@@ -33217,9 +33243,9 @@ class GodotServer {
|
|
|
33217
33243
|
}
|
|
33218
33244
|
async openAnEditor(engine, projectPath, ports) {
|
|
33219
33245
|
clearEditorLog(projectPath);
|
|
33220
|
-
const
|
|
33221
|
-
|
|
33222
|
-
|
|
33246
|
+
const launched = await launchOutsideTheTree({
|
|
33247
|
+
command: engine,
|
|
33248
|
+
args: editorArguments(projectPath, ports, editorLogPath(projectPath)),
|
|
33223
33249
|
env: {
|
|
33224
33250
|
...process.env,
|
|
33225
33251
|
GDHARNESS_RUNTIME_DIR: runtimeDirectory(),
|
|
@@ -33228,23 +33254,15 @@ class GodotServer {
|
|
|
33228
33254
|
[OPENED_BY_A_SERVER]: "1"
|
|
33229
33255
|
}
|
|
33230
33256
|
});
|
|
33231
|
-
if (
|
|
33232
|
-
|
|
33233
|
-
}
|
|
33234
|
-
const started = await new Promise((resolve) => {
|
|
33235
|
-
editor.once("spawn", () => {
|
|
33236
|
-
resolve(null);
|
|
33237
|
-
});
|
|
33238
|
-
editor.once("error", (error) => {
|
|
33239
|
-
resolve(error.message);
|
|
33240
|
-
});
|
|
33241
|
-
});
|
|
33242
|
-
if (started !== null) {
|
|
33243
|
-
return { pid: null, error: started };
|
|
33257
|
+
if ("error" in launched) {
|
|
33258
|
+
return { pid: null, error: launched.error };
|
|
33244
33259
|
}
|
|
33245
|
-
|
|
33246
|
-
this.launchedEditor =
|
|
33247
|
-
|
|
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 };
|
|
33248
33266
|
}
|
|
33249
33267
|
async portsForAnEditor() {
|
|
33250
33268
|
const lsp = portFromEnvOrNull("GDHARNESS_LSP_PORT") ?? await freePort(DEFAULT_LSP_PORT);
|
|
@@ -33314,7 +33332,12 @@ class GodotServer {
|
|
|
33314
33332
|
userArgs: given.value
|
|
33315
33333
|
});
|
|
33316
33334
|
this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
33317
|
-
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
|
+
}
|
|
33318
33341
|
started.announcedBefore = alreadyPlaying;
|
|
33319
33342
|
this.activeProcess = started;
|
|
33320
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." : "";
|
|
@@ -33326,7 +33349,7 @@ class GodotServer {
|
|
|
33326
33349
|
started: true,
|
|
33327
33350
|
through: "gdharness",
|
|
33328
33351
|
alsoRunning: stranded.length === 0 ? undefined : stranded.map((one) => one.pid),
|
|
33329
|
-
pid: started.
|
|
33352
|
+
pid: started.pid,
|
|
33330
33353
|
arguments: cmdArgs,
|
|
33331
33354
|
transcript: started.transcript,
|
|
33332
33355
|
refreshedClasses: refreshed.value,
|
|
@@ -33441,7 +33464,6 @@ class GodotServer {
|
|
|
33441
33464
|
closeSync2(transcript.fd);
|
|
33442
33465
|
const playAnswer = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
|
|
33443
33466
|
const played = {
|
|
33444
|
-
process: null,
|
|
33445
33467
|
pid: null,
|
|
33446
33468
|
log,
|
|
33447
33469
|
transcript: transcript.path,
|
|
@@ -33540,8 +33562,14 @@ class GodotServer {
|
|
|
33540
33562
|
}
|
|
33541
33563
|
async endActiveGame(reason, going) {
|
|
33542
33564
|
const running = this.activeProcess;
|
|
33543
|
-
const recorded = running
|
|
33544
|
-
|
|
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
|
+
}
|
|
33545
33573
|
if (!running || !going) {
|
|
33546
33574
|
return true;
|
|
33547
33575
|
}
|
|
@@ -33549,16 +33577,13 @@ class GodotServer {
|
|
|
33549
33577
|
const announced = this.announcedPidOf(running);
|
|
33550
33578
|
if (running.throughEditor) {
|
|
33551
33579
|
await this.handleViaBridge("stop_playing", {});
|
|
33552
|
-
return await untilGone(
|
|
33553
|
-
}
|
|
33554
|
-
if (running.process !== null) {
|
|
33555
|
-
running.process.kill();
|
|
33556
|
-
return await untilGone(running.process, announced === undefined ? [] : [announced]);
|
|
33580
|
+
return await untilGone(announced === undefined ? [] : [announced]);
|
|
33557
33581
|
}
|
|
33558
33582
|
if (running.pid === null) {
|
|
33559
33583
|
return true;
|
|
33560
33584
|
}
|
|
33561
33585
|
if (recorded === null || recorded.pid !== running.pid || !stillTheRecordedRun(recorded)) {
|
|
33586
|
+
clearTheRecord();
|
|
33562
33587
|
running.endedHere = null;
|
|
33563
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.`);
|
|
33564
33589
|
return true;
|
|
@@ -33571,12 +33596,20 @@ class GodotServer {
|
|
|
33571
33596
|
} catch {
|
|
33572
33597
|
landed = false;
|
|
33573
33598
|
}
|
|
33574
|
-
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();
|
|
33575
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}.`);
|
|
33576
33609
|
return gone;
|
|
33577
33610
|
}
|
|
33578
33611
|
spawnGame(godotPath, cmdArgs, env) {
|
|
33579
|
-
const child =
|
|
33612
|
+
const child = spawn2(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
|
|
33580
33613
|
const cancelled = callSignal();
|
|
33581
33614
|
if (cancelled !== undefined) {
|
|
33582
33615
|
const end = () => {
|
|
@@ -33628,25 +33661,23 @@ class GodotServer {
|
|
|
33628
33661
|
});
|
|
33629
33662
|
return started;
|
|
33630
33663
|
}
|
|
33631
|
-
|
|
33664
|
+
async launchKeptGame(godotPath, cmdArgs, projectPath, env) {
|
|
33632
33665
|
const startedAt = Date.now();
|
|
33633
33666
|
const transcript = openTranscript(startedAt);
|
|
33634
|
-
|
|
33635
|
-
|
|
33636
|
-
|
|
33637
|
-
|
|
33638
|
-
|
|
33639
|
-
|
|
33640
|
-
|
|
33641
|
-
|
|
33642
|
-
|
|
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;
|
|
33643
33676
|
}
|
|
33644
|
-
|
|
33645
|
-
|
|
33646
|
-
|
|
33647
|
-
|
|
33648
|
-
pid: child.pid ?? null,
|
|
33649
|
-
log,
|
|
33677
|
+
sweepTranscripts();
|
|
33678
|
+
return {
|
|
33679
|
+
pid: launched.pid,
|
|
33680
|
+
log: new GameLog,
|
|
33650
33681
|
transcript: transcript.path,
|
|
33651
33682
|
readOffset: 0,
|
|
33652
33683
|
projectPath,
|
|
@@ -33656,34 +33687,6 @@ class GodotServer {
|
|
|
33656
33687
|
throughEditor: false,
|
|
33657
33688
|
brokeOn: null
|
|
33658
33689
|
};
|
|
33659
|
-
child.on("exit", (code, signal) => {
|
|
33660
|
-
this.logDebug(`Godot process exited with code ${code ?? "none"}, signal ${signal ?? "none"}`);
|
|
33661
|
-
this.drainTranscript(started);
|
|
33662
|
-
log.finish();
|
|
33663
|
-
started.exitCode = code;
|
|
33664
|
-
started.exitSignal = code === null ? signal : null;
|
|
33665
|
-
if (started.pid !== null) {
|
|
33666
|
-
recordRunEnded(started.pid, { exitCode: started.exitCode, exitSignal: started.exitSignal });
|
|
33667
|
-
}
|
|
33668
|
-
});
|
|
33669
|
-
child.on("error", (err) => {
|
|
33670
|
-
console.error("Failed to start Godot process:", err);
|
|
33671
|
-
log.append("stderr", `${err.message}
|
|
33672
|
-
`);
|
|
33673
|
-
started.exitCode = -1;
|
|
33674
|
-
});
|
|
33675
|
-
if (started.pid !== null) {
|
|
33676
|
-
writeRunRecord({
|
|
33677
|
-
pid: started.pid,
|
|
33678
|
-
transcript: transcript.path,
|
|
33679
|
-
startedAt,
|
|
33680
|
-
projectPath,
|
|
33681
|
-
arguments: cmdArgs,
|
|
33682
|
-
command: godotPath
|
|
33683
|
-
});
|
|
33684
|
-
}
|
|
33685
|
-
sweepTranscripts();
|
|
33686
|
-
return started;
|
|
33687
33690
|
}
|
|
33688
33691
|
drainTranscript(game) {
|
|
33689
33692
|
if (game.transcript === null) {
|
|
@@ -33739,7 +33742,6 @@ class GodotServer {
|
|
|
33739
33742
|
const left = readEditorRunNote();
|
|
33740
33743
|
const note = left !== null && project !== null && isSameDirectory(left.projectPath, project) ? left : null;
|
|
33741
33744
|
const picked = {
|
|
33742
|
-
process: null,
|
|
33743
33745
|
pid: null,
|
|
33744
33746
|
log: new GameLog,
|
|
33745
33747
|
transcript: note?.transcript ?? null,
|
|
@@ -33876,7 +33878,6 @@ class GodotServer {
|
|
|
33876
33878
|
return null;
|
|
33877
33879
|
}
|
|
33878
33880
|
const adopted = {
|
|
33879
|
-
process: null,
|
|
33880
33881
|
pid: record.pid,
|
|
33881
33882
|
log: new GameLog,
|
|
33882
33883
|
transcript: record.transcript,
|
|
@@ -33886,7 +33887,8 @@ class GodotServer {
|
|
|
33886
33887
|
exitCode: null,
|
|
33887
33888
|
exitSignal: null,
|
|
33888
33889
|
throughEditor: false,
|
|
33889
|
-
brokeOn: null
|
|
33890
|
+
brokeOn: null,
|
|
33891
|
+
pickedUp: true
|
|
33890
33892
|
};
|
|
33891
33893
|
this.drainTranscript(adopted);
|
|
33892
33894
|
if (!alive(record.pid) || !couldStillBeTheRecordedRun(record)) {
|
|
@@ -34021,8 +34023,8 @@ class GodotServer {
|
|
|
34021
34023
|
async runStillGoing(run, editorSays) {
|
|
34022
34024
|
if (!run.throughEditor) {
|
|
34023
34025
|
const going = stillRunning(run);
|
|
34024
|
-
if (!going && run.
|
|
34025
|
-
|
|
34026
|
+
if (!going && run.pid !== null && !exited(run) && run.endedUnwatched !== true && typeof run.endedHere !== "string" && !await this.untilTheExitIsRecorded(run)) {
|
|
34027
|
+
run.endedUnwatched = true;
|
|
34026
34028
|
}
|
|
34027
34029
|
return going;
|
|
34028
34030
|
}
|
|
@@ -34039,6 +34041,21 @@ class GodotServer {
|
|
|
34039
34041
|
}
|
|
34040
34042
|
return runIsUp(run, said);
|
|
34041
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
|
+
}
|
|
34042
34059
|
handleEditorConsole(args) {
|
|
34043
34060
|
const status = this.godotBridge.getStatus();
|
|
34044
34061
|
const projectPath = status.projectPath ?? this.ownProject ?? undefined;
|
|
@@ -34140,7 +34157,8 @@ class GodotServer {
|
|
|
34140
34157
|
throughEditor: run.throughEditor,
|
|
34141
34158
|
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
34142
34159
|
couldAnnounce: run.projectPath !== null && existsSync10(join16(run.projectPath, RUNTIME_AUTOLOAD.path)),
|
|
34143
|
-
endedHere: typeof run.endedHere === "string"
|
|
34160
|
+
endedHere: typeof run.endedHere === "string",
|
|
34161
|
+
pickedUp: run.pickedUp === true
|
|
34144
34162
|
}));
|
|
34145
34163
|
}
|
|
34146
34164
|
if (run.pickedUpPlaying === true) {
|
|
@@ -34166,7 +34184,7 @@ class GodotServer {
|
|
|
34166
34184
|
...hold,
|
|
34167
34185
|
endedUnwatched: run.endedUnwatched === true ? true : undefined,
|
|
34168
34186
|
endedBy: stillRunning(run) ? undefined : run.endedHere ?? null,
|
|
34169
|
-
project:
|
|
34187
|
+
project: run.throughEditor ? undefined : run.projectPath ?? undefined,
|
|
34170
34188
|
transcript: run.transcript ?? undefined,
|
|
34171
34189
|
startedAt: new Date(run.startedAt).toISOString(),
|
|
34172
34190
|
elapsedMs: Date.now() - run.startedAt,
|
package/build/keeper.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/keeper.ts
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { closeSync, openSync as openSync2 } from "node:fs";
|
|
6
|
+
|
|
7
|
+
// src/outside.ts
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
var KEEPER_SCRIPT = fileURLToPath(new URL("./keeper.js", import.meta.url));
|
|
10
|
+
function decodeSpec(encoded) {
|
|
11
|
+
return JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
12
|
+
}
|
|
13
|
+
function readLaunched(printed) {
|
|
14
|
+
const line = printed.split(`
|
|
15
|
+
`).find((one) => one.trim() !== "")?.trim() ?? "";
|
|
16
|
+
const pid = /^pid (\d+)$/.exec(line);
|
|
17
|
+
if (pid?.[1] !== undefined) {
|
|
18
|
+
return { pid: Number(pid[1]) };
|
|
19
|
+
}
|
|
20
|
+
return { error: line.replace(/^error /, "") || "the launcher said nothing" };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/run-record.ts
|
|
24
|
+
import { mkdirSync, openSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
25
|
+
import { basename, join as join2 } from "node:path";
|
|
26
|
+
|
|
27
|
+
// src/process-children.ts
|
|
28
|
+
import { execFile, execFileSync, spawnSync } from "node:child_process";
|
|
29
|
+
import { promisify } from "node:util";
|
|
30
|
+
var run = promisify(execFile);
|
|
31
|
+
|
|
32
|
+
// src/runtime-client.ts
|
|
33
|
+
import { tmpdir } from "node:os";
|
|
34
|
+
import { dirname, join, resolve } from "node:path";
|
|
35
|
+
|
|
36
|
+
// src/launch.ts
|
|
37
|
+
function envValue(name, variables = process.env) {
|
|
38
|
+
const value = variables[name];
|
|
39
|
+
return value === undefined || value === "" ? undefined : value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/runtime-client.ts
|
|
43
|
+
function runtimeDirectory(variables = process.env) {
|
|
44
|
+
const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
|
|
45
|
+
if (explicit) {
|
|
46
|
+
return explicit;
|
|
47
|
+
}
|
|
48
|
+
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
49
|
+
return join(perUser ?? tmpdir(), "gdharness");
|
|
50
|
+
}
|
|
51
|
+
var ERROR_REPORT_KEEP_MS = 60 * 60 * 1000;
|
|
52
|
+
var verdicts = new Map;
|
|
53
|
+
var LATE_ANSWER_MS = 30 * 60000;
|
|
54
|
+
var lateAnswers = new Map;
|
|
55
|
+
|
|
56
|
+
// src/run-record.ts
|
|
57
|
+
function runsDirectory() {
|
|
58
|
+
return join2(runtimeDirectory(), "runs");
|
|
59
|
+
}
|
|
60
|
+
function recordPath() {
|
|
61
|
+
return join2(runsDirectory(), "run.json");
|
|
62
|
+
}
|
|
63
|
+
function writeRunRecord(record) {
|
|
64
|
+
mkdirSync(runsDirectory(), { recursive: true });
|
|
65
|
+
writeFileSync(recordPath(), JSON.stringify(record, null, 2), "utf8");
|
|
66
|
+
}
|
|
67
|
+
function recordRunEnded(pid, ending) {
|
|
68
|
+
const path = recordPath();
|
|
69
|
+
const record = recordAt(path);
|
|
70
|
+
if (record === null || record.pid !== pid) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const written = {
|
|
74
|
+
...record,
|
|
75
|
+
...ending.exitCode === null ? {} : { exitCode: ending.exitCode },
|
|
76
|
+
...ending.exitSignal === null ? {} : { exitSignal: ending.exitSignal }
|
|
77
|
+
};
|
|
78
|
+
try {
|
|
79
|
+
writeFileSync(path, JSON.stringify(written, null, 2), "utf8");
|
|
80
|
+
} catch {}
|
|
81
|
+
}
|
|
82
|
+
function recordAt(path) {
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
if (!parsed || typeof parsed !== "object") {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const fields = parsed;
|
|
93
|
+
const pid = fields["pid"];
|
|
94
|
+
const transcript = fields["transcript"];
|
|
95
|
+
const startedAt = fields["startedAt"];
|
|
96
|
+
if (typeof pid !== "number" || typeof transcript !== "string" || typeof startedAt !== "number") {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const args = fields["arguments"];
|
|
100
|
+
return {
|
|
101
|
+
pid,
|
|
102
|
+
transcript,
|
|
103
|
+
startedAt,
|
|
104
|
+
projectPath: typeof fields["projectPath"] === "string" ? fields["projectPath"] : "",
|
|
105
|
+
arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : [],
|
|
106
|
+
...typeof fields["command"] === "string" ? { command: fields["command"] } : {},
|
|
107
|
+
...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
|
|
108
|
+
...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/keeper.ts
|
|
113
|
+
async function startDetached(spec, output) {
|
|
114
|
+
return await new Promise((resolve) => {
|
|
115
|
+
const child = spawn(spec.command, [...spec.args], {
|
|
116
|
+
stdio: ["ignore", output, output],
|
|
117
|
+
detached: true,
|
|
118
|
+
...spec.env === undefined ? {} : { env: spec.env }
|
|
119
|
+
});
|
|
120
|
+
child.once("error", (error) => {
|
|
121
|
+
resolve({ error: error.message });
|
|
122
|
+
});
|
|
123
|
+
child.once("spawn", () => {
|
|
124
|
+
resolve(child.pid === undefined ? { error: "the operating system gave it no pid" } : { child, pid: child.pid });
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
async function launch(encoded) {
|
|
129
|
+
const spec = decodeSpec(encoded);
|
|
130
|
+
if (spec.run === undefined) {
|
|
131
|
+
const started = await startDetached(spec, "ignore");
|
|
132
|
+
if ("error" in started) {
|
|
133
|
+
process.stdout.write(`error ${started.error}
|
|
134
|
+
`);
|
|
135
|
+
} else {
|
|
136
|
+
started.child.unref();
|
|
137
|
+
process.stdout.write(`pid ${started.pid}
|
|
138
|
+
`);
|
|
139
|
+
}
|
|
140
|
+
process.exit(0);
|
|
141
|
+
}
|
|
142
|
+
const keeper = spawn(process.execPath, [KEEPER_SCRIPT, "keep", encoded], {
|
|
143
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
144
|
+
detached: true,
|
|
145
|
+
windowsHide: true
|
|
146
|
+
});
|
|
147
|
+
let printed = "";
|
|
148
|
+
let complained = "";
|
|
149
|
+
keeper.once("error", (error) => {
|
|
150
|
+
process.stdout.write(`error the keeper did not start: ${error.message}
|
|
151
|
+
`);
|
|
152
|
+
process.exit(0);
|
|
153
|
+
});
|
|
154
|
+
keeper.stderr.on("data", (chunk) => {
|
|
155
|
+
complained += chunk.toString();
|
|
156
|
+
});
|
|
157
|
+
keeper.stdout.on("data", (chunk) => {
|
|
158
|
+
printed += chunk.toString();
|
|
159
|
+
if (!printed.includes(`
|
|
160
|
+
`)) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const launched = readLaunched(printed);
|
|
164
|
+
process.stdout.write("pid" in launched ? `pid ${launched.pid}
|
|
165
|
+
` : `error ${launched.error}
|
|
166
|
+
`);
|
|
167
|
+
keeper.unref();
|
|
168
|
+
process.exit(0);
|
|
169
|
+
});
|
|
170
|
+
keeper.once("close", () => {
|
|
171
|
+
process.stdout.write(`error the keeper ended before it said anything${complained ? `: ${complained.trim()}` : ""}
|
|
172
|
+
`);
|
|
173
|
+
process.exit(0);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
async function keep(encoded) {
|
|
177
|
+
const spec = decodeSpec(encoded);
|
|
178
|
+
if (spec.run === undefined) {
|
|
179
|
+
process.stdout.write(`error a keeper was started without a run
|
|
180
|
+
`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
const run = spec.run;
|
|
184
|
+
const transcript = openSync2(run.transcript, "a");
|
|
185
|
+
let started;
|
|
186
|
+
try {
|
|
187
|
+
started = await startDetached(spec, transcript);
|
|
188
|
+
} finally {
|
|
189
|
+
closeSync(transcript);
|
|
190
|
+
}
|
|
191
|
+
if ("error" in started) {
|
|
192
|
+
process.stdout.write(`error ${started.error}
|
|
193
|
+
`);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
const { child, pid } = started;
|
|
197
|
+
writeRunRecord({
|
|
198
|
+
pid,
|
|
199
|
+
transcript: run.transcript,
|
|
200
|
+
startedAt: run.startedAt,
|
|
201
|
+
projectPath: run.projectPath,
|
|
202
|
+
arguments: spec.args,
|
|
203
|
+
command: spec.command
|
|
204
|
+
});
|
|
205
|
+
child.once("exit", (code, signal) => {
|
|
206
|
+
recordRunEnded(pid, { exitCode: code, exitSignal: code === null ? signal : null });
|
|
207
|
+
process.exit(0);
|
|
208
|
+
});
|
|
209
|
+
process.stdout.write(`pid ${pid}
|
|
210
|
+
`);
|
|
211
|
+
process.stdout.end();
|
|
212
|
+
}
|
|
213
|
+
var [mode, encoded] = process.argv.slice(2);
|
|
214
|
+
if (mode === "launch" && encoded !== undefined) {
|
|
215
|
+
await launch(encoded);
|
|
216
|
+
} else if (mode === "keep" && encoded !== undefined) {
|
|
217
|
+
await keep(encoded);
|
|
218
|
+
} else {
|
|
219
|
+
process.stderr.write(`keeper.js launch|keep <spec>
|
|
220
|
+
`);
|
|
221
|
+
process.exit(2);
|
|
222
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.30",
|
|
4
4
|
"mcpName": "io.github.Aureliolo/gdharness",
|
|
5
5
|
"description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
|
|
6
6
|
"type": "module",
|