gdharness 0.13.40 → 0.13.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js
CHANGED
|
@@ -15393,7 +15393,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15393
15393
|
};
|
|
15394
15394
|
RUNNING_PID = {
|
|
15395
15395
|
type: "number",
|
|
15396
|
-
description: "Which game, when several are running from one project, such as a bench and its workers: its process id, as editor_status lists under runtimes.
|
|
15396
|
+
description: "Which game, when several are running from one project, such as a bench and its workers: its process id, as editor_status lists under runtimes. Without it the game this server started or plays is the one asked when it is among them, and the answer says so under answeredBy; with one game per project it is not needed at all."
|
|
15397
15397
|
};
|
|
15398
15398
|
SCENE_PATH = {
|
|
15399
15399
|
type: "string",
|
|
@@ -16011,6 +16011,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
16011
16011
|
type: "number",
|
|
16012
16012
|
ops: ["check", "wait"],
|
|
16013
16013
|
description: "check: how long to give the boot before it is called hung. Default 60000. wait: how long to wait for the run to end before answering anyway. Default 600000."
|
|
16014
|
+
},
|
|
16015
|
+
andChildren: {
|
|
16016
|
+
type: "boolean",
|
|
16017
|
+
ops: ["stop"],
|
|
16018
|
+
description: "stop: also end what the game started for itself, with OS.create_process or otherwise, such as a bench's workers. Only the children of the run's process that are announced as games of this project are ended, named under endedChildren; any other child is left and named under childrenLeft. Default false, and the run's process is ended either way."
|
|
16014
16019
|
}
|
|
16015
16020
|
},
|
|
16016
16021
|
requires: [],
|
|
@@ -16020,7 +16025,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16020
16025
|
requires: ["projectPath"]
|
|
16021
16026
|
},
|
|
16022
16027
|
stop: {
|
|
16023
|
-
summary: "end the run and answer with what it printed last, naming the process ended under endedPid: a game that started processes of its own keeps them",
|
|
16028
|
+
summary: "end the run and answer with what it printed last, naming the process ended under endedPid: a game that started processes of its own keeps them unless andChildren says to end those too",
|
|
16024
16029
|
requires: []
|
|
16025
16030
|
},
|
|
16026
16031
|
check: {
|
|
@@ -16061,7 +16066,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16061
16066
|
},
|
|
16062
16067
|
{
|
|
16063
16068
|
name: "editor_status",
|
|
16064
|
-
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. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it.",
|
|
16069
|
+
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. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it.",
|
|
16065
16070
|
parameters: {},
|
|
16066
16071
|
requires: []
|
|
16067
16072
|
},
|
|
@@ -36413,23 +36418,55 @@ var init_lsp_client = __esm(() => {
|
|
|
36413
36418
|
init_ports();
|
|
36414
36419
|
});
|
|
36415
36420
|
|
|
36416
|
-
// src/process-
|
|
36421
|
+
// src/process-children.ts
|
|
36417
36422
|
import { execFile as execFile5 } from "node:child_process";
|
|
36418
36423
|
import { promisify as promisify5 } from "node:util";
|
|
36424
|
+
async function childrenOf(pid) {
|
|
36425
|
+
try {
|
|
36426
|
+
const printed = process.platform === "win32" ? await askWindows(pid) : await askPosix(pid);
|
|
36427
|
+
return printed.split(/\r?\n/).map((line) => Number(line.trim())).filter((child) => Number.isInteger(child) && child > 0);
|
|
36428
|
+
} catch (error) {
|
|
36429
|
+
if (process.platform !== "win32" && error.code === 1) {
|
|
36430
|
+
return [];
|
|
36431
|
+
}
|
|
36432
|
+
return;
|
|
36433
|
+
}
|
|
36434
|
+
}
|
|
36435
|
+
async function askWindows(pid) {
|
|
36436
|
+
const { stdout } = await run5("powershell.exe", [
|
|
36437
|
+
"-NoProfile",
|
|
36438
|
+
"-NonInteractive",
|
|
36439
|
+
"-Command",
|
|
36440
|
+
`(Get-CimInstance Win32_Process -Filter "ParentProcessId=${pid}").ProcessId`
|
|
36441
|
+
], { timeout: ASK_TIMEOUT_MS, windowsHide: true });
|
|
36442
|
+
return stdout;
|
|
36443
|
+
}
|
|
36444
|
+
async function askPosix(pid) {
|
|
36445
|
+
const { stdout } = await run5("pgrep", ["-P", String(pid)], { timeout: ASK_TIMEOUT_MS });
|
|
36446
|
+
return stdout;
|
|
36447
|
+
}
|
|
36448
|
+
var run5, ASK_TIMEOUT_MS = 15000;
|
|
36449
|
+
var init_process_children = __esm(() => {
|
|
36450
|
+
run5 = promisify5(execFile5);
|
|
36451
|
+
});
|
|
36452
|
+
|
|
36453
|
+
// src/process-time.ts
|
|
36454
|
+
import { execFile as execFile6 } from "node:child_process";
|
|
36455
|
+
import { promisify as promisify6 } from "node:util";
|
|
36419
36456
|
async function cpuSecondsOf(pid) {
|
|
36420
36457
|
try {
|
|
36421
|
-
return process.platform === "win32" ? await
|
|
36458
|
+
return process.platform === "win32" ? await askWindows2(pid) : await askPosix2(pid);
|
|
36422
36459
|
} catch {
|
|
36423
36460
|
return;
|
|
36424
36461
|
}
|
|
36425
36462
|
}
|
|
36426
|
-
async function
|
|
36427
|
-
const { stdout } = await
|
|
36463
|
+
async function askWindows2(pid) {
|
|
36464
|
+
const { stdout } = await run6("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", `(Get-Process -Id ${pid}).CPU`], { timeout: ASK_TIMEOUT_MS2, windowsHide: true });
|
|
36428
36465
|
const seconds = Number(stdout.trim().replace(",", "."));
|
|
36429
36466
|
return Number.isFinite(seconds) ? seconds : undefined;
|
|
36430
36467
|
}
|
|
36431
|
-
async function
|
|
36432
|
-
const { stdout } = await
|
|
36468
|
+
async function askPosix2(pid) {
|
|
36469
|
+
const { stdout } = await run6("ps", ["-o", "time=", "-p", String(pid)], { timeout: ASK_TIMEOUT_MS2 });
|
|
36433
36470
|
return secondsFromClock(stdout.trim());
|
|
36434
36471
|
}
|
|
36435
36472
|
function secondsFromClock(printed) {
|
|
@@ -36447,9 +36484,9 @@ function secondsFromClock(printed) {
|
|
|
36447
36484
|
const extra = days === null ? 0 : Number(days) * 86400;
|
|
36448
36485
|
return Number.isFinite(extra) ? seconds + extra : undefined;
|
|
36449
36486
|
}
|
|
36450
|
-
var
|
|
36487
|
+
var run6, ASK_TIMEOUT_MS2 = 6000;
|
|
36451
36488
|
var init_process_time = __esm(() => {
|
|
36452
|
-
|
|
36489
|
+
run6 = promisify6(execFile6);
|
|
36453
36490
|
});
|
|
36454
36491
|
|
|
36455
36492
|
// src/project-scan.ts
|
|
@@ -36676,7 +36713,18 @@ function parseAnnouncement(file, pid) {
|
|
|
36676
36713
|
if (protocol !== RUNTIME_PROTOCOL) {
|
|
36677
36714
|
return { kind: "unspoken", unspoken: { pid, protocol: protocol ?? 0, project: named } };
|
|
36678
36715
|
}
|
|
36679
|
-
|
|
36716
|
+
const editorPid = readNumber(fields, "editor_pid");
|
|
36717
|
+
return {
|
|
36718
|
+
kind: "runtime",
|
|
36719
|
+
endpoint: {
|
|
36720
|
+
pid,
|
|
36721
|
+
port,
|
|
36722
|
+
address,
|
|
36723
|
+
project: named,
|
|
36724
|
+
file,
|
|
36725
|
+
...editorPid !== undefined && Number.isInteger(editorPid) && editorPid > 0 ? { editorPid } : {}
|
|
36726
|
+
}
|
|
36727
|
+
};
|
|
36680
36728
|
}
|
|
36681
36729
|
function runtimesAnnounced(directories = runtimeDirectories()) {
|
|
36682
36730
|
const running = new Map;
|
|
@@ -36750,7 +36798,7 @@ async function announcedSince(projectPath, before, waiting = {}) {
|
|
|
36750
36798
|
const directories = waiting.directories ?? runtimeDirectories();
|
|
36751
36799
|
const until = Date.now() + Math.max(waiting.budgetMs ?? ANNOUNCE_BUDGET_MS, 0);
|
|
36752
36800
|
for (;; ) {
|
|
36753
|
-
const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && resolve4(endpoint.project.path) === wanted);
|
|
36801
|
+
const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && resolve4(endpoint.project.path) === wanted && (waiting.accept?.(endpoint) ?? true));
|
|
36754
36802
|
if (fresh !== undefined) {
|
|
36755
36803
|
return fresh;
|
|
36756
36804
|
}
|
|
@@ -37205,7 +37253,7 @@ var init_run_record = __esm(() => {
|
|
|
37205
37253
|
});
|
|
37206
37254
|
|
|
37207
37255
|
// src/server.ts
|
|
37208
|
-
import { execFile as
|
|
37256
|
+
import { execFile as execFile7, spawn } from "node:child_process";
|
|
37209
37257
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
37210
37258
|
import {
|
|
37211
37259
|
appendFileSync,
|
|
@@ -37223,7 +37271,7 @@ import { tmpdir as tmpdir5 } from "node:os";
|
|
|
37223
37271
|
import { basename as basename3, dirname as dirname11, join as join19, normalize as normalize3, relative as relative3 } from "node:path";
|
|
37224
37272
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
37225
37273
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
37226
|
-
import { promisify as
|
|
37274
|
+
import { promisify as promisify7 } from "node:util";
|
|
37227
37275
|
function patienceForFrames(frames, atLeast) {
|
|
37228
37276
|
return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
|
|
37229
37277
|
}
|
|
@@ -37274,6 +37322,18 @@ function endedPreviousRun(ended) {
|
|
|
37274
37322
|
}
|
|
37275
37323
|
return ended.pid ?? true;
|
|
37276
37324
|
}
|
|
37325
|
+
function aboutTheChildren(ended, throughEditor) {
|
|
37326
|
+
if (ended === null) {
|
|
37327
|
+
return throughEditor ? "Anything that game started for itself, with OS.create_process or otherwise, is not the editor's to stop and is still running; andChildren ends what it started that is announced as a game of this project." : "Anything that game started for itself, with OS.create_process or otherwise, is a separate process and is still running; andChildren ends what it started that is announced as a game of this project.";
|
|
37328
|
+
}
|
|
37329
|
+
if (ended.unknown) {
|
|
37330
|
+
return "andChildren was asked for and this platform would not list what the game had started, so nothing else was ended.";
|
|
37331
|
+
}
|
|
37332
|
+
const counted = (n) => `${n} ${n === 1 ? "process" : "processes"}`;
|
|
37333
|
+
const endedToo = ended.ended.length === 0 ? "The game had started nothing announced as a game of this project, so nothing else was ended." : `${counted(ended.ended.length)} the game had started for itself, announced as games of this project, ${ended.ended.length === 1 ? "was" : "were"} ended with it, named under endedChildren.`;
|
|
37334
|
+
const leftToo = ended.left.length === 0 ? "" : ` ${counted(ended.left.length)} it had started ${ended.left.length === 1 ? "was" : "were"} left, named under childrenLeft: not announced as a game of this project, so what ${ended.left.length === 1 ? "it is" : "they are"} cannot be told from here.`;
|
|
37335
|
+
return `${endedToo}${leftToo}`;
|
|
37336
|
+
}
|
|
37277
37337
|
function endedToStartThis(ended) {
|
|
37278
37338
|
if (ended === null) {
|
|
37279
37339
|
return "";
|
|
@@ -37425,6 +37485,16 @@ function hasMainScene(projectFile) {
|
|
|
37425
37485
|
const scene = parseProjectGodot(readFileSync17(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
37426
37486
|
return typeof scene === "string" && scene !== "";
|
|
37427
37487
|
}
|
|
37488
|
+
function runtimeAddonAnnouncesEditors(projectPath) {
|
|
37489
|
+
if (projectPath === null) {
|
|
37490
|
+
return false;
|
|
37491
|
+
}
|
|
37492
|
+
try {
|
|
37493
|
+
return readFileSync17(join19(projectPath, RUNTIME_AUTOLOAD2.path), "utf8").includes('"editor_pid"');
|
|
37494
|
+
} catch {
|
|
37495
|
+
return false;
|
|
37496
|
+
}
|
|
37497
|
+
}
|
|
37428
37498
|
function editorPlaysHeadless(projectFile) {
|
|
37429
37499
|
const runArgs = parseProjectGodot(readFileSync17(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
37430
37500
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
@@ -37963,7 +38033,7 @@ class GodotServer {
|
|
|
37963
38033
|
return op === "restart" ? await this.handleRestartEditor() : await this.handleLaunchEditor(args);
|
|
37964
38034
|
case "editor_run":
|
|
37965
38035
|
if (op === "stop") {
|
|
37966
|
-
return await this.handleStopProject();
|
|
38036
|
+
return await this.handleStopProject(args);
|
|
37967
38037
|
}
|
|
37968
38038
|
if (op === "wait") {
|
|
37969
38039
|
return await this.handleWaitForRun(args);
|
|
@@ -38406,7 +38476,7 @@ class GodotServer {
|
|
|
38406
38476
|
const log = new GameLog;
|
|
38407
38477
|
let exitCode = 0;
|
|
38408
38478
|
try {
|
|
38409
|
-
const { stdout, stderr } = await
|
|
38479
|
+
const { stdout, stderr } = await run7(engine.value, exportArgs, { timeout: 300000 });
|
|
38410
38480
|
log.append("stdout", stdout);
|
|
38411
38481
|
log.append("stderr", stderr);
|
|
38412
38482
|
} catch (error) {
|
|
@@ -38603,7 +38673,7 @@ class GodotServer {
|
|
|
38603
38673
|
}
|
|
38604
38674
|
async godotVersion(godotPath) {
|
|
38605
38675
|
try {
|
|
38606
|
-
const { stdout } = await
|
|
38676
|
+
const { stdout } = await run7(godotPath, ["--version"], { timeout: 1e4 });
|
|
38607
38677
|
this.lastGodotVersion = stdout.trim();
|
|
38608
38678
|
return this.lastGodotVersion;
|
|
38609
38679
|
} catch (error) {
|
|
@@ -38862,7 +38932,7 @@ class GodotServer {
|
|
|
38862
38932
|
const located = await this.locator.find();
|
|
38863
38933
|
const announced = runtimesAnnounced();
|
|
38864
38934
|
const pinged = Promise.all(announced.running.map(async (endpoint) => {
|
|
38865
|
-
const held = this.knownHoldOn(endpoint);
|
|
38935
|
+
const held = this.knownHoldOn(endpoint, announced.running);
|
|
38866
38936
|
const reply = held === null ? await runtimeRequest(endpoint, "ping", {}, HOLD_PING_MS) : {
|
|
38867
38937
|
ok: false,
|
|
38868
38938
|
message: `The game is held by the editor's debugger, ${describeHalt(held)}, and answers no runtime call while it is. debug_control continue lets it go.`
|
|
@@ -38871,6 +38941,7 @@ class GodotServer {
|
|
|
38871
38941
|
pid: endpoint.pid,
|
|
38872
38942
|
port: endpoint.port,
|
|
38873
38943
|
project: endpoint.project,
|
|
38944
|
+
editorPid: endpoint.editorPid,
|
|
38874
38945
|
reachable: reply.ok,
|
|
38875
38946
|
problem: reply.ok ? null : reply.message
|
|
38876
38947
|
};
|
|
@@ -39166,6 +39237,7 @@ class GodotServer {
|
|
|
39166
39237
|
let editorSaysGoing = true;
|
|
39167
39238
|
const endpoint = await announcedSince(projectPath, before, {
|
|
39168
39239
|
budgetMs,
|
|
39240
|
+
accept: (one) => this.isTheGameOf(this.currentRun(), one, projectPath),
|
|
39169
39241
|
giveUp: () => {
|
|
39170
39242
|
if (this.dapClient?.isStopped() === true) {
|
|
39171
39243
|
return true;
|
|
@@ -39471,18 +39543,50 @@ class GodotServer {
|
|
|
39471
39543
|
};
|
|
39472
39544
|
this.activeProcess = picked;
|
|
39473
39545
|
}
|
|
39474
|
-
announcedPidOf(run) {
|
|
39546
|
+
announcedPidOf(run, announced) {
|
|
39475
39547
|
if (!run.throughEditor || run.announcedPid !== undefined) {
|
|
39476
39548
|
return run.announcedPid;
|
|
39477
39549
|
}
|
|
39478
39550
|
const before = run.announcedBefore ?? new Set;
|
|
39479
|
-
const fresh = this.allAnnouncedForOurProject(runtimesAnnounced().running).filter((one) => !before.has(one.pid));
|
|
39480
|
-
const
|
|
39551
|
+
const fresh = this.allAnnouncedForOurProject(announced ?? runtimesAnnounced().running).filter((one) => !before.has(one.pid));
|
|
39552
|
+
const candidates = fresh.filter((one) => this.playedByOurEditor(one, run.projectPath));
|
|
39553
|
+
const theOne = candidates.length === 1 ? candidates[0] : undefined;
|
|
39481
39554
|
if (theOne !== undefined) {
|
|
39482
39555
|
run.announcedPid = theOne.pid;
|
|
39483
39556
|
}
|
|
39484
39557
|
return run.announcedPid;
|
|
39485
39558
|
}
|
|
39559
|
+
ownGameAmong(running, projectPath) {
|
|
39560
|
+
const run = this.currentRun();
|
|
39561
|
+
if (run === null || !stillRunning(run)) {
|
|
39562
|
+
return;
|
|
39563
|
+
}
|
|
39564
|
+
if (projectPath !== undefined && run.projectPath !== null && !isSameDirectory(run.projectPath, projectPath)) {
|
|
39565
|
+
return;
|
|
39566
|
+
}
|
|
39567
|
+
const its = run.throughEditor ? this.announcedPidOf(run, running) : run.pid;
|
|
39568
|
+
return its !== null && its !== undefined && running.some((one) => one.pid === its) ? its : undefined;
|
|
39569
|
+
}
|
|
39570
|
+
playedByOurEditor(endpoint, projectPath) {
|
|
39571
|
+
const editor = this.godotBridge.getStatus().editorPid;
|
|
39572
|
+
if (editor === undefined) {
|
|
39573
|
+
return true;
|
|
39574
|
+
}
|
|
39575
|
+
if (endpoint.editorPid !== undefined) {
|
|
39576
|
+
return endpoint.editorPid === editor;
|
|
39577
|
+
}
|
|
39578
|
+
return !runtimeAddonAnnouncesEditors(projectPath ?? this.ownProject);
|
|
39579
|
+
}
|
|
39580
|
+
isTheGameOf(run, endpoint, projectPath) {
|
|
39581
|
+
if (run === null) {
|
|
39582
|
+
return false;
|
|
39583
|
+
}
|
|
39584
|
+
if (run.throughEditor) {
|
|
39585
|
+
return this.playedByOurEditor(endpoint, projectPath);
|
|
39586
|
+
}
|
|
39587
|
+
const editor = this.godotBridge.getStatus().editorPid;
|
|
39588
|
+
return endpoint.editorPid === undefined || endpoint.editorPid !== editor;
|
|
39589
|
+
}
|
|
39486
39590
|
adoptRecordedRun() {
|
|
39487
39591
|
const record = readRunRecord();
|
|
39488
39592
|
if (record === null || !this.couldBeOurs(record.projectPath)) {
|
|
@@ -39626,6 +39730,9 @@ class GodotServer {
|
|
|
39626
39730
|
if (announced !== undefined && !alive(announced)) {
|
|
39627
39731
|
return false;
|
|
39628
39732
|
}
|
|
39733
|
+
if (announced !== undefined && editorSays === undefined) {
|
|
39734
|
+
return stillRunning(run);
|
|
39735
|
+
}
|
|
39629
39736
|
return runIsUp(run, editorSays === undefined ? (await this.editorPlayingState())?.playing ?? null : editorSays);
|
|
39630
39737
|
}
|
|
39631
39738
|
async handleGetDebugOutput(args, waitedMs) {
|
|
@@ -39699,7 +39806,7 @@ class GodotServer {
|
|
|
39699
39806
|
entries: forAnswer(selected.entries)
|
|
39700
39807
|
});
|
|
39701
39808
|
}
|
|
39702
|
-
async handleStopProject() {
|
|
39809
|
+
async handleStopProject(args) {
|
|
39703
39810
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
39704
39811
|
const stopped = this.currentRun();
|
|
39705
39812
|
if (!stopped) {
|
|
@@ -39708,21 +39815,48 @@ class GodotServer {
|
|
|
39708
39815
|
this.drainEditorOutput(stopped);
|
|
39709
39816
|
this.drainTranscript(stopped);
|
|
39710
39817
|
const wasRunning = await this.runStillGoing(stopped);
|
|
39818
|
+
const endedPid = stopped.pid ?? this.announcedPidOf(stopped) ?? null;
|
|
39819
|
+
const children = readBoolean(args, "andChildren") === true && wasRunning && endedPid !== null ? await childrenOf(endedPid) : null;
|
|
39711
39820
|
this.logDebug("Stopping the running game");
|
|
39712
39821
|
await this.endActiveGame("editor_run stop");
|
|
39822
|
+
const ended = children === null ? null : this.endChildrenAmong(children, stopped.projectPath);
|
|
39713
39823
|
return this.jsonTextResponse({
|
|
39714
39824
|
stopped: true,
|
|
39715
39825
|
through: stopped.throughEditor ? "editor" : "gdharness",
|
|
39716
|
-
endedPid
|
|
39826
|
+
endedPid,
|
|
39827
|
+
...ended === null ? {} : {
|
|
39828
|
+
endedChildren: ended.ended,
|
|
39829
|
+
childrenLeft: ended.left.length === 0 ? undefined : ended.left,
|
|
39830
|
+
childrenUnknown: ended.unknown ? true : undefined
|
|
39831
|
+
},
|
|
39717
39832
|
exitedBeforeStop: stopped.exitCode !== null || !wasRunning,
|
|
39718
39833
|
exitCode: stopped.exitCode,
|
|
39719
39834
|
errors: stopped.log.count("error"),
|
|
39720
39835
|
warnings: stopped.log.count("warning"),
|
|
39721
39836
|
clean: stopped.log.count("error") === 0,
|
|
39722
|
-
note: !wasRunning ? "This run was over before the stop, so nothing was ended here: editor_output has what it printed and how it ended. Anything that game started for itself, with OS.create_process or otherwise, is a separate process and may still be running." : stopped.throughEditor ? "The editor was asked to stop the scene it is playing.
|
|
39837
|
+
note: !wasRunning ? "This run was over before the stop, so nothing was ended here: editor_output has what it printed and how it ended. Anything that game started for itself, with OS.create_process or otherwise, is a separate process and may still be running." : `${stopped.throughEditor ? "The editor was asked to stop the scene it is playing." : "The process named under endedPid was ended."} ${aboutTheChildren(ended, stopped.throughEditor)}`,
|
|
39723
39838
|
entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
39724
39839
|
});
|
|
39725
39840
|
}
|
|
39841
|
+
endChildrenAmong(children, projectPath) {
|
|
39842
|
+
if (children === undefined) {
|
|
39843
|
+
return { ended: [], left: [], unknown: true };
|
|
39844
|
+
}
|
|
39845
|
+
const games = new Set(runtimesAnnounced().running.filter((one) => projectPath === null || isSameDirectory(one.project.path, projectPath)).map((one) => one.pid));
|
|
39846
|
+
const ended = [];
|
|
39847
|
+
const left = [];
|
|
39848
|
+
for (const child of children) {
|
|
39849
|
+
if (!games.has(child)) {
|
|
39850
|
+
left.push(child);
|
|
39851
|
+
continue;
|
|
39852
|
+
}
|
|
39853
|
+
try {
|
|
39854
|
+
process.kill(child);
|
|
39855
|
+
ended.push(child);
|
|
39856
|
+
} catch {}
|
|
39857
|
+
}
|
|
39858
|
+
return { ended, left, unknown: false };
|
|
39859
|
+
}
|
|
39726
39860
|
async handleRescanFilesystem(args) {
|
|
39727
39861
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 30000;
|
|
39728
39862
|
const started = Date.now();
|
|
@@ -39870,13 +40004,13 @@ class GodotServer {
|
|
|
39870
40004
|
const override = Number.parseInt(envValue("GDHARNESS_RUNTIME_TIMEOUT_MS") ?? "", 10);
|
|
39871
40005
|
return Number.isInteger(override) && override > 0 ? override : 1e4;
|
|
39872
40006
|
}
|
|
39873
|
-
knownHoldOn(endpoint) {
|
|
40007
|
+
knownHoldOn(endpoint, announced) {
|
|
39874
40008
|
const run = this.activeProcess;
|
|
39875
40009
|
const session = this.dapClient;
|
|
39876
40010
|
if (run === null || !run.throughEditor || !stillRunning(run) || session === null) {
|
|
39877
40011
|
return null;
|
|
39878
40012
|
}
|
|
39879
|
-
if (this.announcedPidOf(run) !== endpoint.pid || !session.holdIsKnown()) {
|
|
40013
|
+
if (this.announcedPidOf(run, announced) !== endpoint.pid || !session.holdIsKnown()) {
|
|
39880
40014
|
return null;
|
|
39881
40015
|
}
|
|
39882
40016
|
return session.whereItStopped();
|
|
@@ -39917,11 +40051,16 @@ class GodotServer {
|
|
|
39917
40051
|
async handleRuntimeCommand(command, args, timeoutMs = this.runtimeTimeoutMs()) {
|
|
39918
40052
|
const { op: _op, projectPath, pid, ...params } = asParams(args);
|
|
39919
40053
|
const announced = runtimesAnnounced();
|
|
39920
|
-
const
|
|
40054
|
+
const wanted = typeof projectPath === "string" ? projectPath : this.ownProject ?? undefined;
|
|
40055
|
+
const own = typeof pid === "number" ? undefined : this.ownGameAmong(announced.running, wanted);
|
|
40056
|
+
const choice = chooseRuntime(announced.running, wanted, announced.unspoken, typeof pid === "number" ? pid : own);
|
|
39921
40057
|
if ("problem" in choice) {
|
|
39922
40058
|
return this.createErrorResponse(choice.problem);
|
|
39923
40059
|
}
|
|
39924
|
-
const
|
|
40060
|
+
const ofProject = announced.running.filter((one) => wanted === undefined || isSameDirectory(one.project.path, wanted));
|
|
40061
|
+
const chosenAmongSeveral = own !== undefined && ofProject.length > 1;
|
|
40062
|
+
const answeredBy = chosenAmongSeveral ? { answeredBy: own } : {};
|
|
40063
|
+
const held = this.knownHoldOn(choice.endpoint, announced.running);
|
|
39925
40064
|
if (held !== null) {
|
|
39926
40065
|
return this.createErrorResponse(`The game (pid ${choice.endpoint.pid}) is held by the editor's debugger, ${describeHalt(held)}, and answers no runtime call while it is.`, [
|
|
39927
40066
|
"debug_control continue lets it go, after which this call answers",
|
|
@@ -39939,7 +40078,7 @@ class GodotServer {
|
|
|
39939
40078
|
}
|
|
39940
40079
|
const { id: _id, ...payload } = reply.payload;
|
|
39941
40080
|
if (!expectsScreenshot) {
|
|
39942
|
-
return this.jsonTextResponse(payload);
|
|
40081
|
+
return this.jsonTextResponse({ ...payload, ...answeredBy });
|
|
39943
40082
|
}
|
|
39944
40083
|
const returnedPath = readString(payload, "path");
|
|
39945
40084
|
if (readString(payload, "type") !== "screenshot_file" || !returnedPath || !screenshotPath) {
|
|
@@ -39951,7 +40090,10 @@ class GodotServer {
|
|
|
39951
40090
|
const dimensions = `${readNumber(payload, "width") ?? 0}x${readNumber(payload, "height") ?? 0} ${readString(payload, "format") ?? "unknown"}`;
|
|
39952
40091
|
return {
|
|
39953
40092
|
content: [
|
|
39954
|
-
{
|
|
40093
|
+
{
|
|
40094
|
+
type: "text",
|
|
40095
|
+
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}`
|
|
40096
|
+
},
|
|
39955
40097
|
{ type: "image", data: readFileSync17(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
39956
40098
|
]
|
|
39957
40099
|
};
|
|
@@ -40018,7 +40160,7 @@ async function runGodotServer() {
|
|
|
40018
40160
|
const server = new GodotServer;
|
|
40019
40161
|
await server.run();
|
|
40020
40162
|
}
|
|
40021
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250,
|
|
40163
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run7, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CACHE_WRITE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
40022
40164
|
var init_server3 = __esm(() => {
|
|
40023
40165
|
init_mcp();
|
|
40024
40166
|
init_stdio2();
|
|
@@ -40039,6 +40181,7 @@ var init_server3 = __esm(() => {
|
|
|
40039
40181
|
init_lsp_client();
|
|
40040
40182
|
init_paths();
|
|
40041
40183
|
init_ports();
|
|
40184
|
+
init_process_children();
|
|
40042
40185
|
init_process_time();
|
|
40043
40186
|
init_project_scan();
|
|
40044
40187
|
init_resources();
|
|
@@ -40050,7 +40193,7 @@ var init_server3 = __esm(() => {
|
|
|
40050
40193
|
init_setup();
|
|
40051
40194
|
init_tool_definitions();
|
|
40052
40195
|
init_update_check();
|
|
40053
|
-
|
|
40196
|
+
run7 = promisify7(execFile7);
|
|
40054
40197
|
__dirname2 = dirname11(fileURLToPath4(import.meta.url));
|
|
40055
40198
|
VALUE_NAMES = {
|
|
40056
40199
|
string: "a string",
|
|
@@ -41561,8 +41704,10 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
41561
41704
|
|
|
41562
41705
|
- ${projectPathSentence()}. The \`runtime_*\` and \`debug_*\` ones pick between running
|
|
41563
41706
|
games instead, by \`projectPath\`, or by \`pid\` when several run from one project, as a bench
|
|
41564
|
-
and its workers do
|
|
41565
|
-
|
|
41707
|
+
and its workers do: without \`pid\` the game this server started or plays is the one asked, and
|
|
41708
|
+
the answer says so under \`answeredBy\`; the others answer about the editor this server is
|
|
41709
|
+
connected to. There is no ambient project, and an argument a tool does not declare is refused
|
|
41710
|
+
rather than ignored.
|
|
41566
41711
|
- \`editor_status\` says whether an editor is connected and whether its addon matches the server.
|
|
41567
41712
|
\`addonIsStale\` means they differ, and \`staleNote\` says which half is behind: an editor that
|
|
41568
41713
|
needs restarting, or a server that needs reconnecting in your harness. Every answer that came
|
|
@@ -41607,6 +41752,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
41607
41752
|
| Where a 3D node is on screen | \`runtime_inspect\` \`rect\`, rather than unprojecting by hand |
|
|
41608
41753
|
| Wait for something in a running game | \`runtime_wait\`, never a sleep; \`until\` with \`says\` for a panel that rebuilds its own labels |
|
|
41609
41754
|
| Wait for a run to finish | \`editor_run wait\`, never a sleep or a shell loop on the pid: it answers as \`editor_output\` does, once the run is over |
|
|
41755
|
+
| End a bench and the workers it opened | \`editor_run stop\` with \`andChildren\`, never a process listing by scene name: it ends the children of the run's process that are announced as games of the project, names them under \`endedChildren\`, and leaves and names any other child |
|
|
41610
41756
|
| A picture, for a person who asked to see one | \`runtime_capture\` |
|
|
41611
41757
|
|
|
41612
41758
|
These need the runtime autoload, which \`gdharness setup\` registers. \`runtime_capture\` needs a
|
|
@@ -43,6 +43,12 @@ const DAP_ASKED: String = "GDHARNESS_DAP_PORT"
|
|
|
43
43
|
## What a server that opened this editor says about itself, so that being opened by one is stated
|
|
44
44
|
## rather than guessed at from the ports. Kept in step with `OPENED_BY_A_SERVER` in src/launch.ts.
|
|
45
45
|
const OPENED_BY_A_SERVER: String = "GDHARNESS_OPENED_BY_A_SERVER"
|
|
46
|
+
## Put into this editor's own environment as it loads, so every game it plays inherits it and
|
|
47
|
+
## the runtime addon can announce which editor played it. A game of the same project that some
|
|
48
|
+
## other server started has no such variable, or another editor's number, and that is what
|
|
49
|
+
## tells the two apart when both are announced at once. Kept in step with `EDITOR_PID_VARIABLE`
|
|
50
|
+
## in the runtime autoload.
|
|
51
|
+
const EDITOR_PID_VARIABLE: String = "GDHARNESS_EDITOR_PID"
|
|
46
52
|
|
|
47
53
|
## How long one attempt is given before the address is called a bad one.
|
|
48
54
|
##
|
|
@@ -93,6 +99,7 @@ func _ready() -> void:
|
|
|
93
99
|
_project_path = ProjectSettings.globalize_path("res://")
|
|
94
100
|
version_at_load = _loaded_version()
|
|
95
101
|
_keep_breakpoints_through_sessions()
|
|
102
|
+
OS.set_environment(EDITOR_PID_VARIABLE, str(OS.get_process_id()))
|
|
96
103
|
|
|
97
104
|
_reconnect_timer = Timer.new()
|
|
98
105
|
_reconnect_timer.one_shot = true
|
|
@@ -30,6 +30,12 @@ const SCRIPT_FLAGS: PackedStringArray = ["-s", "--script"]
|
|
|
30
30
|
## Serve a script run anyway, for somebody driving a `-s` script rather than a game.
|
|
31
31
|
const SCRIPT_RUNS_SETTING: String = "gdharness/runtime/serve_script_runs"
|
|
32
32
|
|
|
33
|
+
## The variable the editor addon puts into its own environment, which every game the editor plays
|
|
34
|
+
## inherits: it says which editor played this game, so a server can tell the editor's game from
|
|
35
|
+
## another of the same project that some other server started. Kept in step with
|
|
36
|
+
## `EDITOR_PID_VARIABLE` in the editor addon's bridge client.
|
|
37
|
+
const EDITOR_PID_VARIABLE: String = "GDHARNESS_EDITOR_PID"
|
|
38
|
+
|
|
33
39
|
var values: Values = Values.new()
|
|
34
40
|
|
|
35
41
|
# The modules are members and not locals of _init, because a Callable holds its object by id
|
|
@@ -250,7 +256,7 @@ func _announce(bind_address: String) -> void:
|
|
|
250
256
|
## What the announcement file and the welcome both carry: enough to pick this game out of
|
|
251
257
|
## several and to know whether the server speaks its protocol.
|
|
252
258
|
func _identity(bind_address: String) -> Dictionary:
|
|
253
|
-
|
|
259
|
+
var identity: Dictionary = {
|
|
254
260
|
"protocol": PROTOCOL,
|
|
255
261
|
"pid": OS.get_process_id(),
|
|
256
262
|
"port": _port,
|
|
@@ -262,6 +268,10 @@ func _identity(bind_address: String) -> Dictionary:
|
|
|
262
268
|
},
|
|
263
269
|
"godot": Engine.get_version_info().get("string", ""),
|
|
264
270
|
}
|
|
271
|
+
var played_by: String = OS.get_environment(EDITOR_PID_VARIABLE)
|
|
272
|
+
if played_by.is_valid_int():
|
|
273
|
+
identity["editor_pid"] = played_by.to_int()
|
|
274
|
+
return identity
|
|
265
275
|
|
|
266
276
|
|
|
267
277
|
func _send_welcome(client: StreamPeerTCP) -> void:
|
package/build/index.js
CHANGED
|
@@ -10168,7 +10168,7 @@ function defectReport(where, error, context = {}) {
|
|
|
10168
10168
|
}
|
|
10169
10169
|
|
|
10170
10170
|
// src/server.ts
|
|
10171
|
-
import { execFile as
|
|
10171
|
+
import { execFile as execFile5, spawn } from "node:child_process";
|
|
10172
10172
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
10173
10173
|
import {
|
|
10174
10174
|
appendFileSync,
|
|
@@ -10186,7 +10186,7 @@ import { tmpdir as tmpdir4 } from "node:os";
|
|
|
10186
10186
|
import { basename as basename2, dirname as dirname6, join as join13, normalize as normalize2, relative as relative3 } from "node:path";
|
|
10187
10187
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
10188
10188
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
10189
|
-
import { promisify as
|
|
10189
|
+
import { promisify as promisify5 } from "node:util";
|
|
10190
10190
|
|
|
10191
10191
|
// node_modules/zod/v3/helpers/util.js
|
|
10192
10192
|
var util;
|
|
@@ -27709,25 +27709,55 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
27709
27709
|
}
|
|
27710
27710
|
}
|
|
27711
27711
|
|
|
27712
|
-
// src/process-
|
|
27712
|
+
// src/process-children.ts
|
|
27713
27713
|
import { execFile as execFile3 } from "node:child_process";
|
|
27714
27714
|
import { promisify as promisify3 } from "node:util";
|
|
27715
27715
|
var run3 = promisify3(execFile3);
|
|
27716
|
-
var ASK_TIMEOUT_MS =
|
|
27716
|
+
var ASK_TIMEOUT_MS = 15000;
|
|
27717
|
+
async function childrenOf(pid) {
|
|
27718
|
+
try {
|
|
27719
|
+
const printed = process.platform === "win32" ? await askWindows(pid) : await askPosix(pid);
|
|
27720
|
+
return printed.split(/\r?\n/).map((line) => Number(line.trim())).filter((child) => Number.isInteger(child) && child > 0);
|
|
27721
|
+
} catch (error) {
|
|
27722
|
+
if (process.platform !== "win32" && error.code === 1) {
|
|
27723
|
+
return [];
|
|
27724
|
+
}
|
|
27725
|
+
return;
|
|
27726
|
+
}
|
|
27727
|
+
}
|
|
27728
|
+
async function askWindows(pid) {
|
|
27729
|
+
const { stdout } = await run3("powershell.exe", [
|
|
27730
|
+
"-NoProfile",
|
|
27731
|
+
"-NonInteractive",
|
|
27732
|
+
"-Command",
|
|
27733
|
+
`(Get-CimInstance Win32_Process -Filter "ParentProcessId=${pid}").ProcessId`
|
|
27734
|
+
], { timeout: ASK_TIMEOUT_MS, windowsHide: true });
|
|
27735
|
+
return stdout;
|
|
27736
|
+
}
|
|
27737
|
+
async function askPosix(pid) {
|
|
27738
|
+
const { stdout } = await run3("pgrep", ["-P", String(pid)], { timeout: ASK_TIMEOUT_MS });
|
|
27739
|
+
return stdout;
|
|
27740
|
+
}
|
|
27741
|
+
|
|
27742
|
+
// src/process-time.ts
|
|
27743
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
27744
|
+
import { promisify as promisify4 } from "node:util";
|
|
27745
|
+
var run4 = promisify4(execFile4);
|
|
27746
|
+
var ASK_TIMEOUT_MS2 = 6000;
|
|
27717
27747
|
async function cpuSecondsOf(pid) {
|
|
27718
27748
|
try {
|
|
27719
|
-
return process.platform === "win32" ? await
|
|
27749
|
+
return process.platform === "win32" ? await askWindows2(pid) : await askPosix2(pid);
|
|
27720
27750
|
} catch {
|
|
27721
27751
|
return;
|
|
27722
27752
|
}
|
|
27723
27753
|
}
|
|
27724
|
-
async function
|
|
27725
|
-
const { stdout } = await
|
|
27754
|
+
async function askWindows2(pid) {
|
|
27755
|
+
const { stdout } = await run4("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", `(Get-Process -Id ${pid}).CPU`], { timeout: ASK_TIMEOUT_MS2, windowsHide: true });
|
|
27726
27756
|
const seconds = Number(stdout.trim().replace(",", "."));
|
|
27727
27757
|
return Number.isFinite(seconds) ? seconds : undefined;
|
|
27728
27758
|
}
|
|
27729
|
-
async function
|
|
27730
|
-
const { stdout } = await
|
|
27759
|
+
async function askPosix2(pid) {
|
|
27760
|
+
const { stdout } = await run4("ps", ["-o", "time=", "-p", String(pid)], { timeout: ASK_TIMEOUT_MS2 });
|
|
27731
27761
|
return secondsFromClock(stdout.trim());
|
|
27732
27762
|
}
|
|
27733
27763
|
function secondsFromClock(printed) {
|
|
@@ -28270,7 +28300,18 @@ function parseAnnouncement(file, pid) {
|
|
|
28270
28300
|
if (protocol !== RUNTIME_PROTOCOL) {
|
|
28271
28301
|
return { kind: "unspoken", unspoken: { pid, protocol: protocol ?? 0, project: named } };
|
|
28272
28302
|
}
|
|
28273
|
-
|
|
28303
|
+
const editorPid = readNumber(fields, "editor_pid");
|
|
28304
|
+
return {
|
|
28305
|
+
kind: "runtime",
|
|
28306
|
+
endpoint: {
|
|
28307
|
+
pid,
|
|
28308
|
+
port,
|
|
28309
|
+
address,
|
|
28310
|
+
project: named,
|
|
28311
|
+
file,
|
|
28312
|
+
...editorPid !== undefined && Number.isInteger(editorPid) && editorPid > 0 ? { editorPid } : {}
|
|
28313
|
+
}
|
|
28314
|
+
};
|
|
28274
28315
|
}
|
|
28275
28316
|
function runtimesAnnounced(directories = runtimeDirectories()) {
|
|
28276
28317
|
const running = new Map;
|
|
@@ -28349,7 +28390,7 @@ async function announcedSince(projectPath, before, waiting = {}) {
|
|
|
28349
28390
|
const directories = waiting.directories ?? runtimeDirectories();
|
|
28350
28391
|
const until = Date.now() + Math.max(waiting.budgetMs ?? ANNOUNCE_BUDGET_MS, 0);
|
|
28351
28392
|
for (;; ) {
|
|
28352
|
-
const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && resolve4(endpoint.project.path) === wanted);
|
|
28393
|
+
const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && resolve4(endpoint.project.path) === wanted && (waiting.accept?.(endpoint) ?? true));
|
|
28353
28394
|
if (fresh !== undefined) {
|
|
28354
28395
|
return fresh;
|
|
28355
28396
|
}
|
|
@@ -28837,7 +28878,7 @@ var RUNNING_PROJECT_PATH = {
|
|
|
28837
28878
|
};
|
|
28838
28879
|
var RUNNING_PID = {
|
|
28839
28880
|
type: "number",
|
|
28840
|
-
description: "Which game, when several are running from one project, such as a bench and its workers: its process id, as editor_status lists under runtimes.
|
|
28881
|
+
description: "Which game, when several are running from one project, such as a bench and its workers: its process id, as editor_status lists under runtimes. Without it the game this server started or plays is the one asked when it is among them, and the answer says so under answeredBy; with one game per project it is not needed at all."
|
|
28841
28882
|
};
|
|
28842
28883
|
var SCENE_PATH = {
|
|
28843
28884
|
type: "string",
|
|
@@ -29455,6 +29496,11 @@ var TOOL_SPECS = [
|
|
|
29455
29496
|
type: "number",
|
|
29456
29497
|
ops: ["check", "wait"],
|
|
29457
29498
|
description: "check: how long to give the boot before it is called hung. Default 60000. wait: how long to wait for the run to end before answering anyway. Default 600000."
|
|
29499
|
+
},
|
|
29500
|
+
andChildren: {
|
|
29501
|
+
type: "boolean",
|
|
29502
|
+
ops: ["stop"],
|
|
29503
|
+
description: "stop: also end what the game started for itself, with OS.create_process or otherwise, such as a bench's workers. Only the children of the run's process that are announced as games of this project are ended, named under endedChildren; any other child is left and named under childrenLeft. Default false, and the run's process is ended either way."
|
|
29458
29504
|
}
|
|
29459
29505
|
},
|
|
29460
29506
|
requires: [],
|
|
@@ -29464,7 +29510,7 @@ var TOOL_SPECS = [
|
|
|
29464
29510
|
requires: ["projectPath"]
|
|
29465
29511
|
},
|
|
29466
29512
|
stop: {
|
|
29467
|
-
summary: "end the run and answer with what it printed last, naming the process ended under endedPid: a game that started processes of its own keeps them",
|
|
29513
|
+
summary: "end the run and answer with what it printed last, naming the process ended under endedPid: a game that started processes of its own keeps them unless andChildren says to end those too",
|
|
29468
29514
|
requires: []
|
|
29469
29515
|
},
|
|
29470
29516
|
check: {
|
|
@@ -29505,7 +29551,7 @@ var TOOL_SPECS = [
|
|
|
29505
29551
|
},
|
|
29506
29552
|
{
|
|
29507
29553
|
name: "editor_status",
|
|
29508
|
-
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. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it.",
|
|
29554
|
+
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. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it.",
|
|
29509
29555
|
parameters: {},
|
|
29510
29556
|
requires: []
|
|
29511
29557
|
},
|
|
@@ -29926,7 +29972,7 @@ function buildToolDefinitions() {
|
|
|
29926
29972
|
// src/server.ts
|
|
29927
29973
|
var UPDATE_NOTICE_EVERY = 500;
|
|
29928
29974
|
var FEEDBACK_NOTICE_EVERY = 250;
|
|
29929
|
-
var
|
|
29975
|
+
var run5 = promisify5(execFile5);
|
|
29930
29976
|
var __dirname2 = dirname6(fileURLToPath2(import.meta.url));
|
|
29931
29977
|
var EDITOR_RESTART_TIMEOUT_MS = 90000;
|
|
29932
29978
|
var HOLD_PING_MS = 2000;
|
|
@@ -29981,6 +30027,18 @@ function endedPreviousRun(ended) {
|
|
|
29981
30027
|
}
|
|
29982
30028
|
return ended.pid ?? true;
|
|
29983
30029
|
}
|
|
30030
|
+
function aboutTheChildren(ended, throughEditor) {
|
|
30031
|
+
if (ended === null) {
|
|
30032
|
+
return throughEditor ? "Anything that game started for itself, with OS.create_process or otherwise, is not the editor's to stop and is still running; andChildren ends what it started that is announced as a game of this project." : "Anything that game started for itself, with OS.create_process or otherwise, is a separate process and is still running; andChildren ends what it started that is announced as a game of this project.";
|
|
30033
|
+
}
|
|
30034
|
+
if (ended.unknown) {
|
|
30035
|
+
return "andChildren was asked for and this platform would not list what the game had started, so nothing else was ended.";
|
|
30036
|
+
}
|
|
30037
|
+
const counted = (n) => `${n} ${n === 1 ? "process" : "processes"}`;
|
|
30038
|
+
const endedToo = ended.ended.length === 0 ? "The game had started nothing announced as a game of this project, so nothing else was ended." : `${counted(ended.ended.length)} the game had started for itself, announced as games of this project, ${ended.ended.length === 1 ? "was" : "were"} ended with it, named under endedChildren.`;
|
|
30039
|
+
const leftToo = ended.left.length === 0 ? "" : ` ${counted(ended.left.length)} it had started ${ended.left.length === 1 ? "was" : "were"} left, named under childrenLeft: not announced as a game of this project, so what ${ended.left.length === 1 ? "it is" : "they are"} cannot be told from here.`;
|
|
30040
|
+
return `${endedToo}${leftToo}`;
|
|
30041
|
+
}
|
|
29984
30042
|
function endedToStartThis(ended) {
|
|
29985
30043
|
if (ended === null) {
|
|
29986
30044
|
return "";
|
|
@@ -30174,6 +30232,16 @@ function hasMainScene(projectFile) {
|
|
|
30174
30232
|
const scene = parseProjectGodot(readFileSync13(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
30175
30233
|
return typeof scene === "string" && scene !== "";
|
|
30176
30234
|
}
|
|
30235
|
+
function runtimeAddonAnnouncesEditors(projectPath) {
|
|
30236
|
+
if (projectPath === null) {
|
|
30237
|
+
return false;
|
|
30238
|
+
}
|
|
30239
|
+
try {
|
|
30240
|
+
return readFileSync13(join13(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
|
|
30241
|
+
} catch {
|
|
30242
|
+
return false;
|
|
30243
|
+
}
|
|
30244
|
+
}
|
|
30177
30245
|
function editorPlaysHeadless(projectFile) {
|
|
30178
30246
|
const runArgs = parseProjectGodot(readFileSync13(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
30179
30247
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
@@ -30712,7 +30780,7 @@ class GodotServer {
|
|
|
30712
30780
|
return op === "restart" ? await this.handleRestartEditor() : await this.handleLaunchEditor(args);
|
|
30713
30781
|
case "editor_run":
|
|
30714
30782
|
if (op === "stop") {
|
|
30715
|
-
return await this.handleStopProject();
|
|
30783
|
+
return await this.handleStopProject(args);
|
|
30716
30784
|
}
|
|
30717
30785
|
if (op === "wait") {
|
|
30718
30786
|
return await this.handleWaitForRun(args);
|
|
@@ -31155,7 +31223,7 @@ class GodotServer {
|
|
|
31155
31223
|
const log = new GameLog;
|
|
31156
31224
|
let exitCode = 0;
|
|
31157
31225
|
try {
|
|
31158
|
-
const { stdout, stderr } = await
|
|
31226
|
+
const { stdout, stderr } = await run5(engine.value, exportArgs, { timeout: 300000 });
|
|
31159
31227
|
log.append("stdout", stdout);
|
|
31160
31228
|
log.append("stderr", stderr);
|
|
31161
31229
|
} catch (error) {
|
|
@@ -31352,7 +31420,7 @@ class GodotServer {
|
|
|
31352
31420
|
}
|
|
31353
31421
|
async godotVersion(godotPath) {
|
|
31354
31422
|
try {
|
|
31355
|
-
const { stdout } = await
|
|
31423
|
+
const { stdout } = await run5(godotPath, ["--version"], { timeout: 1e4 });
|
|
31356
31424
|
this.lastGodotVersion = stdout.trim();
|
|
31357
31425
|
return this.lastGodotVersion;
|
|
31358
31426
|
} catch (error) {
|
|
@@ -31611,7 +31679,7 @@ class GodotServer {
|
|
|
31611
31679
|
const located = await this.locator.find();
|
|
31612
31680
|
const announced = runtimesAnnounced();
|
|
31613
31681
|
const pinged = Promise.all(announced.running.map(async (endpoint) => {
|
|
31614
|
-
const held = this.knownHoldOn(endpoint);
|
|
31682
|
+
const held = this.knownHoldOn(endpoint, announced.running);
|
|
31615
31683
|
const reply = held === null ? await runtimeRequest(endpoint, "ping", {}, HOLD_PING_MS) : {
|
|
31616
31684
|
ok: false,
|
|
31617
31685
|
message: `The game is held by the editor's debugger, ${describeHalt(held)}, and answers no runtime call while it is. debug_control continue lets it go.`
|
|
@@ -31620,6 +31688,7 @@ class GodotServer {
|
|
|
31620
31688
|
pid: endpoint.pid,
|
|
31621
31689
|
port: endpoint.port,
|
|
31622
31690
|
project: endpoint.project,
|
|
31691
|
+
editorPid: endpoint.editorPid,
|
|
31623
31692
|
reachable: reply.ok,
|
|
31624
31693
|
problem: reply.ok ? null : reply.message
|
|
31625
31694
|
};
|
|
@@ -31915,6 +31984,7 @@ class GodotServer {
|
|
|
31915
31984
|
let editorSaysGoing = true;
|
|
31916
31985
|
const endpoint = await announcedSince(projectPath, before, {
|
|
31917
31986
|
budgetMs,
|
|
31987
|
+
accept: (one) => this.isTheGameOf(this.currentRun(), one, projectPath),
|
|
31918
31988
|
giveUp: () => {
|
|
31919
31989
|
if (this.dapClient?.isStopped() === true) {
|
|
31920
31990
|
return true;
|
|
@@ -32220,18 +32290,50 @@ class GodotServer {
|
|
|
32220
32290
|
};
|
|
32221
32291
|
this.activeProcess = picked;
|
|
32222
32292
|
}
|
|
32223
|
-
announcedPidOf(run) {
|
|
32293
|
+
announcedPidOf(run, announced) {
|
|
32224
32294
|
if (!run.throughEditor || run.announcedPid !== undefined) {
|
|
32225
32295
|
return run.announcedPid;
|
|
32226
32296
|
}
|
|
32227
32297
|
const before = run.announcedBefore ?? new Set;
|
|
32228
|
-
const fresh = this.allAnnouncedForOurProject(runtimesAnnounced().running).filter((one) => !before.has(one.pid));
|
|
32229
|
-
const
|
|
32298
|
+
const fresh = this.allAnnouncedForOurProject(announced ?? runtimesAnnounced().running).filter((one) => !before.has(one.pid));
|
|
32299
|
+
const candidates = fresh.filter((one) => this.playedByOurEditor(one, run.projectPath));
|
|
32300
|
+
const theOne = candidates.length === 1 ? candidates[0] : undefined;
|
|
32230
32301
|
if (theOne !== undefined) {
|
|
32231
32302
|
run.announcedPid = theOne.pid;
|
|
32232
32303
|
}
|
|
32233
32304
|
return run.announcedPid;
|
|
32234
32305
|
}
|
|
32306
|
+
ownGameAmong(running, projectPath) {
|
|
32307
|
+
const run = this.currentRun();
|
|
32308
|
+
if (run === null || !stillRunning(run)) {
|
|
32309
|
+
return;
|
|
32310
|
+
}
|
|
32311
|
+
if (projectPath !== undefined && run.projectPath !== null && !isSameDirectory(run.projectPath, projectPath)) {
|
|
32312
|
+
return;
|
|
32313
|
+
}
|
|
32314
|
+
const its = run.throughEditor ? this.announcedPidOf(run, running) : run.pid;
|
|
32315
|
+
return its !== null && its !== undefined && running.some((one) => one.pid === its) ? its : undefined;
|
|
32316
|
+
}
|
|
32317
|
+
playedByOurEditor(endpoint, projectPath) {
|
|
32318
|
+
const editor = this.godotBridge.getStatus().editorPid;
|
|
32319
|
+
if (editor === undefined) {
|
|
32320
|
+
return true;
|
|
32321
|
+
}
|
|
32322
|
+
if (endpoint.editorPid !== undefined) {
|
|
32323
|
+
return endpoint.editorPid === editor;
|
|
32324
|
+
}
|
|
32325
|
+
return !runtimeAddonAnnouncesEditors(projectPath ?? this.ownProject);
|
|
32326
|
+
}
|
|
32327
|
+
isTheGameOf(run, endpoint, projectPath) {
|
|
32328
|
+
if (run === null) {
|
|
32329
|
+
return false;
|
|
32330
|
+
}
|
|
32331
|
+
if (run.throughEditor) {
|
|
32332
|
+
return this.playedByOurEditor(endpoint, projectPath);
|
|
32333
|
+
}
|
|
32334
|
+
const editor = this.godotBridge.getStatus().editorPid;
|
|
32335
|
+
return endpoint.editorPid === undefined || endpoint.editorPid !== editor;
|
|
32336
|
+
}
|
|
32235
32337
|
adoptRecordedRun() {
|
|
32236
32338
|
const record = readRunRecord();
|
|
32237
32339
|
if (record === null || !this.couldBeOurs(record.projectPath)) {
|
|
@@ -32375,6 +32477,9 @@ class GodotServer {
|
|
|
32375
32477
|
if (announced !== undefined && !alive(announced)) {
|
|
32376
32478
|
return false;
|
|
32377
32479
|
}
|
|
32480
|
+
if (announced !== undefined && editorSays === undefined) {
|
|
32481
|
+
return stillRunning(run);
|
|
32482
|
+
}
|
|
32378
32483
|
return runIsUp(run, editorSays === undefined ? (await this.editorPlayingState())?.playing ?? null : editorSays);
|
|
32379
32484
|
}
|
|
32380
32485
|
async handleGetDebugOutput(args, waitedMs) {
|
|
@@ -32448,7 +32553,7 @@ class GodotServer {
|
|
|
32448
32553
|
entries: forAnswer(selected.entries)
|
|
32449
32554
|
});
|
|
32450
32555
|
}
|
|
32451
|
-
async handleStopProject() {
|
|
32556
|
+
async handleStopProject(args) {
|
|
32452
32557
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
32453
32558
|
const stopped = this.currentRun();
|
|
32454
32559
|
if (!stopped) {
|
|
@@ -32457,21 +32562,48 @@ class GodotServer {
|
|
|
32457
32562
|
this.drainEditorOutput(stopped);
|
|
32458
32563
|
this.drainTranscript(stopped);
|
|
32459
32564
|
const wasRunning = await this.runStillGoing(stopped);
|
|
32565
|
+
const endedPid = stopped.pid ?? this.announcedPidOf(stopped) ?? null;
|
|
32566
|
+
const children = readBoolean(args, "andChildren") === true && wasRunning && endedPid !== null ? await childrenOf(endedPid) : null;
|
|
32460
32567
|
this.logDebug("Stopping the running game");
|
|
32461
32568
|
await this.endActiveGame("editor_run stop");
|
|
32569
|
+
const ended = children === null ? null : this.endChildrenAmong(children, stopped.projectPath);
|
|
32462
32570
|
return this.jsonTextResponse({
|
|
32463
32571
|
stopped: true,
|
|
32464
32572
|
through: stopped.throughEditor ? "editor" : "gdharness",
|
|
32465
|
-
endedPid
|
|
32573
|
+
endedPid,
|
|
32574
|
+
...ended === null ? {} : {
|
|
32575
|
+
endedChildren: ended.ended,
|
|
32576
|
+
childrenLeft: ended.left.length === 0 ? undefined : ended.left,
|
|
32577
|
+
childrenUnknown: ended.unknown ? true : undefined
|
|
32578
|
+
},
|
|
32466
32579
|
exitedBeforeStop: stopped.exitCode !== null || !wasRunning,
|
|
32467
32580
|
exitCode: stopped.exitCode,
|
|
32468
32581
|
errors: stopped.log.count("error"),
|
|
32469
32582
|
warnings: stopped.log.count("warning"),
|
|
32470
32583
|
clean: stopped.log.count("error") === 0,
|
|
32471
|
-
note: !wasRunning ? "This run was over before the stop, so nothing was ended here: editor_output has what it printed and how it ended. Anything that game started for itself, with OS.create_process or otherwise, is a separate process and may still be running." : stopped.throughEditor ? "The editor was asked to stop the scene it is playing.
|
|
32584
|
+
note: !wasRunning ? "This run was over before the stop, so nothing was ended here: editor_output has what it printed and how it ended. Anything that game started for itself, with OS.create_process or otherwise, is a separate process and may still be running." : `${stopped.throughEditor ? "The editor was asked to stop the scene it is playing." : "The process named under endedPid was ended."} ${aboutTheChildren(ended, stopped.throughEditor)}`,
|
|
32472
32585
|
entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
32473
32586
|
});
|
|
32474
32587
|
}
|
|
32588
|
+
endChildrenAmong(children, projectPath) {
|
|
32589
|
+
if (children === undefined) {
|
|
32590
|
+
return { ended: [], left: [], unknown: true };
|
|
32591
|
+
}
|
|
32592
|
+
const games = new Set(runtimesAnnounced().running.filter((one) => projectPath === null || isSameDirectory(one.project.path, projectPath)).map((one) => one.pid));
|
|
32593
|
+
const ended = [];
|
|
32594
|
+
const left = [];
|
|
32595
|
+
for (const child of children) {
|
|
32596
|
+
if (!games.has(child)) {
|
|
32597
|
+
left.push(child);
|
|
32598
|
+
continue;
|
|
32599
|
+
}
|
|
32600
|
+
try {
|
|
32601
|
+
process.kill(child);
|
|
32602
|
+
ended.push(child);
|
|
32603
|
+
} catch {}
|
|
32604
|
+
}
|
|
32605
|
+
return { ended, left, unknown: false };
|
|
32606
|
+
}
|
|
32475
32607
|
async handleRescanFilesystem(args) {
|
|
32476
32608
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 30000;
|
|
32477
32609
|
const started = Date.now();
|
|
@@ -32619,13 +32751,13 @@ class GodotServer {
|
|
|
32619
32751
|
const override = Number.parseInt(envValue("GDHARNESS_RUNTIME_TIMEOUT_MS") ?? "", 10);
|
|
32620
32752
|
return Number.isInteger(override) && override > 0 ? override : 1e4;
|
|
32621
32753
|
}
|
|
32622
|
-
knownHoldOn(endpoint) {
|
|
32754
|
+
knownHoldOn(endpoint, announced) {
|
|
32623
32755
|
const run = this.activeProcess;
|
|
32624
32756
|
const session = this.dapClient;
|
|
32625
32757
|
if (run === null || !run.throughEditor || !stillRunning(run) || session === null) {
|
|
32626
32758
|
return null;
|
|
32627
32759
|
}
|
|
32628
|
-
if (this.announcedPidOf(run) !== endpoint.pid || !session.holdIsKnown()) {
|
|
32760
|
+
if (this.announcedPidOf(run, announced) !== endpoint.pid || !session.holdIsKnown()) {
|
|
32629
32761
|
return null;
|
|
32630
32762
|
}
|
|
32631
32763
|
return session.whereItStopped();
|
|
@@ -32666,11 +32798,16 @@ class GodotServer {
|
|
|
32666
32798
|
async handleRuntimeCommand(command, args, timeoutMs = this.runtimeTimeoutMs()) {
|
|
32667
32799
|
const { op: _op, projectPath, pid, ...params } = asParams(args);
|
|
32668
32800
|
const announced = runtimesAnnounced();
|
|
32669
|
-
const
|
|
32801
|
+
const wanted = typeof projectPath === "string" ? projectPath : this.ownProject ?? undefined;
|
|
32802
|
+
const own = typeof pid === "number" ? undefined : this.ownGameAmong(announced.running, wanted);
|
|
32803
|
+
const choice = chooseRuntime(announced.running, wanted, announced.unspoken, typeof pid === "number" ? pid : own);
|
|
32670
32804
|
if ("problem" in choice) {
|
|
32671
32805
|
return this.createErrorResponse(choice.problem);
|
|
32672
32806
|
}
|
|
32673
|
-
const
|
|
32807
|
+
const ofProject = announced.running.filter((one) => wanted === undefined || isSameDirectory(one.project.path, wanted));
|
|
32808
|
+
const chosenAmongSeveral = own !== undefined && ofProject.length > 1;
|
|
32809
|
+
const answeredBy = chosenAmongSeveral ? { answeredBy: own } : {};
|
|
32810
|
+
const held = this.knownHoldOn(choice.endpoint, announced.running);
|
|
32674
32811
|
if (held !== null) {
|
|
32675
32812
|
return this.createErrorResponse(`The game (pid ${choice.endpoint.pid}) is held by the editor's debugger, ${describeHalt(held)}, and answers no runtime call while it is.`, [
|
|
32676
32813
|
"debug_control continue lets it go, after which this call answers",
|
|
@@ -32688,7 +32825,7 @@ class GodotServer {
|
|
|
32688
32825
|
}
|
|
32689
32826
|
const { id: _id, ...payload } = reply.payload;
|
|
32690
32827
|
if (!expectsScreenshot) {
|
|
32691
|
-
return this.jsonTextResponse(payload);
|
|
32828
|
+
return this.jsonTextResponse({ ...payload, ...answeredBy });
|
|
32692
32829
|
}
|
|
32693
32830
|
const returnedPath = readString(payload, "path");
|
|
32694
32831
|
if (readString(payload, "type") !== "screenshot_file" || !returnedPath || !screenshotPath) {
|
|
@@ -32700,7 +32837,10 @@ class GodotServer {
|
|
|
32700
32837
|
const dimensions = `${readNumber(payload, "width") ?? 0}x${readNumber(payload, "height") ?? 0} ${readString(payload, "format") ?? "unknown"}`;
|
|
32701
32838
|
return {
|
|
32702
32839
|
content: [
|
|
32703
|
-
{
|
|
32840
|
+
{
|
|
32841
|
+
type: "text",
|
|
32842
|
+
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}`
|
|
32843
|
+
},
|
|
32704
32844
|
{ type: "image", data: readFileSync13(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
32705
32845
|
]
|
|
32706
32846
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.42",
|
|
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",
|