gdharness 0.13.34 → 0.13.36
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 +120 -22
- package/build/index.js +113 -20
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -15994,7 +15994,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15994
15994
|
},
|
|
15995
15995
|
{
|
|
15996
15996
|
name: "editor_output",
|
|
15997
|
-
description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. Answers with the counts and the verdict as well as the entries. A run that has quit still answers here, with running false and its exit code, until the next one starts. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state:
|
|
15997
|
+
description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. Answers with the counts and the verdict as well as the entries. A run that has quit still answers here, with running false and its exit code, until the next one starts. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering.",
|
|
15998
15998
|
parameters: {
|
|
15999
15999
|
severity: {
|
|
16000
16000
|
type: "string",
|
|
@@ -16022,7 +16022,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16022
16022
|
},
|
|
16023
16023
|
{
|
|
16024
16024
|
name: "editor_status",
|
|
16025
|
-
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. 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. 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.',
|
|
16025
|
+
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. 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. 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. 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.',
|
|
16026
16026
|
parameters: {},
|
|
16027
16027
|
requires: []
|
|
16028
16028
|
},
|
|
@@ -36662,23 +36662,32 @@ function recordAt(path) {
|
|
|
36662
36662
|
}
|
|
36663
36663
|
function runningAs(pid) {
|
|
36664
36664
|
if (process.platform === "win32") {
|
|
36665
|
-
const
|
|
36666
|
-
if (
|
|
36667
|
-
|
|
36665
|
+
const answer = windowsCommandLine(pid);
|
|
36666
|
+
if (answer !== null) {
|
|
36667
|
+
const [, ...rest] = answer.split(`
|
|
36668
|
+
`);
|
|
36669
|
+
const line = rest.join(`
|
|
36670
|
+
`).trim();
|
|
36671
|
+
const began = startedAt(pid, answer);
|
|
36672
|
+
if (line !== "") {
|
|
36673
|
+
return { kind: "commandLine", text: line, ...began === null ? {} : { startedAt: began } };
|
|
36674
|
+
}
|
|
36668
36675
|
}
|
|
36669
36676
|
const image = windowsImage(pid);
|
|
36670
36677
|
return image === null ? null : { kind: "image", text: image };
|
|
36671
36678
|
}
|
|
36672
36679
|
try {
|
|
36680
|
+
const began = startedAt(pid, null);
|
|
36681
|
+
const when = began === null ? {} : { startedAt: began };
|
|
36673
36682
|
if (process.platform === "linux") {
|
|
36674
36683
|
const raw = readFileSync14(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
36675
|
-
return raw === "" ? null : { kind: "commandLine", text: raw };
|
|
36684
|
+
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
36676
36685
|
}
|
|
36677
36686
|
const args = execFileSync("ps", ["-p", String(pid), "-o", "args="], {
|
|
36678
36687
|
encoding: "utf8",
|
|
36679
36688
|
timeout: 15000
|
|
36680
36689
|
}).trim();
|
|
36681
|
-
return args === "" ? null : { kind: "commandLine", text: args };
|
|
36690
|
+
return args === "" ? null : { kind: "commandLine", text: args, ...when };
|
|
36682
36691
|
} catch {
|
|
36683
36692
|
return null;
|
|
36684
36693
|
}
|
|
@@ -36717,13 +36726,30 @@ function windowsCommandLine(pid) {
|
|
|
36717
36726
|
"-NoProfile",
|
|
36718
36727
|
"-NonInteractive",
|
|
36719
36728
|
"-Command",
|
|
36720
|
-
|
|
36729
|
+
`$p = Get-CimInstance Win32_Process -Filter "ProcessId=${pid}"; if ($p) { $p.CreationDate.ToUniversalTime().ToString("o"); $p.CommandLine }`
|
|
36721
36730
|
], { encoding: "utf8", timeout: 15000, windowsHide: true }).trim();
|
|
36722
36731
|
return answer === "" ? null : answer;
|
|
36723
36732
|
} catch {
|
|
36724
36733
|
return null;
|
|
36725
36734
|
}
|
|
36726
36735
|
}
|
|
36736
|
+
function startedAt(pid, windowsAnswer) {
|
|
36737
|
+
if (process.platform === "win32") {
|
|
36738
|
+
const when = Date.parse(windowsAnswer?.split(`
|
|
36739
|
+
`)[0]?.trim() ?? "");
|
|
36740
|
+
return Number.isNaN(when) ? null : when;
|
|
36741
|
+
}
|
|
36742
|
+
try {
|
|
36743
|
+
const said = execFileSync("ps", ["-p", String(pid), "-o", "lstart="], {
|
|
36744
|
+
encoding: "utf8",
|
|
36745
|
+
timeout: 15000
|
|
36746
|
+
}).trim();
|
|
36747
|
+
const when = Date.parse(said);
|
|
36748
|
+
return said === "" || Number.isNaN(when) ? null : when;
|
|
36749
|
+
} catch {
|
|
36750
|
+
return null;
|
|
36751
|
+
}
|
|
36752
|
+
}
|
|
36727
36753
|
function windowsImage(pid) {
|
|
36728
36754
|
try {
|
|
36729
36755
|
const csv = execFileSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
|
|
@@ -36746,12 +36772,18 @@ function judgeRun(record, running, needed) {
|
|
|
36746
36772
|
if (running === null) {
|
|
36747
36773
|
return false;
|
|
36748
36774
|
}
|
|
36775
|
+
if (running.startedAt !== undefined && running.startedAt - record.startedAt > SAME_RUN_WINDOW_MS) {
|
|
36776
|
+
return false;
|
|
36777
|
+
}
|
|
36749
36778
|
const engine = record.command === undefined ? null : basename2(record.command);
|
|
36750
36779
|
const said = process.platform === "win32" ? running.text.toLowerCase() : running.text;
|
|
36751
36780
|
const wanted = engine === null || process.platform !== "win32" ? engine : engine.toLowerCase();
|
|
36752
36781
|
if (wanted !== null && !said.includes(wanted)) {
|
|
36753
36782
|
return false;
|
|
36754
36783
|
}
|
|
36784
|
+
if (running.kind === "commandLine" && AN_EDITOR.test(said)) {
|
|
36785
|
+
return false;
|
|
36786
|
+
}
|
|
36755
36787
|
if (record.projectPath === "") {
|
|
36756
36788
|
return wanted !== null;
|
|
36757
36789
|
}
|
|
@@ -36801,8 +36833,10 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
36801
36833
|
} catch {}
|
|
36802
36834
|
}
|
|
36803
36835
|
}
|
|
36836
|
+
var AN_EDITOR, SAME_RUN_WINDOW_MS = 1e4;
|
|
36804
36837
|
var init_run_record = __esm(() => {
|
|
36805
36838
|
init_runtime_client();
|
|
36839
|
+
AN_EDITOR = /(?:^|\s)(?:-e|--editor)(?:\s|$)/;
|
|
36806
36840
|
});
|
|
36807
36841
|
|
|
36808
36842
|
// src/server.ts
|
|
@@ -36835,12 +36869,18 @@ function stillRunning(run) {
|
|
|
36835
36869
|
if (!run.throughEditor && run.process === null && run.pid !== null) {
|
|
36836
36870
|
return alive(run.pid);
|
|
36837
36871
|
}
|
|
36872
|
+
if (run.announcedPid !== undefined && !alive(run.announcedPid)) {
|
|
36873
|
+
return false;
|
|
36874
|
+
}
|
|
36838
36875
|
return true;
|
|
36839
36876
|
}
|
|
36840
36877
|
function runIsUp(run, editorSays) {
|
|
36841
36878
|
if (run?.throughEditor !== true) {
|
|
36842
36879
|
return stillRunning(run);
|
|
36843
36880
|
}
|
|
36881
|
+
if (run.announcedPid !== undefined && !alive(run.announcedPid)) {
|
|
36882
|
+
return false;
|
|
36883
|
+
}
|
|
36844
36884
|
return editorSays ?? stillRunning(run);
|
|
36845
36885
|
}
|
|
36846
36886
|
function runtimeVerdict(endpoint, after) {
|
|
@@ -38229,7 +38269,10 @@ class GodotServer {
|
|
|
38229
38269
|
return `The debug adapter on port ${port} belongs to process ${holder}, and the editor on this server's bridge is process ${ours}. Godot gives every editor the same debug adapter port by default, so the one this editor names in its settings is not necessarily the one it holds. Playing the game now would put another editor's console and another project's debugger under this project's name.`;
|
|
38230
38270
|
}
|
|
38231
38271
|
anEditorIsStillComing(listeningSince) {
|
|
38232
|
-
return anEditorIsStillComing(listeningSince, this.
|
|
38272
|
+
return anEditorIsStillComing(listeningSince, this.launchedEditorIsUp());
|
|
38273
|
+
}
|
|
38274
|
+
launchedEditorIsUp() {
|
|
38275
|
+
return this.launchedEditor?.exitCode === null && this.launchedEditor.signalCode === null;
|
|
38233
38276
|
}
|
|
38234
38277
|
dap() {
|
|
38235
38278
|
const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
|
|
@@ -38275,7 +38318,7 @@ class GodotServer {
|
|
|
38275
38318
|
addonIsStale: status.connected ? stale : undefined,
|
|
38276
38319
|
bridgeAvailable: this.bridgeStartupError === null,
|
|
38277
38320
|
mayYetConnect: status.connected ? undefined : this.anEditorIsStillComing(status.listeningSince),
|
|
38278
|
-
awaitingLaunchedEditor: status.connected || this.
|
|
38321
|
+
awaitingLaunchedEditor: status.connected || !this.launchedEditorIsUp() ? undefined : this.launchedEditor?.pid ?? undefined,
|
|
38279
38322
|
...status.connected ? this.whatTheLaunchedEditorDropped() : {},
|
|
38280
38323
|
startupError: this.bridgeStartupError,
|
|
38281
38324
|
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
|
|
@@ -38311,7 +38354,8 @@ class GodotServer {
|
|
|
38311
38354
|
}
|
|
38312
38355
|
async handleEditorStatus() {
|
|
38313
38356
|
const godotPath = await this.locator.find();
|
|
38314
|
-
const
|
|
38357
|
+
const announced = runtimesAnnounced();
|
|
38358
|
+
const games = await Promise.all(announced.running.map(async (endpoint) => {
|
|
38315
38359
|
const reply = await runtimeRequest(endpoint, "ping", {}, this.runtimeTimeoutMs());
|
|
38316
38360
|
return {
|
|
38317
38361
|
pid: endpoint.pid,
|
|
@@ -38333,10 +38377,17 @@ class GodotServer {
|
|
|
38333
38377
|
version: godotPath === null ? null : await this.godotVersion(godotPath)
|
|
38334
38378
|
},
|
|
38335
38379
|
game: {
|
|
38336
|
-
processActive:
|
|
38380
|
+
processActive: runIsUp(this.currentRun(), playing === null ? null : playing.playing),
|
|
38337
38381
|
playingInEditor: playing,
|
|
38338
38382
|
runtimeConnected: games.some((game) => game.reachable),
|
|
38339
|
-
runtimes: games
|
|
38383
|
+
runtimes: games,
|
|
38384
|
+
unreadable: announced.unspoken.map((game) => ({
|
|
38385
|
+
pid: game.pid,
|
|
38386
|
+
project: game.project,
|
|
38387
|
+
protocol: game.protocol,
|
|
38388
|
+
serverSpeaks: RUNTIME_PROTOCOL,
|
|
38389
|
+
behind: game.protocol > RUNTIME_PROTOCOL ? "server" : "addon"
|
|
38390
|
+
}))
|
|
38340
38391
|
}
|
|
38341
38392
|
});
|
|
38342
38393
|
}
|
|
@@ -38486,7 +38537,7 @@ class GodotServer {
|
|
|
38486
38537
|
return { pid: null, error: started };
|
|
38487
38538
|
}
|
|
38488
38539
|
editor.unref();
|
|
38489
|
-
this.launchedEditor = editor.pid
|
|
38540
|
+
this.launchedEditor = editor.pid === undefined ? null : editor;
|
|
38490
38541
|
return { pid: editor.pid ?? null, error: null };
|
|
38491
38542
|
}
|
|
38492
38543
|
async portsForAnEditor() {
|
|
@@ -38533,6 +38584,7 @@ class GodotServer {
|
|
|
38533
38584
|
await this.endActiveGame("editor_run start, which ends the run that was going before it starts another");
|
|
38534
38585
|
}
|
|
38535
38586
|
const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
|
|
38587
|
+
const wereHereFirst = this.everyRuntimeOfOurs();
|
|
38536
38588
|
const headless = resolveHeadless(args["headless"], {
|
|
38537
38589
|
platform: process.platform,
|
|
38538
38590
|
variables: process.env
|
|
@@ -38553,16 +38605,20 @@ class GodotServer {
|
|
|
38553
38605
|
this.activeProcess = started;
|
|
38554
38606
|
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." : "";
|
|
38555
38607
|
const endedForThis = ended === null ? "" : ` The run that was going, pid ${ended}, was ended to start this one; its output is no longer what editor_output answers about.`;
|
|
38608
|
+
const stranded = wereHereFirst.filter((one) => one.pid !== ended && alive(one.pid));
|
|
38609
|
+
const named = stranded.map((one) => one.protocol === null ? `pid ${one.pid}` : `pid ${one.pid} on protocol ${one.protocol}`).join(", ");
|
|
38610
|
+
const alsoRunning = stranded.length === 0 ? "" : ` A game of this project was already running and still is: ${named}. Starting this one did not end it and nothing here can, because this server did not start it. End it yourself, or bring the halves to one version where a protocol is named.`;
|
|
38556
38611
|
return this.jsonTextResponse({
|
|
38557
38612
|
started: true,
|
|
38558
38613
|
through: "gdharness",
|
|
38614
|
+
alsoRunning: stranded.length === 0 ? undefined : stranded.map((one) => one.pid),
|
|
38559
38615
|
pid: started.process.pid ?? null,
|
|
38560
38616
|
arguments: cmdArgs,
|
|
38561
38617
|
transcript: started.transcript,
|
|
38562
38618
|
refreshedClasses: refreshed.value,
|
|
38563
38619
|
endedPreviousRun: ended ?? undefined,
|
|
38564
38620
|
runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
|
|
38565
|
-
message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}`
|
|
38621
|
+
message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}${alsoRunning}`
|
|
38566
38622
|
});
|
|
38567
38623
|
}
|
|
38568
38624
|
async gameIsUp() {
|
|
@@ -38578,6 +38634,10 @@ class GodotServer {
|
|
|
38578
38634
|
budgetMs,
|
|
38579
38635
|
giveUp: () => this.dapClient?.isStopped() === true || !stillRunning(this.currentRun())
|
|
38580
38636
|
});
|
|
38637
|
+
const going = this.currentRun();
|
|
38638
|
+
if (endpoint !== null && going?.throughEditor === true && going.announcedPid === undefined) {
|
|
38639
|
+
going.announcedPid = endpoint.pid;
|
|
38640
|
+
}
|
|
38581
38641
|
return runtimeVerdict(endpoint, {
|
|
38582
38642
|
addon: true,
|
|
38583
38643
|
budgetMs,
|
|
@@ -38687,9 +38747,15 @@ class GodotServer {
|
|
|
38687
38747
|
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.`);
|
|
38688
38748
|
return;
|
|
38689
38749
|
}
|
|
38750
|
+
const signalled = runningAs(running.pid);
|
|
38751
|
+
const described = signalled === null ? "nothing it would name" : `${signalled.kind}: ${signalled.text}`;
|
|
38752
|
+
let landed = true;
|
|
38690
38753
|
try {
|
|
38691
38754
|
process.kill(running.pid);
|
|
38692
|
-
} catch {
|
|
38755
|
+
} catch {
|
|
38756
|
+
landed = false;
|
|
38757
|
+
}
|
|
38758
|
+
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}.`);
|
|
38693
38759
|
}
|
|
38694
38760
|
spawnGame(godotPath, cmdArgs, env) {
|
|
38695
38761
|
const child = spawn(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
|
|
@@ -38878,7 +38944,12 @@ class GodotServer {
|
|
|
38878
38944
|
const noEditor = !status.connected ? " No editor is connected, so a game the editor is playing cannot be seen from here at all:" + " editor_status says whether one is on its way, and a run of yours that is still up is" + " reachable again once it is." : "";
|
|
38879
38945
|
const live = this.aRuntimeOfOursIsAnswering();
|
|
38880
38946
|
if (live !== null) {
|
|
38881
|
-
return `A game is running for ${live.project.path} and has announced its runtime on port ${live.port}, pid ${live.pid},` + " but this server did not start it and cannot reach it through the editor, so editor_run and" + " editor_output have nothing to answer about. The runtime_* tools reach it now." + "
|
|
38947
|
+
return `A game is running for ${live.project.path} and has announced its runtime on port ${live.port}, pid ${live.pid},` + " but this server did not start it and cannot reach it through the editor, so editor_run and" + " editor_output have nothing to answer about. The runtime_* tools reach it now." + " A run started here is a separate process rather than a replacement, so starting one leaves" + " this game running as well." + (this.godotBridge.getStatus().connected ? "" : " editor_status says whether an editor is on its way, and editor_run can end it once one is.");
|
|
38948
|
+
}
|
|
38949
|
+
const unreadable = this.aRuntimeOfOursIsTooFarOff();
|
|
38950
|
+
if (unreadable !== null) {
|
|
38951
|
+
const behind = unreadable.protocol > RUNTIME_PROTOCOL ? "this server is the older half: reconnect it so it spawns the installed version" : "the addon is the older half: reinstall it and restart the game";
|
|
38952
|
+
return `A game is running for ${unreadable.project.path}, pid ${unreadable.pid}, announced in protocol` + ` ${unreadable.protocol}, which this server does not speak: it speaks ${RUNTIME_PROTOCOL}, so ${behind}.` + " Until then neither editor_run nor the runtime_* tools can reach it." + " A run started here is a separate process rather than a replacement, so starting one leaves" + " this game running as well.";
|
|
38882
38953
|
}
|
|
38883
38954
|
const record = readRunRecord();
|
|
38884
38955
|
if (record === null) {
|
|
@@ -38890,12 +38961,34 @@ class GodotServer {
|
|
|
38890
38961
|
return `No game of this server's is running.${noEditor} A run started from ${whose} is recorded on this machine, and ${ours}, so it is not this server's to answer for or to end. Start one with editor_run.`;
|
|
38891
38962
|
}
|
|
38892
38963
|
aRuntimeOfOursIsAnswering() {
|
|
38964
|
+
return this.announcedForOurProject(discoverRuntimes());
|
|
38965
|
+
}
|
|
38966
|
+
aRuntimeOfOursIsTooFarOff() {
|
|
38967
|
+
return this.announcedForOurProject(runtimesAnnounced().unspoken);
|
|
38968
|
+
}
|
|
38969
|
+
announcedForOurProject(announced) {
|
|
38970
|
+
return this.allAnnouncedForOurProject(announced)[0] ?? null;
|
|
38971
|
+
}
|
|
38972
|
+
allAnnouncedForOurProject(announced) {
|
|
38893
38973
|
const status = this.godotBridge.getStatus();
|
|
38894
38974
|
const mine = this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
|
|
38895
38975
|
if (mine === null) {
|
|
38896
|
-
return
|
|
38976
|
+
return [];
|
|
38897
38977
|
}
|
|
38898
|
-
return
|
|
38978
|
+
return announced.filter((one) => one.project.path !== "" && isSameDirectory(mine, one.project.path));
|
|
38979
|
+
}
|
|
38980
|
+
everyRuntimeOfOurs() {
|
|
38981
|
+
const announced = runtimesAnnounced();
|
|
38982
|
+
return [
|
|
38983
|
+
...this.allAnnouncedForOurProject(announced.running).map((one) => ({
|
|
38984
|
+
pid: one.pid,
|
|
38985
|
+
protocol: null
|
|
38986
|
+
})),
|
|
38987
|
+
...this.allAnnouncedForOurProject(announced.unspoken).map((one) => ({
|
|
38988
|
+
pid: one.pid,
|
|
38989
|
+
protocol: one.protocol
|
|
38990
|
+
}))
|
|
38991
|
+
];
|
|
38899
38992
|
}
|
|
38900
38993
|
couldBeOurs(project) {
|
|
38901
38994
|
const status = this.godotBridge.getStatus();
|
|
@@ -39000,7 +39093,7 @@ class GodotServer {
|
|
|
39000
39093
|
notes.push(run.exitCode === 0 ? "This run quit on its own, cleanly: exit code 0." : "Nothing here ended this run: it stopped on its own or something outside this server stopped it.");
|
|
39001
39094
|
}
|
|
39002
39095
|
if (run.transcript !== null && selected.omitted > 0) {
|
|
39003
|
-
notes.push(`Everything this run has printed is in ${run.transcript}, uncapped and still being written. Read it for output, not for whether the run is alive: running above is asked of the operating system, and
|
|
39096
|
+
notes.push(`Everything this run has printed is in ${run.transcript}, uncapped and still being written. Read it for output, not for whether the run is alive: a transcript that stops growing is a run between prints. running above is asked of the operating system while there is a process to ask about, and for an editor-played run that announced no runtime it is the editor's answer instead, which can lag after the game has gone.`);
|
|
39004
39097
|
}
|
|
39005
39098
|
return this.jsonTextResponse({
|
|
39006
39099
|
running: stillRunning(run),
|
|
@@ -40796,8 +40889,13 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
40796
40889
|
with their backtraces, and a \`clean\` verdict, so a run that printed an error is one call away
|
|
40797
40890
|
from being known. For a long run it also names a transcript file: read that for output and this
|
|
40798
40891
|
answer for state. Never decide a run has ended by watching the file stop growing, or a process
|
|
40799
|
-
list, or a timeout
|
|
40800
|
-
gdharness ended it
|
|
40892
|
+
list, or a timeout; a bench between prints looks exactly like a bench that died to everything else.
|
|
40893
|
+
\`endedBy\` says whether gdharness ended it, and \`running\` is asked of the operating system
|
|
40894
|
+
whenever there is a process to ask about: every run started through \`editor_run\`, and every
|
|
40895
|
+
editor-played run whose game announced its runtime. An editor-played run in a project with no
|
|
40896
|
+
runtime addon announces nothing, so there is no process id and \`running\` is the editor's answer,
|
|
40897
|
+
which can lag by seconds after the game has gone. \`editor_status\` names the game under
|
|
40898
|
+
\`runtimes\`, which does not depend on the editor answering.
|
|
40801
40899
|
- One server, one editor. A second project is a second harness session with its own server, and
|
|
40802
40900
|
that works: each editor is opened on its own language server and debug adapter ports, and says
|
|
40803
40901
|
where it serves. Two editors on one project is the thing to refuse.
|
package/build/index.js
CHANGED
|
@@ -28256,23 +28256,32 @@ function recordAt(path) {
|
|
|
28256
28256
|
}
|
|
28257
28257
|
function runningAs(pid) {
|
|
28258
28258
|
if (process.platform === "win32") {
|
|
28259
|
-
const
|
|
28260
|
-
if (
|
|
28261
|
-
|
|
28259
|
+
const answer = windowsCommandLine(pid);
|
|
28260
|
+
if (answer !== null) {
|
|
28261
|
+
const [, ...rest] = answer.split(`
|
|
28262
|
+
`);
|
|
28263
|
+
const line = rest.join(`
|
|
28264
|
+
`).trim();
|
|
28265
|
+
const began = startedAt(pid, answer);
|
|
28266
|
+
if (line !== "") {
|
|
28267
|
+
return { kind: "commandLine", text: line, ...began === null ? {} : { startedAt: began } };
|
|
28268
|
+
}
|
|
28262
28269
|
}
|
|
28263
28270
|
const image = windowsImage(pid);
|
|
28264
28271
|
return image === null ? null : { kind: "image", text: image };
|
|
28265
28272
|
}
|
|
28266
28273
|
try {
|
|
28274
|
+
const began = startedAt(pid, null);
|
|
28275
|
+
const when = began === null ? {} : { startedAt: began };
|
|
28267
28276
|
if (process.platform === "linux") {
|
|
28268
28277
|
const raw = readFileSync9(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
28269
|
-
return raw === "" ? null : { kind: "commandLine", text: raw };
|
|
28278
|
+
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
28270
28279
|
}
|
|
28271
28280
|
const args = execFileSync("ps", ["-p", String(pid), "-o", "args="], {
|
|
28272
28281
|
encoding: "utf8",
|
|
28273
28282
|
timeout: 15000
|
|
28274
28283
|
}).trim();
|
|
28275
|
-
return args === "" ? null : { kind: "commandLine", text: args };
|
|
28284
|
+
return args === "" ? null : { kind: "commandLine", text: args, ...when };
|
|
28276
28285
|
} catch {
|
|
28277
28286
|
return null;
|
|
28278
28287
|
}
|
|
@@ -28311,13 +28320,30 @@ function windowsCommandLine(pid) {
|
|
|
28311
28320
|
"-NoProfile",
|
|
28312
28321
|
"-NonInteractive",
|
|
28313
28322
|
"-Command",
|
|
28314
|
-
|
|
28323
|
+
`$p = Get-CimInstance Win32_Process -Filter "ProcessId=${pid}"; if ($p) { $p.CreationDate.ToUniversalTime().ToString("o"); $p.CommandLine }`
|
|
28315
28324
|
], { encoding: "utf8", timeout: 15000, windowsHide: true }).trim();
|
|
28316
28325
|
return answer === "" ? null : answer;
|
|
28317
28326
|
} catch {
|
|
28318
28327
|
return null;
|
|
28319
28328
|
}
|
|
28320
28329
|
}
|
|
28330
|
+
function startedAt(pid, windowsAnswer) {
|
|
28331
|
+
if (process.platform === "win32") {
|
|
28332
|
+
const when = Date.parse(windowsAnswer?.split(`
|
|
28333
|
+
`)[0]?.trim() ?? "");
|
|
28334
|
+
return Number.isNaN(when) ? null : when;
|
|
28335
|
+
}
|
|
28336
|
+
try {
|
|
28337
|
+
const said = execFileSync("ps", ["-p", String(pid), "-o", "lstart="], {
|
|
28338
|
+
encoding: "utf8",
|
|
28339
|
+
timeout: 15000
|
|
28340
|
+
}).trim();
|
|
28341
|
+
const when = Date.parse(said);
|
|
28342
|
+
return said === "" || Number.isNaN(when) ? null : when;
|
|
28343
|
+
} catch {
|
|
28344
|
+
return null;
|
|
28345
|
+
}
|
|
28346
|
+
}
|
|
28321
28347
|
function windowsImage(pid) {
|
|
28322
28348
|
try {
|
|
28323
28349
|
const csv = execFileSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
|
|
@@ -28336,16 +28362,24 @@ function stillTheRecordedRun(record) {
|
|
|
28336
28362
|
function couldStillBeTheRecordedRun(record) {
|
|
28337
28363
|
return judgeRun(record, runningAs(record.pid), "possible");
|
|
28338
28364
|
}
|
|
28365
|
+
var AN_EDITOR = /(?:^|\s)(?:-e|--editor)(?:\s|$)/;
|
|
28366
|
+
var SAME_RUN_WINDOW_MS = 1e4;
|
|
28339
28367
|
function judgeRun(record, running, needed) {
|
|
28340
28368
|
if (running === null) {
|
|
28341
28369
|
return false;
|
|
28342
28370
|
}
|
|
28371
|
+
if (running.startedAt !== undefined && running.startedAt - record.startedAt > SAME_RUN_WINDOW_MS) {
|
|
28372
|
+
return false;
|
|
28373
|
+
}
|
|
28343
28374
|
const engine = record.command === undefined ? null : basename(record.command);
|
|
28344
28375
|
const said = process.platform === "win32" ? running.text.toLowerCase() : running.text;
|
|
28345
28376
|
const wanted = engine === null || process.platform !== "win32" ? engine : engine.toLowerCase();
|
|
28346
28377
|
if (wanted !== null && !said.includes(wanted)) {
|
|
28347
28378
|
return false;
|
|
28348
28379
|
}
|
|
28380
|
+
if (running.kind === "commandLine" && AN_EDITOR.test(said)) {
|
|
28381
|
+
return false;
|
|
28382
|
+
}
|
|
28349
28383
|
if (record.projectPath === "") {
|
|
28350
28384
|
return wanted !== null;
|
|
28351
28385
|
}
|
|
@@ -29076,7 +29110,7 @@ var TOOL_SPECS = [
|
|
|
29076
29110
|
},
|
|
29077
29111
|
{
|
|
29078
29112
|
name: "editor_output",
|
|
29079
|
-
description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. Answers with the counts and the verdict as well as the entries. A run that has quit still answers here, with running false and its exit code, until the next one starts. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state:
|
|
29113
|
+
description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. Answers with the counts and the verdict as well as the entries. A run that has quit still answers here, with running false and its exit code, until the next one starts. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering.",
|
|
29080
29114
|
parameters: {
|
|
29081
29115
|
severity: {
|
|
29082
29116
|
type: "string",
|
|
@@ -29104,7 +29138,7 @@ var TOOL_SPECS = [
|
|
|
29104
29138
|
},
|
|
29105
29139
|
{
|
|
29106
29140
|
name: "editor_status",
|
|
29107
|
-
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. 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. 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.',
|
|
29141
|
+
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. 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. 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. 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.',
|
|
29108
29142
|
parameters: {},
|
|
29109
29143
|
requires: []
|
|
29110
29144
|
},
|
|
@@ -29534,12 +29568,18 @@ function stillRunning(run) {
|
|
|
29534
29568
|
if (!run.throughEditor && run.process === null && run.pid !== null) {
|
|
29535
29569
|
return alive(run.pid);
|
|
29536
29570
|
}
|
|
29571
|
+
if (run.announcedPid !== undefined && !alive(run.announcedPid)) {
|
|
29572
|
+
return false;
|
|
29573
|
+
}
|
|
29537
29574
|
return true;
|
|
29538
29575
|
}
|
|
29539
29576
|
function runIsUp(run, editorSays) {
|
|
29540
29577
|
if (run?.throughEditor !== true) {
|
|
29541
29578
|
return stillRunning(run);
|
|
29542
29579
|
}
|
|
29580
|
+
if (run.announcedPid !== undefined && !alive(run.announcedPid)) {
|
|
29581
|
+
return false;
|
|
29582
|
+
}
|
|
29543
29583
|
return editorSays ?? stillRunning(run);
|
|
29544
29584
|
}
|
|
29545
29585
|
function runtimeVerdict(endpoint, after) {
|
|
@@ -30970,7 +31010,10 @@ class GodotServer {
|
|
|
30970
31010
|
return `The debug adapter on port ${port} belongs to process ${holder}, and the editor on this server's bridge is process ${ours}. Godot gives every editor the same debug adapter port by default, so the one this editor names in its settings is not necessarily the one it holds. Playing the game now would put another editor's console and another project's debugger under this project's name.`;
|
|
30971
31011
|
}
|
|
30972
31012
|
anEditorIsStillComing(listeningSince) {
|
|
30973
|
-
return anEditorIsStillComing(listeningSince, this.
|
|
31013
|
+
return anEditorIsStillComing(listeningSince, this.launchedEditorIsUp());
|
|
31014
|
+
}
|
|
31015
|
+
launchedEditorIsUp() {
|
|
31016
|
+
return this.launchedEditor?.exitCode === null && this.launchedEditor.signalCode === null;
|
|
30974
31017
|
}
|
|
30975
31018
|
dap() {
|
|
30976
31019
|
const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
|
|
@@ -31016,7 +31059,7 @@ class GodotServer {
|
|
|
31016
31059
|
addonIsStale: status.connected ? stale : undefined,
|
|
31017
31060
|
bridgeAvailable: this.bridgeStartupError === null,
|
|
31018
31061
|
mayYetConnect: status.connected ? undefined : this.anEditorIsStillComing(status.listeningSince),
|
|
31019
|
-
awaitingLaunchedEditor: status.connected || this.
|
|
31062
|
+
awaitingLaunchedEditor: status.connected || !this.launchedEditorIsUp() ? undefined : this.launchedEditor?.pid ?? undefined,
|
|
31020
31063
|
...status.connected ? this.whatTheLaunchedEditorDropped() : {},
|
|
31021
31064
|
startupError: this.bridgeStartupError,
|
|
31022
31065
|
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
|
|
@@ -31052,7 +31095,8 @@ class GodotServer {
|
|
|
31052
31095
|
}
|
|
31053
31096
|
async handleEditorStatus() {
|
|
31054
31097
|
const godotPath = await this.locator.find();
|
|
31055
|
-
const
|
|
31098
|
+
const announced = runtimesAnnounced();
|
|
31099
|
+
const games = await Promise.all(announced.running.map(async (endpoint) => {
|
|
31056
31100
|
const reply = await runtimeRequest(endpoint, "ping", {}, this.runtimeTimeoutMs());
|
|
31057
31101
|
return {
|
|
31058
31102
|
pid: endpoint.pid,
|
|
@@ -31074,10 +31118,17 @@ class GodotServer {
|
|
|
31074
31118
|
version: godotPath === null ? null : await this.godotVersion(godotPath)
|
|
31075
31119
|
},
|
|
31076
31120
|
game: {
|
|
31077
|
-
processActive:
|
|
31121
|
+
processActive: runIsUp(this.currentRun(), playing === null ? null : playing.playing),
|
|
31078
31122
|
playingInEditor: playing,
|
|
31079
31123
|
runtimeConnected: games.some((game) => game.reachable),
|
|
31080
|
-
runtimes: games
|
|
31124
|
+
runtimes: games,
|
|
31125
|
+
unreadable: announced.unspoken.map((game) => ({
|
|
31126
|
+
pid: game.pid,
|
|
31127
|
+
project: game.project,
|
|
31128
|
+
protocol: game.protocol,
|
|
31129
|
+
serverSpeaks: RUNTIME_PROTOCOL,
|
|
31130
|
+
behind: game.protocol > RUNTIME_PROTOCOL ? "server" : "addon"
|
|
31131
|
+
}))
|
|
31081
31132
|
}
|
|
31082
31133
|
});
|
|
31083
31134
|
}
|
|
@@ -31227,7 +31278,7 @@ class GodotServer {
|
|
|
31227
31278
|
return { pid: null, error: started };
|
|
31228
31279
|
}
|
|
31229
31280
|
editor.unref();
|
|
31230
|
-
this.launchedEditor = editor.pid
|
|
31281
|
+
this.launchedEditor = editor.pid === undefined ? null : editor;
|
|
31231
31282
|
return { pid: editor.pid ?? null, error: null };
|
|
31232
31283
|
}
|
|
31233
31284
|
async portsForAnEditor() {
|
|
@@ -31274,6 +31325,7 @@ class GodotServer {
|
|
|
31274
31325
|
await this.endActiveGame("editor_run start, which ends the run that was going before it starts another");
|
|
31275
31326
|
}
|
|
31276
31327
|
const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
|
|
31328
|
+
const wereHereFirst = this.everyRuntimeOfOurs();
|
|
31277
31329
|
const headless = resolveHeadless(args["headless"], {
|
|
31278
31330
|
platform: process.platform,
|
|
31279
31331
|
variables: process.env
|
|
@@ -31294,16 +31346,20 @@ class GodotServer {
|
|
|
31294
31346
|
this.activeProcess = started;
|
|
31295
31347
|
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." : "";
|
|
31296
31348
|
const endedForThis = ended === null ? "" : ` The run that was going, pid ${ended}, was ended to start this one; its output is no longer what editor_output answers about.`;
|
|
31349
|
+
const stranded = wereHereFirst.filter((one) => one.pid !== ended && alive(one.pid));
|
|
31350
|
+
const named = stranded.map((one) => one.protocol === null ? `pid ${one.pid}` : `pid ${one.pid} on protocol ${one.protocol}`).join(", ");
|
|
31351
|
+
const alsoRunning = stranded.length === 0 ? "" : ` A game of this project was already running and still is: ${named}. Starting this one did not end it and nothing here can, because this server did not start it. End it yourself, or bring the halves to one version where a protocol is named.`;
|
|
31297
31352
|
return this.jsonTextResponse({
|
|
31298
31353
|
started: true,
|
|
31299
31354
|
through: "gdharness",
|
|
31355
|
+
alsoRunning: stranded.length === 0 ? undefined : stranded.map((one) => one.pid),
|
|
31300
31356
|
pid: started.process.pid ?? null,
|
|
31301
31357
|
arguments: cmdArgs,
|
|
31302
31358
|
transcript: started.transcript,
|
|
31303
31359
|
refreshedClasses: refreshed.value,
|
|
31304
31360
|
endedPreviousRun: ended ?? undefined,
|
|
31305
31361
|
runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
|
|
31306
|
-
message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}`
|
|
31362
|
+
message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}${alsoRunning}`
|
|
31307
31363
|
});
|
|
31308
31364
|
}
|
|
31309
31365
|
async gameIsUp() {
|
|
@@ -31319,6 +31375,10 @@ class GodotServer {
|
|
|
31319
31375
|
budgetMs,
|
|
31320
31376
|
giveUp: () => this.dapClient?.isStopped() === true || !stillRunning(this.currentRun())
|
|
31321
31377
|
});
|
|
31378
|
+
const going = this.currentRun();
|
|
31379
|
+
if (endpoint !== null && going?.throughEditor === true && going.announcedPid === undefined) {
|
|
31380
|
+
going.announcedPid = endpoint.pid;
|
|
31381
|
+
}
|
|
31322
31382
|
return runtimeVerdict(endpoint, {
|
|
31323
31383
|
addon: true,
|
|
31324
31384
|
budgetMs,
|
|
@@ -31428,9 +31488,15 @@ class GodotServer {
|
|
|
31428
31488
|
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.`);
|
|
31429
31489
|
return;
|
|
31430
31490
|
}
|
|
31491
|
+
const signalled = runningAs(running.pid);
|
|
31492
|
+
const described = signalled === null ? "nothing it would name" : `${signalled.kind}: ${signalled.text}`;
|
|
31493
|
+
let landed = true;
|
|
31431
31494
|
try {
|
|
31432
31495
|
process.kill(running.pid);
|
|
31433
|
-
} catch {
|
|
31496
|
+
} catch {
|
|
31497
|
+
landed = false;
|
|
31498
|
+
}
|
|
31499
|
+
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}.`);
|
|
31434
31500
|
}
|
|
31435
31501
|
spawnGame(godotPath, cmdArgs, env) {
|
|
31436
31502
|
const child = spawn(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
|
|
@@ -31619,7 +31685,12 @@ class GodotServer {
|
|
|
31619
31685
|
const noEditor = !status.connected ? " No editor is connected, so a game the editor is playing cannot be seen from here at all:" + " editor_status says whether one is on its way, and a run of yours that is still up is" + " reachable again once it is." : "";
|
|
31620
31686
|
const live = this.aRuntimeOfOursIsAnswering();
|
|
31621
31687
|
if (live !== null) {
|
|
31622
|
-
return `A game is running for ${live.project.path} and has announced its runtime on port ${live.port}, pid ${live.pid},` + " but this server did not start it and cannot reach it through the editor, so editor_run and" + " editor_output have nothing to answer about. The runtime_* tools reach it now." + "
|
|
31688
|
+
return `A game is running for ${live.project.path} and has announced its runtime on port ${live.port}, pid ${live.pid},` + " but this server did not start it and cannot reach it through the editor, so editor_run and" + " editor_output have nothing to answer about. The runtime_* tools reach it now." + " A run started here is a separate process rather than a replacement, so starting one leaves" + " this game running as well." + (this.godotBridge.getStatus().connected ? "" : " editor_status says whether an editor is on its way, and editor_run can end it once one is.");
|
|
31689
|
+
}
|
|
31690
|
+
const unreadable = this.aRuntimeOfOursIsTooFarOff();
|
|
31691
|
+
if (unreadable !== null) {
|
|
31692
|
+
const behind = unreadable.protocol > RUNTIME_PROTOCOL ? "this server is the older half: reconnect it so it spawns the installed version" : "the addon is the older half: reinstall it and restart the game";
|
|
31693
|
+
return `A game is running for ${unreadable.project.path}, pid ${unreadable.pid}, announced in protocol` + ` ${unreadable.protocol}, which this server does not speak: it speaks ${RUNTIME_PROTOCOL}, so ${behind}.` + " Until then neither editor_run nor the runtime_* tools can reach it." + " A run started here is a separate process rather than a replacement, so starting one leaves" + " this game running as well.";
|
|
31623
31694
|
}
|
|
31624
31695
|
const record = readRunRecord();
|
|
31625
31696
|
if (record === null) {
|
|
@@ -31631,12 +31702,34 @@ class GodotServer {
|
|
|
31631
31702
|
return `No game of this server's is running.${noEditor} A run started from ${whose} is recorded on this machine, and ${ours}, so it is not this server's to answer for or to end. Start one with editor_run.`;
|
|
31632
31703
|
}
|
|
31633
31704
|
aRuntimeOfOursIsAnswering() {
|
|
31705
|
+
return this.announcedForOurProject(discoverRuntimes());
|
|
31706
|
+
}
|
|
31707
|
+
aRuntimeOfOursIsTooFarOff() {
|
|
31708
|
+
return this.announcedForOurProject(runtimesAnnounced().unspoken);
|
|
31709
|
+
}
|
|
31710
|
+
announcedForOurProject(announced) {
|
|
31711
|
+
return this.allAnnouncedForOurProject(announced)[0] ?? null;
|
|
31712
|
+
}
|
|
31713
|
+
allAnnouncedForOurProject(announced) {
|
|
31634
31714
|
const status = this.godotBridge.getStatus();
|
|
31635
31715
|
const mine = this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
|
|
31636
31716
|
if (mine === null) {
|
|
31637
|
-
return
|
|
31717
|
+
return [];
|
|
31638
31718
|
}
|
|
31639
|
-
return
|
|
31719
|
+
return announced.filter((one) => one.project.path !== "" && isSameDirectory(mine, one.project.path));
|
|
31720
|
+
}
|
|
31721
|
+
everyRuntimeOfOurs() {
|
|
31722
|
+
const announced = runtimesAnnounced();
|
|
31723
|
+
return [
|
|
31724
|
+
...this.allAnnouncedForOurProject(announced.running).map((one) => ({
|
|
31725
|
+
pid: one.pid,
|
|
31726
|
+
protocol: null
|
|
31727
|
+
})),
|
|
31728
|
+
...this.allAnnouncedForOurProject(announced.unspoken).map((one) => ({
|
|
31729
|
+
pid: one.pid,
|
|
31730
|
+
protocol: one.protocol
|
|
31731
|
+
}))
|
|
31732
|
+
];
|
|
31640
31733
|
}
|
|
31641
31734
|
couldBeOurs(project) {
|
|
31642
31735
|
const status = this.godotBridge.getStatus();
|
|
@@ -31741,7 +31834,7 @@ class GodotServer {
|
|
|
31741
31834
|
notes.push(run.exitCode === 0 ? "This run quit on its own, cleanly: exit code 0." : "Nothing here ended this run: it stopped on its own or something outside this server stopped it.");
|
|
31742
31835
|
}
|
|
31743
31836
|
if (run.transcript !== null && selected.omitted > 0) {
|
|
31744
|
-
notes.push(`Everything this run has printed is in ${run.transcript}, uncapped and still being written. Read it for output, not for whether the run is alive: running above is asked of the operating system, and
|
|
31837
|
+
notes.push(`Everything this run has printed is in ${run.transcript}, uncapped and still being written. Read it for output, not for whether the run is alive: a transcript that stops growing is a run between prints. running above is asked of the operating system while there is a process to ask about, and for an editor-played run that announced no runtime it is the editor's answer instead, which can lag after the game has gone.`);
|
|
31745
31838
|
}
|
|
31746
31839
|
return this.jsonTextResponse({
|
|
31747
31840
|
running: stillRunning(run),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.36",
|
|
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",
|