gdharness 1.1.30 → 1.1.31
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 +108 -19
- package/build/godot/addons/gdharness_editor/bridge_client.gd +6 -0
- package/build/index.js +107 -17
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -384,9 +384,18 @@ function readCommandLine(command) {
|
|
|
384
384
|
return {
|
|
385
385
|
executable,
|
|
386
386
|
projectPath,
|
|
387
|
-
editor: words.some((word) => word === "-e" || word === "--editor")
|
|
387
|
+
editor: words.some((word) => word === "-e" || word === "--editor"),
|
|
388
|
+
oneShot: words.find((word) => ONE_SHOT_OPTIONS.includes(word)) ?? null
|
|
388
389
|
};
|
|
389
390
|
}
|
|
391
|
+
async function oneShotOptionOf(pid) {
|
|
392
|
+
const tree = await processTree();
|
|
393
|
+
const listed = tree?.get(pid);
|
|
394
|
+
if (listed === undefined || listed.command === "") {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
return readCommandLine(listed.command).oneShot;
|
|
398
|
+
}
|
|
390
399
|
function commandWords(command) {
|
|
391
400
|
const words = [];
|
|
392
401
|
let word = "";
|
|
@@ -422,10 +431,31 @@ async function askPosix() {
|
|
|
422
431
|
});
|
|
423
432
|
return stdout;
|
|
424
433
|
}
|
|
425
|
-
var run, ASK_TIMEOUT_MS = 15000, lastListingFailure;
|
|
434
|
+
var run, ASK_TIMEOUT_MS = 15000, lastListingFailure, ONE_SHOT_OPTIONS;
|
|
426
435
|
var init_process_children = __esm(() => {
|
|
427
436
|
init_windows_ask();
|
|
428
437
|
run = promisify(execFile);
|
|
438
|
+
ONE_SHOT_OPTIONS = [
|
|
439
|
+
"--import",
|
|
440
|
+
"--quit",
|
|
441
|
+
"--quit-after",
|
|
442
|
+
"--export-release",
|
|
443
|
+
"--export-debug",
|
|
444
|
+
"--export-pack",
|
|
445
|
+
"--export-patch",
|
|
446
|
+
"--doctool",
|
|
447
|
+
"--gdscript-docs",
|
|
448
|
+
"--script",
|
|
449
|
+
"-s",
|
|
450
|
+
"--check-only",
|
|
451
|
+
"--build-solutions",
|
|
452
|
+
"--dump-gdextension-interface",
|
|
453
|
+
"--dump-extension-api",
|
|
454
|
+
"--dump-extension-api-with-docs",
|
|
455
|
+
"--validate-extension-api",
|
|
456
|
+
"--convert-3to4",
|
|
457
|
+
"--validate-conversion-3to4"
|
|
458
|
+
];
|
|
429
459
|
});
|
|
430
460
|
|
|
431
461
|
// src/bridge-announce.ts
|
|
@@ -17617,7 +17647,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17617
17647
|
},
|
|
17618
17648
|
{
|
|
17619
17649
|
name: "editor_status",
|
|
17620
|
-
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it; null when there is none. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. 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. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs.",
|
|
17650
|
+
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it; null when there is none. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. 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. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option.",
|
|
17621
17651
|
parameters: {},
|
|
17622
17652
|
requires: []
|
|
17623
17653
|
},
|
|
@@ -37477,7 +37507,7 @@ function getDefaultBridge(ownProject = null) {
|
|
|
37477
37507
|
defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost(), DEFAULT_TIMEOUT_MS, ownProject);
|
|
37478
37508
|
return defaultBridge;
|
|
37479
37509
|
}
|
|
37480
|
-
var DEFAULT_PORT = 6505, DEFAULT_HOST = "127.0.0.1", DEFAULT_TIMEOUT_MS = 30000, KEEPALIVE_INTERVAL_MS = 1e4, SECOND_CONNECTION_CLOSE_CODE = 4000, OTHER_PROJECT_CLOSE_CODE = 4001, CONNECT_WINDOW_MS = 35000, GodotBridge, defaultBridge = null;
|
|
37510
|
+
var DEFAULT_PORT = 6505, DEFAULT_HOST = "127.0.0.1", DEFAULT_TIMEOUT_MS = 30000, KEEPALIVE_INTERVAL_MS = 1e4, SECOND_CONNECTION_CLOSE_CODE = 4000, OTHER_PROJECT_CLOSE_CODE = 4001, ONE_SHOT_CLOSE_CODE = 4002, CONNECT_WINDOW_MS = 35000, GodotBridge, defaultBridge = null;
|
|
37481
37511
|
var init_godot_bridge = __esm(() => {
|
|
37482
37512
|
init_wrapper();
|
|
37483
37513
|
init_errors();
|
|
@@ -37490,6 +37520,9 @@ var init_godot_bridge = __esm(() => {
|
|
|
37490
37520
|
socket = null;
|
|
37491
37521
|
pingInterval = null;
|
|
37492
37522
|
connectionInfo = null;
|
|
37523
|
+
oneShotReader = null;
|
|
37524
|
+
oneShots = new Map;
|
|
37525
|
+
turnedAway = null;
|
|
37493
37526
|
pendingRequests = new Map;
|
|
37494
37527
|
resourceQueues = new Map;
|
|
37495
37528
|
wantedPort;
|
|
@@ -37504,6 +37537,9 @@ var init_godot_bridge = __esm(() => {
|
|
|
37504
37537
|
this.timeoutMs = timeoutMs;
|
|
37505
37538
|
this.ownProject = ownProject;
|
|
37506
37539
|
}
|
|
37540
|
+
identifyEditorsWith(reader) {
|
|
37541
|
+
this.oneShotReader = reader;
|
|
37542
|
+
}
|
|
37507
37543
|
get mayMove() {
|
|
37508
37544
|
return this.ownProject !== null;
|
|
37509
37545
|
}
|
|
@@ -37610,25 +37646,29 @@ var init_godot_bridge = __esm(() => {
|
|
|
37610
37646
|
this.log("info", "WebSocket bridge stopped");
|
|
37611
37647
|
}
|
|
37612
37648
|
isConnected() {
|
|
37613
|
-
return this.socket?.readyState === import_websocket.default.OPEN;
|
|
37649
|
+
return this.socket?.readyState === import_websocket.default.OPEN && this.connectionInfo?.identifying !== true;
|
|
37614
37650
|
}
|
|
37615
37651
|
getStatus() {
|
|
37652
|
+
const identifying = this.connectionInfo?.identifying === true;
|
|
37653
|
+
const editor = identifying ? null : this.connectionInfo;
|
|
37616
37654
|
return {
|
|
37617
37655
|
host: this.host,
|
|
37618
37656
|
port: this.port,
|
|
37619
37657
|
connected: this.isConnected(),
|
|
37620
|
-
projectPath:
|
|
37621
|
-
connectedAt:
|
|
37622
|
-
lastPongAt:
|
|
37623
|
-
addonVersion:
|
|
37624
|
-
addonDigest:
|
|
37625
|
-
editorPid:
|
|
37626
|
-
lspPort:
|
|
37627
|
-
dapPort:
|
|
37628
|
-
debugPort:
|
|
37629
|
-
openedByAServer:
|
|
37630
|
-
startedByAnEditor:
|
|
37631
|
-
syncsBreakpoints:
|
|
37658
|
+
projectPath: editor?.projectPath,
|
|
37659
|
+
connectedAt: editor?.connectedAt,
|
|
37660
|
+
lastPongAt: editor?.lastPongAt,
|
|
37661
|
+
addonVersion: editor?.addonVersion,
|
|
37662
|
+
addonDigest: editor?.addonDigest,
|
|
37663
|
+
editorPid: editor?.editorPid,
|
|
37664
|
+
lspPort: editor?.lspPort,
|
|
37665
|
+
dapPort: editor?.dapPort,
|
|
37666
|
+
debugPort: editor?.debugPort,
|
|
37667
|
+
openedByAServer: editor?.openedByAServer,
|
|
37668
|
+
startedByAnEditor: editor?.startedByAnEditor,
|
|
37669
|
+
syncsBreakpoints: editor?.syncsBreakpoints,
|
|
37670
|
+
turnedAway: this.turnedAway ?? undefined,
|
|
37671
|
+
identifying: identifying ? true : undefined,
|
|
37632
37672
|
pendingRequests: this.pendingRequests.size,
|
|
37633
37673
|
queuedResources: this.resourceQueues.size,
|
|
37634
37674
|
listeningSince: this.listeningSince ?? undefined,
|
|
@@ -37776,6 +37816,11 @@ var init_godot_bridge = __esm(() => {
|
|
|
37776
37816
|
this.connectionInfo.startedByAnEditor = typeof message.started_by_an_editor === "boolean" ? message.started_by_an_editor : undefined;
|
|
37777
37817
|
this.connectionInfo.syncsBreakpoints = typeof message.syncs_breakpoints === "boolean" ? message.syncs_breakpoints : undefined;
|
|
37778
37818
|
this.log("info", `Godot ready: ${message.project_path}`);
|
|
37819
|
+
const pid = this.connectionInfo.editorPid;
|
|
37820
|
+
if (!this.connectionInfo.openedByAServer && pid !== undefined && this.oneShotReader !== null) {
|
|
37821
|
+
this.identify(pid);
|
|
37822
|
+
return;
|
|
37823
|
+
}
|
|
37779
37824
|
this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
|
|
37780
37825
|
}
|
|
37781
37826
|
return;
|
|
@@ -37866,6 +37911,45 @@ var init_godot_bridge = __esm(() => {
|
|
|
37866
37911
|
this.stopKeepalive();
|
|
37867
37912
|
stranger?.close(OTHER_PROJECT_CLOSE_CODE, said);
|
|
37868
37913
|
}
|
|
37914
|
+
identify(pid) {
|
|
37915
|
+
const known = this.oneShots.get(pid);
|
|
37916
|
+
if (known !== undefined) {
|
|
37917
|
+
this.turnAway(pid, known);
|
|
37918
|
+
return;
|
|
37919
|
+
}
|
|
37920
|
+
const info = this.connectionInfo;
|
|
37921
|
+
const socket = this.socket;
|
|
37922
|
+
const reader = this.oneShotReader;
|
|
37923
|
+
if (info === null || reader === null) {
|
|
37924
|
+
return;
|
|
37925
|
+
}
|
|
37926
|
+
info.identifying = true;
|
|
37927
|
+
reader(pid).catch(() => {
|
|
37928
|
+
return;
|
|
37929
|
+
}).then((option) => {
|
|
37930
|
+
if (this.socket !== socket || this.connectionInfo !== info) {
|
|
37931
|
+
return;
|
|
37932
|
+
}
|
|
37933
|
+
if (typeof option === "string") {
|
|
37934
|
+
this.oneShots.set(pid, option);
|
|
37935
|
+
this.turnAway(pid, option);
|
|
37936
|
+
return;
|
|
37937
|
+
}
|
|
37938
|
+
info.identifying = false;
|
|
37939
|
+
this.log("info", `Godot editor identified: pid ${pid}`);
|
|
37940
|
+
this.emitBridgeEvent("godot_connected", { projectPath: info.projectPath });
|
|
37941
|
+
});
|
|
37942
|
+
}
|
|
37943
|
+
turnAway(pid, option) {
|
|
37944
|
+
const said = `pid ${pid} was started with ${option}, which does one job and quits, so it is not an editor`;
|
|
37945
|
+
this.log("info", `Turning away an engine run: ${said}`);
|
|
37946
|
+
this.turnedAway = { pid, option, at: new Date };
|
|
37947
|
+
const run = this.socket;
|
|
37948
|
+
this.socket = null;
|
|
37949
|
+
this.connectionInfo = null;
|
|
37950
|
+
this.stopKeepalive();
|
|
37951
|
+
run?.close(ONE_SHOT_CLOSE_CODE, said);
|
|
37952
|
+
}
|
|
37869
37953
|
handleDisconnect(disconnectedSocket, reason) {
|
|
37870
37954
|
if (disconnectedSocket && this.socket && disconnectedSocket !== this.socket) {
|
|
37871
37955
|
this.log("debug", "Ignoring stale Godot socket disconnect event");
|
|
@@ -40259,6 +40343,7 @@ class GodotServer {
|
|
|
40259
40343
|
constructor() {
|
|
40260
40344
|
this.ownProject = envValue("GDHARNESS_PROJECT") ?? null;
|
|
40261
40345
|
this.godotBridge = getDefaultBridge(this.ownProject);
|
|
40346
|
+
this.godotBridge.identifyEditorsWith(oneShotOptionOf);
|
|
40262
40347
|
this.godotBridge.on("godot_connected", () => {
|
|
40263
40348
|
this.settleTheLaunchReading();
|
|
40264
40349
|
});
|
|
@@ -42518,6 +42603,10 @@ class GodotServer {
|
|
|
42518
42603
|
const dap = portFromEnvOrNull("GDHARNESS_DAP_PORT") ?? await freePort(DEFAULT_DAP_PORT);
|
|
42519
42604
|
return { lsp, dap };
|
|
42520
42605
|
}
|
|
42606
|
+
editorOnTheBridge() {
|
|
42607
|
+
const pid = this.godotBridge.getStatus().editorPid;
|
|
42608
|
+
return pid === undefined ? "the editor on this bridge" : `the editor on this bridge (pid ${pid})`;
|
|
42609
|
+
}
|
|
42521
42610
|
async handleRunProject(args, op) {
|
|
42522
42611
|
const project = this.project(args);
|
|
42523
42612
|
if (!project.ok) {
|
|
@@ -42525,7 +42614,7 @@ class GodotServer {
|
|
|
42525
42614
|
}
|
|
42526
42615
|
const open = this.godotBridge.isConnected() ? this.godotBridge.getStatus().projectPath : undefined;
|
|
42527
42616
|
if (op !== "check" && open !== undefined && open !== "" && !isSameDirectory(open, project.value.path)) {
|
|
42528
|
-
return this.createErrorResponse(`This start names ${project.value.path}, and
|
|
42617
|
+
return this.createErrorResponse(`This start names ${project.value.path}, and ${this.editorOnTheBridge()} has ${open} open.`, [
|
|
42529
42618
|
"editor_status names the project this server's editor is showing",
|
|
42530
42619
|
"One server serves one editor: start a second server for the other project"
|
|
42531
42620
|
]);
|
|
@@ -44242,7 +44331,7 @@ class GodotServer {
|
|
|
44242
44331
|
const open = this.godotBridge.getStatus().projectPath;
|
|
44243
44332
|
const meant = readNonEmptyString(args, "projectPath");
|
|
44244
44333
|
if (open !== undefined && open !== "" && meant !== undefined && !isSameDirectory(open, meant)) {
|
|
44245
|
-
return this.createErrorResponse(`This call names ${meant}, and
|
|
44334
|
+
return this.createErrorResponse(`This call names ${meant}, and ${this.editorOnTheBridge()} has ${open} open.`, [
|
|
44246
44335
|
"editor_status names the project this server's editor is showing",
|
|
44247
44336
|
"One server serves one editor: start a second server for the other project"
|
|
44248
44337
|
]);
|
|
@@ -25,6 +25,10 @@ const MAX_RECONNECT_DELAY: float = 30.0
|
|
|
25
25
|
## What a server closes with when the editor that said hello belongs to another project. Kept in
|
|
26
26
|
## step with `OTHER_PROJECT_CLOSE_CODE` in src/godot-bridge.ts.
|
|
27
27
|
const ELSEWHERE_CLOSE_CODE: int = 4001
|
|
28
|
+
## What a server closes with when this process is one job that quits, such as a gate's `--import`,
|
|
29
|
+
## rather than an editor: dialling in again changes nothing for the rest of the run. Kept in step
|
|
30
|
+
## with `ONE_SHOT_CLOSE_CODE` in src/godot-bridge.ts.
|
|
31
|
+
const ONE_SHOT_CLOSE_CODE: int = 4002
|
|
28
32
|
|
|
29
33
|
## Where Godot keeps the three ports an editor serves. None of them is per editor: the settings
|
|
30
34
|
## file is one for every editor of this engine version on the machine, so two open at once want the
|
|
@@ -447,6 +451,8 @@ func _read_marker(path: String) -> String:
|
|
|
447
451
|
func _handle_disconnect() -> void:
|
|
448
452
|
_is_connected = false
|
|
449
453
|
_said_elsewhere()
|
|
454
|
+
if socket.get_close_code() == ONE_SHOT_CLOSE_CODE:
|
|
455
|
+
_should_reconnect = false
|
|
450
456
|
disconnected.emit()
|
|
451
457
|
|
|
452
458
|
if _should_reconnect:
|
package/build/index.js
CHANGED
|
@@ -25284,6 +25284,27 @@ function ancestorsIn(tree, pid, root) {
|
|
|
25284
25284
|
}
|
|
25285
25285
|
return chain;
|
|
25286
25286
|
}
|
|
25287
|
+
var ONE_SHOT_OPTIONS = [
|
|
25288
|
+
"--import",
|
|
25289
|
+
"--quit",
|
|
25290
|
+
"--quit-after",
|
|
25291
|
+
"--export-release",
|
|
25292
|
+
"--export-debug",
|
|
25293
|
+
"--export-pack",
|
|
25294
|
+
"--export-patch",
|
|
25295
|
+
"--doctool",
|
|
25296
|
+
"--gdscript-docs",
|
|
25297
|
+
"--script",
|
|
25298
|
+
"-s",
|
|
25299
|
+
"--check-only",
|
|
25300
|
+
"--build-solutions",
|
|
25301
|
+
"--dump-gdextension-interface",
|
|
25302
|
+
"--dump-extension-api",
|
|
25303
|
+
"--dump-extension-api-with-docs",
|
|
25304
|
+
"--validate-extension-api",
|
|
25305
|
+
"--convert-3to4",
|
|
25306
|
+
"--validate-conversion-3to4"
|
|
25307
|
+
];
|
|
25287
25308
|
function readCommandLine(command) {
|
|
25288
25309
|
const all = commandWords(command);
|
|
25289
25310
|
const separator = all.findIndex((word) => word === "--" || word === "++");
|
|
@@ -25304,9 +25325,18 @@ function readCommandLine(command) {
|
|
|
25304
25325
|
return {
|
|
25305
25326
|
executable,
|
|
25306
25327
|
projectPath,
|
|
25307
|
-
editor: words.some((word) => word === "-e" || word === "--editor")
|
|
25328
|
+
editor: words.some((word) => word === "-e" || word === "--editor"),
|
|
25329
|
+
oneShot: words.find((word) => ONE_SHOT_OPTIONS.includes(word)) ?? null
|
|
25308
25330
|
};
|
|
25309
25331
|
}
|
|
25332
|
+
async function oneShotOptionOf(pid) {
|
|
25333
|
+
const tree = await processTree();
|
|
25334
|
+
const listed = tree?.get(pid);
|
|
25335
|
+
if (listed === undefined || listed.command === "") {
|
|
25336
|
+
return;
|
|
25337
|
+
}
|
|
25338
|
+
return readCommandLine(listed.command).oneShot;
|
|
25339
|
+
}
|
|
25310
25340
|
function commandWords(command) {
|
|
25311
25341
|
const words = [];
|
|
25312
25342
|
let word = "";
|
|
@@ -27664,6 +27694,7 @@ var DEFAULT_TIMEOUT_MS = 30000;
|
|
|
27664
27694
|
var KEEPALIVE_INTERVAL_MS = 1e4;
|
|
27665
27695
|
var SECOND_CONNECTION_CLOSE_CODE = 4000;
|
|
27666
27696
|
var OTHER_PROJECT_CLOSE_CODE = 4001;
|
|
27697
|
+
var ONE_SHOT_CLOSE_CODE = 4002;
|
|
27667
27698
|
function resolveDefaultBridgePort() {
|
|
27668
27699
|
return portFromEnv("GDHARNESS_BRIDGE_PORT", DEFAULT_PORT);
|
|
27669
27700
|
}
|
|
@@ -27695,6 +27726,9 @@ class GodotBridge extends EventEmitter {
|
|
|
27695
27726
|
socket = null;
|
|
27696
27727
|
pingInterval = null;
|
|
27697
27728
|
connectionInfo = null;
|
|
27729
|
+
oneShotReader = null;
|
|
27730
|
+
oneShots = new Map;
|
|
27731
|
+
turnedAway = null;
|
|
27698
27732
|
pendingRequests = new Map;
|
|
27699
27733
|
resourceQueues = new Map;
|
|
27700
27734
|
wantedPort;
|
|
@@ -27709,6 +27743,9 @@ class GodotBridge extends EventEmitter {
|
|
|
27709
27743
|
this.timeoutMs = timeoutMs;
|
|
27710
27744
|
this.ownProject = ownProject;
|
|
27711
27745
|
}
|
|
27746
|
+
identifyEditorsWith(reader) {
|
|
27747
|
+
this.oneShotReader = reader;
|
|
27748
|
+
}
|
|
27712
27749
|
get mayMove() {
|
|
27713
27750
|
return this.ownProject !== null;
|
|
27714
27751
|
}
|
|
@@ -27815,25 +27852,29 @@ class GodotBridge extends EventEmitter {
|
|
|
27815
27852
|
this.log("info", "WebSocket bridge stopped");
|
|
27816
27853
|
}
|
|
27817
27854
|
isConnected() {
|
|
27818
|
-
return this.socket?.readyState === import_websocket.default.OPEN;
|
|
27855
|
+
return this.socket?.readyState === import_websocket.default.OPEN && this.connectionInfo?.identifying !== true;
|
|
27819
27856
|
}
|
|
27820
27857
|
getStatus() {
|
|
27858
|
+
const identifying = this.connectionInfo?.identifying === true;
|
|
27859
|
+
const editor = identifying ? null : this.connectionInfo;
|
|
27821
27860
|
return {
|
|
27822
27861
|
host: this.host,
|
|
27823
27862
|
port: this.port,
|
|
27824
27863
|
connected: this.isConnected(),
|
|
27825
|
-
projectPath:
|
|
27826
|
-
connectedAt:
|
|
27827
|
-
lastPongAt:
|
|
27828
|
-
addonVersion:
|
|
27829
|
-
addonDigest:
|
|
27830
|
-
editorPid:
|
|
27831
|
-
lspPort:
|
|
27832
|
-
dapPort:
|
|
27833
|
-
debugPort:
|
|
27834
|
-
openedByAServer:
|
|
27835
|
-
startedByAnEditor:
|
|
27836
|
-
syncsBreakpoints:
|
|
27864
|
+
projectPath: editor?.projectPath,
|
|
27865
|
+
connectedAt: editor?.connectedAt,
|
|
27866
|
+
lastPongAt: editor?.lastPongAt,
|
|
27867
|
+
addonVersion: editor?.addonVersion,
|
|
27868
|
+
addonDigest: editor?.addonDigest,
|
|
27869
|
+
editorPid: editor?.editorPid,
|
|
27870
|
+
lspPort: editor?.lspPort,
|
|
27871
|
+
dapPort: editor?.dapPort,
|
|
27872
|
+
debugPort: editor?.debugPort,
|
|
27873
|
+
openedByAServer: editor?.openedByAServer,
|
|
27874
|
+
startedByAnEditor: editor?.startedByAnEditor,
|
|
27875
|
+
syncsBreakpoints: editor?.syncsBreakpoints,
|
|
27876
|
+
turnedAway: this.turnedAway ?? undefined,
|
|
27877
|
+
identifying: identifying ? true : undefined,
|
|
27837
27878
|
pendingRequests: this.pendingRequests.size,
|
|
27838
27879
|
queuedResources: this.resourceQueues.size,
|
|
27839
27880
|
listeningSince: this.listeningSince ?? undefined,
|
|
@@ -27981,6 +28022,11 @@ class GodotBridge extends EventEmitter {
|
|
|
27981
28022
|
this.connectionInfo.startedByAnEditor = typeof message.started_by_an_editor === "boolean" ? message.started_by_an_editor : undefined;
|
|
27982
28023
|
this.connectionInfo.syncsBreakpoints = typeof message.syncs_breakpoints === "boolean" ? message.syncs_breakpoints : undefined;
|
|
27983
28024
|
this.log("info", `Godot ready: ${message.project_path}`);
|
|
28025
|
+
const pid = this.connectionInfo.editorPid;
|
|
28026
|
+
if (!this.connectionInfo.openedByAServer && pid !== undefined && this.oneShotReader !== null) {
|
|
28027
|
+
this.identify(pid);
|
|
28028
|
+
return;
|
|
28029
|
+
}
|
|
27984
28030
|
this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
|
|
27985
28031
|
}
|
|
27986
28032
|
return;
|
|
@@ -28071,6 +28117,45 @@ class GodotBridge extends EventEmitter {
|
|
|
28071
28117
|
this.stopKeepalive();
|
|
28072
28118
|
stranger?.close(OTHER_PROJECT_CLOSE_CODE, said);
|
|
28073
28119
|
}
|
|
28120
|
+
identify(pid) {
|
|
28121
|
+
const known = this.oneShots.get(pid);
|
|
28122
|
+
if (known !== undefined) {
|
|
28123
|
+
this.turnAway(pid, known);
|
|
28124
|
+
return;
|
|
28125
|
+
}
|
|
28126
|
+
const info = this.connectionInfo;
|
|
28127
|
+
const socket = this.socket;
|
|
28128
|
+
const reader = this.oneShotReader;
|
|
28129
|
+
if (info === null || reader === null) {
|
|
28130
|
+
return;
|
|
28131
|
+
}
|
|
28132
|
+
info.identifying = true;
|
|
28133
|
+
reader(pid).catch(() => {
|
|
28134
|
+
return;
|
|
28135
|
+
}).then((option) => {
|
|
28136
|
+
if (this.socket !== socket || this.connectionInfo !== info) {
|
|
28137
|
+
return;
|
|
28138
|
+
}
|
|
28139
|
+
if (typeof option === "string") {
|
|
28140
|
+
this.oneShots.set(pid, option);
|
|
28141
|
+
this.turnAway(pid, option);
|
|
28142
|
+
return;
|
|
28143
|
+
}
|
|
28144
|
+
info.identifying = false;
|
|
28145
|
+
this.log("info", `Godot editor identified: pid ${pid}`);
|
|
28146
|
+
this.emitBridgeEvent("godot_connected", { projectPath: info.projectPath });
|
|
28147
|
+
});
|
|
28148
|
+
}
|
|
28149
|
+
turnAway(pid, option) {
|
|
28150
|
+
const said = `pid ${pid} was started with ${option}, which does one job and quits, so it is not an editor`;
|
|
28151
|
+
this.log("info", `Turning away an engine run: ${said}`);
|
|
28152
|
+
this.turnedAway = { pid, option, at: new Date };
|
|
28153
|
+
const run = this.socket;
|
|
28154
|
+
this.socket = null;
|
|
28155
|
+
this.connectionInfo = null;
|
|
28156
|
+
this.stopKeepalive();
|
|
28157
|
+
run?.close(ONE_SHOT_CLOSE_CODE, said);
|
|
28158
|
+
}
|
|
28074
28159
|
handleDisconnect(disconnectedSocket, reason) {
|
|
28075
28160
|
if (disconnectedSocket && this.socket && disconnectedSocket !== this.socket) {
|
|
28076
28161
|
this.log("debug", "Ignoring stale Godot socket disconnect event");
|
|
@@ -31601,7 +31686,7 @@ var TOOL_SPECS = [
|
|
|
31601
31686
|
},
|
|
31602
31687
|
{
|
|
31603
31688
|
name: "editor_status",
|
|
31604
|
-
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it; null when there is none. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. 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. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs.",
|
|
31689
|
+
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it; null when there is none. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. 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. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option.",
|
|
31605
31690
|
parameters: {},
|
|
31606
31691
|
requires: []
|
|
31607
31692
|
},
|
|
@@ -32839,6 +32924,7 @@ class GodotServer {
|
|
|
32839
32924
|
constructor() {
|
|
32840
32925
|
this.ownProject = envValue("GDHARNESS_PROJECT") ?? null;
|
|
32841
32926
|
this.godotBridge = getDefaultBridge(this.ownProject);
|
|
32927
|
+
this.godotBridge.identifyEditorsWith(oneShotOptionOf);
|
|
32842
32928
|
this.godotBridge.on("godot_connected", () => {
|
|
32843
32929
|
this.settleTheLaunchReading();
|
|
32844
32930
|
});
|
|
@@ -35098,6 +35184,10 @@ class GodotServer {
|
|
|
35098
35184
|
const dap = portFromEnvOrNull("GDHARNESS_DAP_PORT") ?? await freePort(DEFAULT_DAP_PORT);
|
|
35099
35185
|
return { lsp, dap };
|
|
35100
35186
|
}
|
|
35187
|
+
editorOnTheBridge() {
|
|
35188
|
+
const pid = this.godotBridge.getStatus().editorPid;
|
|
35189
|
+
return pid === undefined ? "the editor on this bridge" : `the editor on this bridge (pid ${pid})`;
|
|
35190
|
+
}
|
|
35101
35191
|
async handleRunProject(args, op) {
|
|
35102
35192
|
const project = this.project(args);
|
|
35103
35193
|
if (!project.ok) {
|
|
@@ -35105,7 +35195,7 @@ class GodotServer {
|
|
|
35105
35195
|
}
|
|
35106
35196
|
const open = this.godotBridge.isConnected() ? this.godotBridge.getStatus().projectPath : undefined;
|
|
35107
35197
|
if (op !== "check" && open !== undefined && open !== "" && !isSameDirectory(open, project.value.path)) {
|
|
35108
|
-
return this.createErrorResponse(`This start names ${project.value.path}, and
|
|
35198
|
+
return this.createErrorResponse(`This start names ${project.value.path}, and ${this.editorOnTheBridge()} has ${open} open.`, [
|
|
35109
35199
|
"editor_status names the project this server's editor is showing",
|
|
35110
35200
|
"One server serves one editor: start a second server for the other project"
|
|
35111
35201
|
]);
|
|
@@ -36822,7 +36912,7 @@ class GodotServer {
|
|
|
36822
36912
|
const open = this.godotBridge.getStatus().projectPath;
|
|
36823
36913
|
const meant = readNonEmptyString(args, "projectPath");
|
|
36824
36914
|
if (open !== undefined && open !== "" && meant !== undefined && !isSameDirectory(open, meant)) {
|
|
36825
|
-
return this.createErrorResponse(`This call names ${meant}, and
|
|
36915
|
+
return this.createErrorResponse(`This call names ${meant}, and ${this.editorOnTheBridge()} has ${open} open.`, [
|
|
36826
36916
|
"editor_status names the project this server's editor is showing",
|
|
36827
36917
|
"One server serves one editor: start a second server for the other project"
|
|
36828
36918
|
]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.31",
|
|
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",
|