gdharness 0.13.41 → 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 +124 -25
- package/build/index.js +114 -21
- package/package.json +1 -1
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: {
|
|
@@ -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
|
|
@@ -37216,7 +37253,7 @@ var init_run_record = __esm(() => {
|
|
|
37216
37253
|
});
|
|
37217
37254
|
|
|
37218
37255
|
// src/server.ts
|
|
37219
|
-
import { execFile as
|
|
37256
|
+
import { execFile as execFile7, spawn } from "node:child_process";
|
|
37220
37257
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
37221
37258
|
import {
|
|
37222
37259
|
appendFileSync,
|
|
@@ -37234,7 +37271,7 @@ import { tmpdir as tmpdir5 } from "node:os";
|
|
|
37234
37271
|
import { basename as basename3, dirname as dirname11, join as join19, normalize as normalize3, relative as relative3 } from "node:path";
|
|
37235
37272
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
37236
37273
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
37237
|
-
import { promisify as
|
|
37274
|
+
import { promisify as promisify7 } from "node:util";
|
|
37238
37275
|
function patienceForFrames(frames, atLeast) {
|
|
37239
37276
|
return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
|
|
37240
37277
|
}
|
|
@@ -37285,6 +37322,18 @@ function endedPreviousRun(ended) {
|
|
|
37285
37322
|
}
|
|
37286
37323
|
return ended.pid ?? true;
|
|
37287
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
|
+
}
|
|
37288
37337
|
function endedToStartThis(ended) {
|
|
37289
37338
|
if (ended === null) {
|
|
37290
37339
|
return "";
|
|
@@ -37984,7 +38033,7 @@ class GodotServer {
|
|
|
37984
38033
|
return op === "restart" ? await this.handleRestartEditor() : await this.handleLaunchEditor(args);
|
|
37985
38034
|
case "editor_run":
|
|
37986
38035
|
if (op === "stop") {
|
|
37987
|
-
return await this.handleStopProject();
|
|
38036
|
+
return await this.handleStopProject(args);
|
|
37988
38037
|
}
|
|
37989
38038
|
if (op === "wait") {
|
|
37990
38039
|
return await this.handleWaitForRun(args);
|
|
@@ -38427,7 +38476,7 @@ class GodotServer {
|
|
|
38427
38476
|
const log = new GameLog;
|
|
38428
38477
|
let exitCode = 0;
|
|
38429
38478
|
try {
|
|
38430
|
-
const { stdout, stderr } = await
|
|
38479
|
+
const { stdout, stderr } = await run7(engine.value, exportArgs, { timeout: 300000 });
|
|
38431
38480
|
log.append("stdout", stdout);
|
|
38432
38481
|
log.append("stderr", stderr);
|
|
38433
38482
|
} catch (error) {
|
|
@@ -38624,7 +38673,7 @@ class GodotServer {
|
|
|
38624
38673
|
}
|
|
38625
38674
|
async godotVersion(godotPath) {
|
|
38626
38675
|
try {
|
|
38627
|
-
const { stdout } = await
|
|
38676
|
+
const { stdout } = await run7(godotPath, ["--version"], { timeout: 1e4 });
|
|
38628
38677
|
this.lastGodotVersion = stdout.trim();
|
|
38629
38678
|
return this.lastGodotVersion;
|
|
38630
38679
|
} catch (error) {
|
|
@@ -39507,6 +39556,17 @@ class GodotServer {
|
|
|
39507
39556
|
}
|
|
39508
39557
|
return run.announcedPid;
|
|
39509
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
|
+
}
|
|
39510
39570
|
playedByOurEditor(endpoint, projectPath) {
|
|
39511
39571
|
const editor = this.godotBridge.getStatus().editorPid;
|
|
39512
39572
|
if (editor === undefined) {
|
|
@@ -39746,7 +39806,7 @@ class GodotServer {
|
|
|
39746
39806
|
entries: forAnswer(selected.entries)
|
|
39747
39807
|
});
|
|
39748
39808
|
}
|
|
39749
|
-
async handleStopProject() {
|
|
39809
|
+
async handleStopProject(args) {
|
|
39750
39810
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
39751
39811
|
const stopped = this.currentRun();
|
|
39752
39812
|
if (!stopped) {
|
|
@@ -39755,21 +39815,48 @@ class GodotServer {
|
|
|
39755
39815
|
this.drainEditorOutput(stopped);
|
|
39756
39816
|
this.drainTranscript(stopped);
|
|
39757
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;
|
|
39758
39820
|
this.logDebug("Stopping the running game");
|
|
39759
39821
|
await this.endActiveGame("editor_run stop");
|
|
39822
|
+
const ended = children === null ? null : this.endChildrenAmong(children, stopped.projectPath);
|
|
39760
39823
|
return this.jsonTextResponse({
|
|
39761
39824
|
stopped: true,
|
|
39762
39825
|
through: stopped.throughEditor ? "editor" : "gdharness",
|
|
39763
|
-
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
|
+
},
|
|
39764
39832
|
exitedBeforeStop: stopped.exitCode !== null || !wasRunning,
|
|
39765
39833
|
exitCode: stopped.exitCode,
|
|
39766
39834
|
errors: stopped.log.count("error"),
|
|
39767
39835
|
warnings: stopped.log.count("warning"),
|
|
39768
39836
|
clean: stopped.log.count("error") === 0,
|
|
39769
|
-
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)}`,
|
|
39770
39838
|
entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
39771
39839
|
});
|
|
39772
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
|
+
}
|
|
39773
39860
|
async handleRescanFilesystem(args) {
|
|
39774
39861
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 30000;
|
|
39775
39862
|
const started = Date.now();
|
|
@@ -39964,10 +40051,15 @@ class GodotServer {
|
|
|
39964
40051
|
async handleRuntimeCommand(command, args, timeoutMs = this.runtimeTimeoutMs()) {
|
|
39965
40052
|
const { op: _op, projectPath, pid, ...params } = asParams(args);
|
|
39966
40053
|
const announced = runtimesAnnounced();
|
|
39967
|
-
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);
|
|
39968
40057
|
if ("problem" in choice) {
|
|
39969
40058
|
return this.createErrorResponse(choice.problem);
|
|
39970
40059
|
}
|
|
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 } : {};
|
|
39971
40063
|
const held = this.knownHoldOn(choice.endpoint, announced.running);
|
|
39972
40064
|
if (held !== null) {
|
|
39973
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.`, [
|
|
@@ -39986,7 +40078,7 @@ class GodotServer {
|
|
|
39986
40078
|
}
|
|
39987
40079
|
const { id: _id, ...payload } = reply.payload;
|
|
39988
40080
|
if (!expectsScreenshot) {
|
|
39989
|
-
return this.jsonTextResponse(payload);
|
|
40081
|
+
return this.jsonTextResponse({ ...payload, ...answeredBy });
|
|
39990
40082
|
}
|
|
39991
40083
|
const returnedPath = readString(payload, "path");
|
|
39992
40084
|
if (readString(payload, "type") !== "screenshot_file" || !returnedPath || !screenshotPath) {
|
|
@@ -39998,7 +40090,10 @@ class GodotServer {
|
|
|
39998
40090
|
const dimensions = `${readNumber(payload, "width") ?? 0}x${readNumber(payload, "height") ?? 0} ${readString(payload, "format") ?? "unknown"}`;
|
|
39999
40091
|
return {
|
|
40000
40092
|
content: [
|
|
40001
|
-
{
|
|
40093
|
+
{
|
|
40094
|
+
type: "text",
|
|
40095
|
+
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}`
|
|
40096
|
+
},
|
|
40002
40097
|
{ type: "image", data: readFileSync17(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
40003
40098
|
]
|
|
40004
40099
|
};
|
|
@@ -40065,7 +40160,7 @@ async function runGodotServer() {
|
|
|
40065
40160
|
const server = new GodotServer;
|
|
40066
40161
|
await server.run();
|
|
40067
40162
|
}
|
|
40068
|
-
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/";
|
|
40069
40164
|
var init_server3 = __esm(() => {
|
|
40070
40165
|
init_mcp();
|
|
40071
40166
|
init_stdio2();
|
|
@@ -40086,6 +40181,7 @@ var init_server3 = __esm(() => {
|
|
|
40086
40181
|
init_lsp_client();
|
|
40087
40182
|
init_paths();
|
|
40088
40183
|
init_ports();
|
|
40184
|
+
init_process_children();
|
|
40089
40185
|
init_process_time();
|
|
40090
40186
|
init_project_scan();
|
|
40091
40187
|
init_resources();
|
|
@@ -40097,7 +40193,7 @@ var init_server3 = __esm(() => {
|
|
|
40097
40193
|
init_setup();
|
|
40098
40194
|
init_tool_definitions();
|
|
40099
40195
|
init_update_check();
|
|
40100
|
-
|
|
40196
|
+
run7 = promisify7(execFile7);
|
|
40101
40197
|
__dirname2 = dirname11(fileURLToPath4(import.meta.url));
|
|
40102
40198
|
VALUE_NAMES = {
|
|
40103
40199
|
string: "a string",
|
|
@@ -41608,8 +41704,10 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
41608
41704
|
|
|
41609
41705
|
- ${projectPathSentence()}. The \`runtime_*\` and \`debug_*\` ones pick between running
|
|
41610
41706
|
games instead, by \`projectPath\`, or by \`pid\` when several run from one project, as a bench
|
|
41611
|
-
and its workers do
|
|
41612
|
-
|
|
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.
|
|
41613
41711
|
- \`editor_status\` says whether an editor is connected and whether its addon matches the server.
|
|
41614
41712
|
\`addonIsStale\` means they differ, and \`staleNote\` says which half is behind: an editor that
|
|
41615
41713
|
needs restarting, or a server that needs reconnecting in your harness. Every answer that came
|
|
@@ -41654,6 +41752,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
41654
41752
|
| Where a 3D node is on screen | \`runtime_inspect\` \`rect\`, rather than unprojecting by hand |
|
|
41655
41753
|
| Wait for something in a running game | \`runtime_wait\`, never a sleep; \`until\` with \`says\` for a panel that rebuilds its own labels |
|
|
41656
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 |
|
|
41657
41756
|
| A picture, for a person who asked to see one | \`runtime_capture\` |
|
|
41658
41757
|
|
|
41659
41758
|
These need the runtime autoload, which \`gdharness setup\` registers. \`runtime_capture\` needs a
|
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) {
|
|
@@ -28848,7 +28878,7 @@ var RUNNING_PROJECT_PATH = {
|
|
|
28848
28878
|
};
|
|
28849
28879
|
var RUNNING_PID = {
|
|
28850
28880
|
type: "number",
|
|
28851
|
-
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."
|
|
28852
28882
|
};
|
|
28853
28883
|
var SCENE_PATH = {
|
|
28854
28884
|
type: "string",
|
|
@@ -29466,6 +29496,11 @@ var TOOL_SPECS = [
|
|
|
29466
29496
|
type: "number",
|
|
29467
29497
|
ops: ["check", "wait"],
|
|
29468
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."
|
|
29469
29504
|
}
|
|
29470
29505
|
},
|
|
29471
29506
|
requires: [],
|
|
@@ -29475,7 +29510,7 @@ var TOOL_SPECS = [
|
|
|
29475
29510
|
requires: ["projectPath"]
|
|
29476
29511
|
},
|
|
29477
29512
|
stop: {
|
|
29478
|
-
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",
|
|
29479
29514
|
requires: []
|
|
29480
29515
|
},
|
|
29481
29516
|
check: {
|
|
@@ -29937,7 +29972,7 @@ function buildToolDefinitions() {
|
|
|
29937
29972
|
// src/server.ts
|
|
29938
29973
|
var UPDATE_NOTICE_EVERY = 500;
|
|
29939
29974
|
var FEEDBACK_NOTICE_EVERY = 250;
|
|
29940
|
-
var
|
|
29975
|
+
var run5 = promisify5(execFile5);
|
|
29941
29976
|
var __dirname2 = dirname6(fileURLToPath2(import.meta.url));
|
|
29942
29977
|
var EDITOR_RESTART_TIMEOUT_MS = 90000;
|
|
29943
29978
|
var HOLD_PING_MS = 2000;
|
|
@@ -29992,6 +30027,18 @@ function endedPreviousRun(ended) {
|
|
|
29992
30027
|
}
|
|
29993
30028
|
return ended.pid ?? true;
|
|
29994
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
|
+
}
|
|
29995
30042
|
function endedToStartThis(ended) {
|
|
29996
30043
|
if (ended === null) {
|
|
29997
30044
|
return "";
|
|
@@ -30733,7 +30780,7 @@ class GodotServer {
|
|
|
30733
30780
|
return op === "restart" ? await this.handleRestartEditor() : await this.handleLaunchEditor(args);
|
|
30734
30781
|
case "editor_run":
|
|
30735
30782
|
if (op === "stop") {
|
|
30736
|
-
return await this.handleStopProject();
|
|
30783
|
+
return await this.handleStopProject(args);
|
|
30737
30784
|
}
|
|
30738
30785
|
if (op === "wait") {
|
|
30739
30786
|
return await this.handleWaitForRun(args);
|
|
@@ -31176,7 +31223,7 @@ class GodotServer {
|
|
|
31176
31223
|
const log = new GameLog;
|
|
31177
31224
|
let exitCode = 0;
|
|
31178
31225
|
try {
|
|
31179
|
-
const { stdout, stderr } = await
|
|
31226
|
+
const { stdout, stderr } = await run5(engine.value, exportArgs, { timeout: 300000 });
|
|
31180
31227
|
log.append("stdout", stdout);
|
|
31181
31228
|
log.append("stderr", stderr);
|
|
31182
31229
|
} catch (error) {
|
|
@@ -31373,7 +31420,7 @@ class GodotServer {
|
|
|
31373
31420
|
}
|
|
31374
31421
|
async godotVersion(godotPath) {
|
|
31375
31422
|
try {
|
|
31376
|
-
const { stdout } = await
|
|
31423
|
+
const { stdout } = await run5(godotPath, ["--version"], { timeout: 1e4 });
|
|
31377
31424
|
this.lastGodotVersion = stdout.trim();
|
|
31378
31425
|
return this.lastGodotVersion;
|
|
31379
31426
|
} catch (error) {
|
|
@@ -32256,6 +32303,17 @@ class GodotServer {
|
|
|
32256
32303
|
}
|
|
32257
32304
|
return run.announcedPid;
|
|
32258
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
|
+
}
|
|
32259
32317
|
playedByOurEditor(endpoint, projectPath) {
|
|
32260
32318
|
const editor = this.godotBridge.getStatus().editorPid;
|
|
32261
32319
|
if (editor === undefined) {
|
|
@@ -32495,7 +32553,7 @@ class GodotServer {
|
|
|
32495
32553
|
entries: forAnswer(selected.entries)
|
|
32496
32554
|
});
|
|
32497
32555
|
}
|
|
32498
|
-
async handleStopProject() {
|
|
32556
|
+
async handleStopProject(args) {
|
|
32499
32557
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
32500
32558
|
const stopped = this.currentRun();
|
|
32501
32559
|
if (!stopped) {
|
|
@@ -32504,21 +32562,48 @@ class GodotServer {
|
|
|
32504
32562
|
this.drainEditorOutput(stopped);
|
|
32505
32563
|
this.drainTranscript(stopped);
|
|
32506
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;
|
|
32507
32567
|
this.logDebug("Stopping the running game");
|
|
32508
32568
|
await this.endActiveGame("editor_run stop");
|
|
32569
|
+
const ended = children === null ? null : this.endChildrenAmong(children, stopped.projectPath);
|
|
32509
32570
|
return this.jsonTextResponse({
|
|
32510
32571
|
stopped: true,
|
|
32511
32572
|
through: stopped.throughEditor ? "editor" : "gdharness",
|
|
32512
|
-
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
|
+
},
|
|
32513
32579
|
exitedBeforeStop: stopped.exitCode !== null || !wasRunning,
|
|
32514
32580
|
exitCode: stopped.exitCode,
|
|
32515
32581
|
errors: stopped.log.count("error"),
|
|
32516
32582
|
warnings: stopped.log.count("warning"),
|
|
32517
32583
|
clean: stopped.log.count("error") === 0,
|
|
32518
|
-
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)}`,
|
|
32519
32585
|
entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
32520
32586
|
});
|
|
32521
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
|
+
}
|
|
32522
32607
|
async handleRescanFilesystem(args) {
|
|
32523
32608
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 30000;
|
|
32524
32609
|
const started = Date.now();
|
|
@@ -32713,10 +32798,15 @@ class GodotServer {
|
|
|
32713
32798
|
async handleRuntimeCommand(command, args, timeoutMs = this.runtimeTimeoutMs()) {
|
|
32714
32799
|
const { op: _op, projectPath, pid, ...params } = asParams(args);
|
|
32715
32800
|
const announced = runtimesAnnounced();
|
|
32716
|
-
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);
|
|
32717
32804
|
if ("problem" in choice) {
|
|
32718
32805
|
return this.createErrorResponse(choice.problem);
|
|
32719
32806
|
}
|
|
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 } : {};
|
|
32720
32810
|
const held = this.knownHoldOn(choice.endpoint, announced.running);
|
|
32721
32811
|
if (held !== null) {
|
|
32722
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.`, [
|
|
@@ -32735,7 +32825,7 @@ class GodotServer {
|
|
|
32735
32825
|
}
|
|
32736
32826
|
const { id: _id, ...payload } = reply.payload;
|
|
32737
32827
|
if (!expectsScreenshot) {
|
|
32738
|
-
return this.jsonTextResponse(payload);
|
|
32828
|
+
return this.jsonTextResponse({ ...payload, ...answeredBy });
|
|
32739
32829
|
}
|
|
32740
32830
|
const returnedPath = readString(payload, "path");
|
|
32741
32831
|
if (readString(payload, "type") !== "screenshot_file" || !returnedPath || !screenshotPath) {
|
|
@@ -32747,7 +32837,10 @@ class GodotServer {
|
|
|
32747
32837
|
const dimensions = `${readNumber(payload, "width") ?? 0}x${readNumber(payload, "height") ?? 0} ${readString(payload, "format") ?? "unknown"}`;
|
|
32748
32838
|
return {
|
|
32749
32839
|
content: [
|
|
32750
|
-
{
|
|
32840
|
+
{
|
|
32841
|
+
type: "text",
|
|
32842
|
+
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}`
|
|
32843
|
+
},
|
|
32751
32844
|
{ type: "image", data: readFileSync13(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
32752
32845
|
]
|
|
32753
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",
|