gdharness 0.9.1 → 0.9.3
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 +182 -39
- package/build/godot/addons/gdharness_editor/bridge_client.gd +63 -27
- package/build/godot/addons/gdharness_editor/tool_executor.gd +1 -0
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +27 -0
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +6 -3
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +8 -6
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +52 -33
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +31 -0
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +8 -6
- package/build/index.js +216 -89
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -173,6 +173,7 @@ function userDataIn(home, variables = process.env) {
|
|
|
173
173
|
}
|
|
174
174
|
return carried;
|
|
175
175
|
}
|
|
176
|
+
var OPENED_BY_A_SERVER = "GDHARNESS_OPENED_BY_A_SERVER";
|
|
176
177
|
|
|
177
178
|
// node_modules/yaml/dist/nodes/identity.js
|
|
178
179
|
var require_identity = __commonJS(function(exports) {
|
|
@@ -14711,6 +14712,9 @@ var init_types = __esm(() => {
|
|
|
14711
14712
|
|
|
14712
14713
|
// src/paths.ts
|
|
14713
14714
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
14715
|
+
function isSameDirectory(onePath, otherPath) {
|
|
14716
|
+
return relative(resolve(onePath), resolve(otherPath)) === "";
|
|
14717
|
+
}
|
|
14714
14718
|
function isWithinRoot(rootPath, candidatePath) {
|
|
14715
14719
|
const step = relative(resolve(rootPath), resolve(candidatePath));
|
|
14716
14720
|
return step !== ".." && !step.startsWith(`..${sep}`) && !isAbsolute(step);
|
|
@@ -15845,7 +15849,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
15845
15849
|
blank: true,
|
|
15846
15850
|
description: `set: the value, fitted to the property's type. "" writes an empty string.`
|
|
15847
15851
|
},
|
|
15848
|
-
method: {
|
|
15852
|
+
method: {
|
|
15853
|
+
type: "string",
|
|
15854
|
+
ops: ["call"],
|
|
15855
|
+
description: 'call: which one to call. Colons call through the objects a node holds, "_game:run:advance", the same way a property is written through them.'
|
|
15856
|
+
},
|
|
15849
15857
|
args: {
|
|
15850
15858
|
type: "array",
|
|
15851
15859
|
ops: ["call"],
|
|
@@ -15858,7 +15866,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
15858
15866
|
summary: "set a property, on a node or on an object it holds",
|
|
15859
15867
|
requires: ["property", "value"]
|
|
15860
15868
|
},
|
|
15861
|
-
call: {
|
|
15869
|
+
call: {
|
|
15870
|
+
summary: "call a method, on a node or on an object it holds, and return its result",
|
|
15871
|
+
requires: ["method"]
|
|
15872
|
+
}
|
|
15862
15873
|
}
|
|
15863
15874
|
},
|
|
15864
15875
|
{
|
|
@@ -30772,11 +30783,29 @@ var init_stdio2 = __esm(() => {
|
|
|
30772
30783
|
});
|
|
30773
30784
|
|
|
30774
30785
|
// src/bridge-announce.ts
|
|
30775
|
-
import { mkdirSync as mkdirSync5, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
|
|
30786
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync9, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
|
|
30776
30787
|
import { dirname as dirname6, join as join11 } from "node:path";
|
|
30777
30788
|
function announcementPath(projectPath) {
|
|
30778
30789
|
return join11(projectPath, ".godot", "gdharness-bridge.json");
|
|
30779
30790
|
}
|
|
30791
|
+
function readAnnouncement(path) {
|
|
30792
|
+
try {
|
|
30793
|
+
const parsed = JSON.parse(readFileSync9(path, "utf8"));
|
|
30794
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
30795
|
+
return null;
|
|
30796
|
+
}
|
|
30797
|
+
const said = parsed;
|
|
30798
|
+
if (typeof said.port !== "number" || typeof said.pid !== "number") {
|
|
30799
|
+
return null;
|
|
30800
|
+
}
|
|
30801
|
+
return said;
|
|
30802
|
+
} catch {
|
|
30803
|
+
return null;
|
|
30804
|
+
}
|
|
30805
|
+
}
|
|
30806
|
+
function announcementIsOurs(path) {
|
|
30807
|
+
return readAnnouncement(path)?.pid === process.pid;
|
|
30808
|
+
}
|
|
30780
30809
|
function announceBridge(projectPath, bridge) {
|
|
30781
30810
|
const path = announcementPath(projectPath);
|
|
30782
30811
|
try {
|
|
@@ -30798,7 +30827,7 @@ function announceBridge(projectPath, bridge) {
|
|
|
30798
30827
|
}
|
|
30799
30828
|
}
|
|
30800
30829
|
function withdrawBridge(path) {
|
|
30801
|
-
if (path === null) {
|
|
30830
|
+
if (path === null || !announcementIsOurs(path)) {
|
|
30802
30831
|
return;
|
|
30803
30832
|
}
|
|
30804
30833
|
try {
|
|
@@ -34324,14 +34353,15 @@ function resolveDefaultBridgeHost() {
|
|
|
34324
34353
|
function servedPort(said) {
|
|
34325
34354
|
return typeof said === "number" && Number.isInteger(said) && said > 0 && said <= 65535 ? said : undefined;
|
|
34326
34355
|
}
|
|
34327
|
-
function getDefaultBridge(
|
|
34328
|
-
defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost(), DEFAULT_TIMEOUT_MS,
|
|
34356
|
+
function getDefaultBridge(ownProject = null) {
|
|
34357
|
+
defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost(), DEFAULT_TIMEOUT_MS, ownProject);
|
|
34329
34358
|
return defaultBridge;
|
|
34330
34359
|
}
|
|
34331
|
-
var DEFAULT_PORT = 6505, DEFAULT_HOST = "127.0.0.1", DEFAULT_TIMEOUT_MS = 30000, KEEPALIVE_INTERVAL_MS = 1e4, SECOND_CONNECTION_CLOSE_CODE = 4000, GodotBridge, defaultBridge = null;
|
|
34360
|
+
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, GodotBridge, defaultBridge = null;
|
|
34332
34361
|
var init_godot_bridge = __esm(() => {
|
|
34333
34362
|
init_wrapper();
|
|
34334
34363
|
init_errors();
|
|
34364
|
+
init_paths();
|
|
34335
34365
|
init_ports();
|
|
34336
34366
|
GodotBridge = class GodotBridge extends EventEmitter {
|
|
34337
34367
|
httpServer = null;
|
|
@@ -34345,13 +34375,16 @@ var init_godot_bridge = __esm(() => {
|
|
|
34345
34375
|
boundPort = null;
|
|
34346
34376
|
host;
|
|
34347
34377
|
timeoutMs;
|
|
34348
|
-
|
|
34349
|
-
constructor(port = DEFAULT_PORT, host = DEFAULT_HOST, timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
34378
|
+
ownProject;
|
|
34379
|
+
constructor(port = DEFAULT_PORT, host = DEFAULT_HOST, timeoutMs = DEFAULT_TIMEOUT_MS, ownProject = null) {
|
|
34350
34380
|
super();
|
|
34351
34381
|
this.wantedPort = port;
|
|
34352
34382
|
this.host = host;
|
|
34353
34383
|
this.timeoutMs = timeoutMs;
|
|
34354
|
-
this.
|
|
34384
|
+
this.ownProject = ownProject;
|
|
34385
|
+
}
|
|
34386
|
+
get mayMove() {
|
|
34387
|
+
return this.ownProject !== null;
|
|
34355
34388
|
}
|
|
34356
34389
|
get port() {
|
|
34357
34390
|
return this.boundPort ?? this.wantedPort;
|
|
@@ -34467,6 +34500,7 @@ var init_godot_bridge = __esm(() => {
|
|
|
34467
34500
|
lspPort: this.connectionInfo?.lspPort,
|
|
34468
34501
|
dapPort: this.connectionInfo?.dapPort,
|
|
34469
34502
|
debugPort: this.connectionInfo?.debugPort,
|
|
34503
|
+
openedByAServer: this.connectionInfo?.openedByAServer,
|
|
34470
34504
|
pendingRequests: this.pendingRequests.size,
|
|
34471
34505
|
queuedResources: this.resourceQueues.size
|
|
34472
34506
|
};
|
|
@@ -34597,12 +34631,17 @@ var init_godot_bridge = __esm(() => {
|
|
|
34597
34631
|
}
|
|
34598
34632
|
case "godot_ready":
|
|
34599
34633
|
if (this.connectionInfo) {
|
|
34634
|
+
if (this.belongsElsewhere(message.project_path)) {
|
|
34635
|
+
this.sendElsewhere(message.project_path);
|
|
34636
|
+
return;
|
|
34637
|
+
}
|
|
34600
34638
|
this.connectionInfo.projectPath = message.project_path;
|
|
34601
34639
|
this.connectionInfo.addonVersion = message.addon_version ?? "";
|
|
34602
34640
|
this.connectionInfo.editorPid = message.editor_pid;
|
|
34603
34641
|
this.connectionInfo.lspPort = servedPort(message.lsp_port);
|
|
34604
34642
|
this.connectionInfo.dapPort = servedPort(message.dap_port);
|
|
34605
34643
|
this.connectionInfo.debugPort = servedPort(message.debug_port);
|
|
34644
|
+
this.connectionInfo.openedByAServer = message.opened_by_a_server === true;
|
|
34606
34645
|
this.log("info", `Godot ready: ${message.project_path}`);
|
|
34607
34646
|
this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
|
|
34608
34647
|
}
|
|
@@ -34679,6 +34718,21 @@ var init_godot_bridge = __esm(() => {
|
|
|
34679
34718
|
clearInterval(this.pingInterval);
|
|
34680
34719
|
this.pingInterval = null;
|
|
34681
34720
|
}
|
|
34721
|
+
belongsElsewhere(theirProject) {
|
|
34722
|
+
if (this.ownProject === null || theirProject.trim() === "") {
|
|
34723
|
+
return false;
|
|
34724
|
+
}
|
|
34725
|
+
return !isSameDirectory(this.ownProject, theirProject);
|
|
34726
|
+
}
|
|
34727
|
+
sendElsewhere(theirProject) {
|
|
34728
|
+
const said = `this server serves ${this.ownProject ?? ""}, and that editor has ${theirProject} open`;
|
|
34729
|
+
this.log("warn", `Turning away an editor from another project: ${said}`);
|
|
34730
|
+
const stranger = this.socket;
|
|
34731
|
+
this.socket = null;
|
|
34732
|
+
this.connectionInfo = null;
|
|
34733
|
+
this.stopKeepalive();
|
|
34734
|
+
stranger?.close(OTHER_PROJECT_CLOSE_CODE, said);
|
|
34735
|
+
}
|
|
34682
34736
|
handleDisconnect(disconnectedSocket, reason) {
|
|
34683
34737
|
if (disconnectedSocket && this.socket && disconnectedSocket !== this.socket) {
|
|
34684
34738
|
this.log("debug", "Ignoring stale Godot socket disconnect event");
|
|
@@ -35603,7 +35657,7 @@ var init_lsp_client = __esm(() => {
|
|
|
35603
35657
|
});
|
|
35604
35658
|
|
|
35605
35659
|
// src/project-scan.ts
|
|
35606
|
-
import { readdirSync as readdirSync4, readFileSync as
|
|
35660
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync10 } from "node:fs";
|
|
35607
35661
|
import { join as join12 } from "node:path";
|
|
35608
35662
|
function projectStructure(projectPath) {
|
|
35609
35663
|
const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
|
|
@@ -35666,7 +35720,7 @@ function searchProject(projectPath, options) {
|
|
|
35666
35720
|
}
|
|
35667
35721
|
result.summary.files_searched += 1;
|
|
35668
35722
|
const matches = [];
|
|
35669
|
-
for (const [index, line] of
|
|
35723
|
+
for (const [index, line] of readFileSync10(entryPath, "utf8").split(`
|
|
35670
35724
|
`).entries()) {
|
|
35671
35725
|
if (full()) {
|
|
35672
35726
|
break;
|
|
@@ -35694,7 +35748,7 @@ var init_project_scan = __esm(() => {
|
|
|
35694
35748
|
});
|
|
35695
35749
|
|
|
35696
35750
|
// src/runtime-client.ts
|
|
35697
|
-
import { existsSync as existsSync10, readdirSync as readdirSync5, readFileSync as
|
|
35751
|
+
import { existsSync as existsSync10, readdirSync as readdirSync5, readFileSync as readFileSync11, unlinkSync } from "node:fs";
|
|
35698
35752
|
import { createConnection as createConnection3 } from "node:net";
|
|
35699
35753
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
35700
35754
|
import { join as join13, resolve as resolve4 } from "node:path";
|
|
@@ -35725,7 +35779,7 @@ function processAlive(pid) {
|
|
|
35725
35779
|
function parseAnnouncement(file, pid) {
|
|
35726
35780
|
let fields;
|
|
35727
35781
|
try {
|
|
35728
|
-
fields = asParams(JSON.parse(
|
|
35782
|
+
fields = asParams(JSON.parse(readFileSync11(file, "utf8")));
|
|
35729
35783
|
} catch {
|
|
35730
35784
|
return { kind: "rubbish" };
|
|
35731
35785
|
}
|
|
@@ -35918,14 +35972,14 @@ var init_runtime_client = __esm(() => {
|
|
|
35918
35972
|
});
|
|
35919
35973
|
|
|
35920
35974
|
// src/setup.ts
|
|
35921
|
-
import { cpSync as cpSync2, existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as
|
|
35975
|
+
import { cpSync as cpSync2, existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync12, rmSync as rmSync7, writeFileSync as writeFileSync8 } from "node:fs";
|
|
35922
35976
|
import { dirname as dirname8, join as join14 } from "node:path";
|
|
35923
35977
|
function installedAddonVersion(projectPath) {
|
|
35924
35978
|
const marker = join14(projectPath, "addons", ADDONS2[0], VERSION_MARKER2);
|
|
35925
35979
|
if (!existsSync11(marker)) {
|
|
35926
35980
|
return null;
|
|
35927
35981
|
}
|
|
35928
|
-
const said =
|
|
35982
|
+
const said = readFileSync12(marker, "utf8").trim();
|
|
35929
35983
|
return said === "" ? null : said;
|
|
35930
35984
|
}
|
|
35931
35985
|
var ADDONS2, VERSION_MARKER2 = ".gdharness-version";
|
|
@@ -35939,7 +35993,7 @@ var init_setup = __esm(() => {
|
|
|
35939
35993
|
|
|
35940
35994
|
// src/server.ts
|
|
35941
35995
|
import { execFile as execFile5, spawn } from "node:child_process";
|
|
35942
|
-
import { existsSync as existsSync12, mkdtempSync as mkdtempSync3, readdirSync as readdirSync6, readFileSync as
|
|
35996
|
+
import { existsSync as existsSync12, mkdtempSync as mkdtempSync3, readdirSync as readdirSync6, readFileSync as readFileSync13, realpathSync as realpathSync2, rmSync as rmSync8 } from "node:fs";
|
|
35943
35997
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
35944
35998
|
import { basename as basename2, dirname as dirname9, join as join15, normalize as normalize3 } from "node:path";
|
|
35945
35999
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
@@ -35948,6 +36002,17 @@ import { promisify as promisify5 } from "node:util";
|
|
|
35948
36002
|
function patienceForFrames(frames, atLeast) {
|
|
35949
36003
|
return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
|
|
35950
36004
|
}
|
|
36005
|
+
function alive(pid) {
|
|
36006
|
+
if (pid === undefined) {
|
|
36007
|
+
return false;
|
|
36008
|
+
}
|
|
36009
|
+
try {
|
|
36010
|
+
process.kill(pid, 0);
|
|
36011
|
+
return true;
|
|
36012
|
+
} catch {
|
|
36013
|
+
return false;
|
|
36014
|
+
}
|
|
36015
|
+
}
|
|
35951
36016
|
function isOfType(value, kind) {
|
|
35952
36017
|
switch (kind) {
|
|
35953
36018
|
case "string":
|
|
@@ -36020,11 +36085,11 @@ function aboveTheRunner(entry) {
|
|
|
36020
36085
|
};
|
|
36021
36086
|
}
|
|
36022
36087
|
function hasMainScene(projectFile) {
|
|
36023
|
-
const scene = parseProjectGodot(
|
|
36088
|
+
const scene = parseProjectGodot(readFileSync13(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
36024
36089
|
return typeof scene === "string" && scene !== "";
|
|
36025
36090
|
}
|
|
36026
36091
|
function editorPlaysHeadless(projectFile) {
|
|
36027
|
-
const runArgs = parseProjectGodot(
|
|
36092
|
+
const runArgs = parseProjectGodot(readFileSync13(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
36028
36093
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
36029
36094
|
}
|
|
36030
36095
|
function camelCased(params) {
|
|
@@ -36052,13 +36117,14 @@ class GodotServer {
|
|
|
36052
36117
|
dapClient = null;
|
|
36053
36118
|
bridgeStartupError = null;
|
|
36054
36119
|
bridgeRetry = null;
|
|
36120
|
+
successorWatch = null;
|
|
36055
36121
|
lastProjectPath = null;
|
|
36056
36122
|
shutdownInitiated = false;
|
|
36057
36123
|
ownProject;
|
|
36058
36124
|
announcedAt = null;
|
|
36059
36125
|
constructor() {
|
|
36060
36126
|
this.ownProject = envValue("GDHARNESS_PROJECT") ?? null;
|
|
36061
|
-
this.godotBridge = getDefaultBridge(this.ownProject
|
|
36127
|
+
this.godotBridge = getDefaultBridge(this.ownProject);
|
|
36062
36128
|
this.mcp = new McpServer({ name: "gdharness", version: SERVER_VERSION }, { capabilities: { tools: {}, resources: {} } });
|
|
36063
36129
|
this.setupToolHandlers();
|
|
36064
36130
|
setupResourceHandlers(this.mcp, () => this.lastProjectPath);
|
|
@@ -36151,6 +36217,7 @@ class GodotServer {
|
|
|
36151
36217
|
port: status.port,
|
|
36152
36218
|
version: SERVER_VERSION
|
|
36153
36219
|
});
|
|
36220
|
+
this.watchForASuccessor();
|
|
36154
36221
|
}
|
|
36155
36222
|
stopTryingTheBridge() {
|
|
36156
36223
|
if (this.bridgeRetry !== null) {
|
|
@@ -36158,9 +36225,41 @@ class GodotServer {
|
|
|
36158
36225
|
this.bridgeRetry = null;
|
|
36159
36226
|
}
|
|
36160
36227
|
}
|
|
36228
|
+
watchForASuccessor() {
|
|
36229
|
+
if (this.ownProject === null || this.successorWatch !== null) {
|
|
36230
|
+
return;
|
|
36231
|
+
}
|
|
36232
|
+
const watch = setInterval(() => {
|
|
36233
|
+
const successor = this.supersededBy();
|
|
36234
|
+
if (successor === null) {
|
|
36235
|
+
return;
|
|
36236
|
+
}
|
|
36237
|
+
console.error(`[SERVER] pid ${successor} now serves ${this.ownProject}; this server is finished and is stopping.`);
|
|
36238
|
+
this.handleShutdown(`superseded by ${successor}`, 0);
|
|
36239
|
+
}, SUCCESSOR_CHECK_MS);
|
|
36240
|
+
watch.unref();
|
|
36241
|
+
this.successorWatch = watch;
|
|
36242
|
+
}
|
|
36243
|
+
supersededBy() {
|
|
36244
|
+
if (this.ownProject === null) {
|
|
36245
|
+
return null;
|
|
36246
|
+
}
|
|
36247
|
+
const announced = readAnnouncement(announcementPath(this.ownProject));
|
|
36248
|
+
if (announced === null || announced.pid === process.pid) {
|
|
36249
|
+
return null;
|
|
36250
|
+
}
|
|
36251
|
+
return alive(announced.pid) ? announced.pid : null;
|
|
36252
|
+
}
|
|
36253
|
+
stopWatchingForASuccessor() {
|
|
36254
|
+
if (this.successorWatch !== null) {
|
|
36255
|
+
clearInterval(this.successorWatch);
|
|
36256
|
+
this.successorWatch = null;
|
|
36257
|
+
}
|
|
36258
|
+
}
|
|
36161
36259
|
async cleanup() {
|
|
36162
36260
|
this.logDebug("Cleaning up resources");
|
|
36163
36261
|
this.stopTryingTheBridge();
|
|
36262
|
+
this.stopWatchingForASuccessor();
|
|
36164
36263
|
withdrawBridge(this.announcedAt);
|
|
36165
36264
|
this.announcedAt = null;
|
|
36166
36265
|
if (this.activeProcess) {
|
|
@@ -36742,7 +36841,7 @@ class GodotServer {
|
|
|
36742
36841
|
if (!engine.ok) {
|
|
36743
36842
|
return engine.response;
|
|
36744
36843
|
}
|
|
36745
|
-
const application = parseProjectGodot(
|
|
36844
|
+
const application = parseProjectGodot(readFileSync13(project.value.file, "utf8"))["application"] ?? {};
|
|
36746
36845
|
const name = application["config/name"];
|
|
36747
36846
|
const mainScene = application["run/main_scene"];
|
|
36748
36847
|
const info = {
|
|
@@ -36917,7 +37016,7 @@ class GodotServer {
|
|
|
36917
37016
|
const written = existsSync12(reportsDir) ? readdirSync6(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
36918
37017
|
const newest = written.at(-1);
|
|
36919
37018
|
if (newest !== undefined) {
|
|
36920
|
-
report = parseJUnit(
|
|
37019
|
+
report = parseJUnit(readFileSync13(join15(reportsDir, newest, "results.xml"), "utf8"));
|
|
36921
37020
|
}
|
|
36922
37021
|
} catch (error) {
|
|
36923
37022
|
reportProblem = errorMessage(error);
|
|
@@ -37116,7 +37215,8 @@ class GodotServer {
|
|
|
37116
37215
|
"editor_status says whether the bridge is up and what has reached it"
|
|
37117
37216
|
]);
|
|
37118
37217
|
}
|
|
37119
|
-
const
|
|
37218
|
+
const mine = before.openedByAServer === true && before.projectPath !== undefined;
|
|
37219
|
+
const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid) : await this.handleViaBridge("restart_editor", {});
|
|
37120
37220
|
if (asked.isError === true) {
|
|
37121
37221
|
return asked;
|
|
37122
37222
|
}
|
|
@@ -37144,6 +37244,26 @@ class GodotServer {
|
|
|
37144
37244
|
tookMs: Date.now() - began
|
|
37145
37245
|
});
|
|
37146
37246
|
}
|
|
37247
|
+
async startItAgain(projectPath, editorPid) {
|
|
37248
|
+
const engine = await this.engine();
|
|
37249
|
+
if (!engine.ok) {
|
|
37250
|
+
return engine.response;
|
|
37251
|
+
}
|
|
37252
|
+
const ports = {
|
|
37253
|
+
lsp: this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT),
|
|
37254
|
+
dap: this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT)
|
|
37255
|
+
};
|
|
37256
|
+
const asked = await this.handleViaBridge("quit_editor", {});
|
|
37257
|
+
if (asked.isError === true) {
|
|
37258
|
+
return asked;
|
|
37259
|
+
}
|
|
37260
|
+
await this.waitForBridge(() => !alive(editorPid), Date.now() + EDITOR_RESTART_TIMEOUT_MS);
|
|
37261
|
+
const opened = await this.openAnEditor(engine.value, projectPath, ports);
|
|
37262
|
+
if (opened.error !== null) {
|
|
37263
|
+
return this.createErrorResponse(`The editor was asked to go and could not be started again: ${opened.error}`, [`editor_launch opens one on ${projectPath}`]);
|
|
37264
|
+
}
|
|
37265
|
+
return this.jsonTextResponse({ restarting: true, pid: opened.pid });
|
|
37266
|
+
}
|
|
37147
37267
|
async waitForBridge(reached, deadline) {
|
|
37148
37268
|
for (;; ) {
|
|
37149
37269
|
if (reached()) {
|
|
@@ -37173,14 +37293,28 @@ class GodotServer {
|
|
|
37173
37293
|
}
|
|
37174
37294
|
this.logDebug(`Launching Godot editor for project: ${project.value.path}`);
|
|
37175
37295
|
const ports = await this.portsForAnEditor();
|
|
37176
|
-
const
|
|
37296
|
+
const opened = await this.openAnEditor(engine.value, project.value.path, ports);
|
|
37297
|
+
if (opened.error !== null) {
|
|
37298
|
+
return this.createErrorResponse(`Could not start the editor: ${opened.error}`);
|
|
37299
|
+
}
|
|
37300
|
+
return this.jsonTextResponse({
|
|
37301
|
+
launched: true,
|
|
37302
|
+
pid: opened.pid,
|
|
37303
|
+
projectPath: project.value.path,
|
|
37304
|
+
lspPort: ports.lsp,
|
|
37305
|
+
dapPort: ports.dap
|
|
37306
|
+
});
|
|
37307
|
+
}
|
|
37308
|
+
async openAnEditor(engine, projectPath, ports) {
|
|
37309
|
+
const editor = spawn(engine, editorArguments(projectPath, ports), {
|
|
37177
37310
|
stdio: "ignore",
|
|
37178
37311
|
detached: true,
|
|
37179
37312
|
env: {
|
|
37180
37313
|
...process.env,
|
|
37181
37314
|
GDHARNESS_RUNTIME_DIR: runtimeDirectory(),
|
|
37182
37315
|
GDHARNESS_LSP_PORT: String(ports.lsp),
|
|
37183
|
-
GDHARNESS_DAP_PORT: String(ports.dap)
|
|
37316
|
+
GDHARNESS_DAP_PORT: String(ports.dap),
|
|
37317
|
+
[OPENED_BY_A_SERVER]: "1"
|
|
37184
37318
|
}
|
|
37185
37319
|
});
|
|
37186
37320
|
const started = await new Promise((resolve) => {
|
|
@@ -37192,16 +37326,10 @@ class GodotServer {
|
|
|
37192
37326
|
});
|
|
37193
37327
|
});
|
|
37194
37328
|
if (started !== null) {
|
|
37195
|
-
return
|
|
37329
|
+
return { pid: null, error: started };
|
|
37196
37330
|
}
|
|
37197
37331
|
editor.unref();
|
|
37198
|
-
return
|
|
37199
|
-
launched: true,
|
|
37200
|
-
pid: editor.pid ?? null,
|
|
37201
|
-
projectPath: project.value.path,
|
|
37202
|
-
lspPort: ports.lsp,
|
|
37203
|
-
dapPort: ports.dap
|
|
37204
|
-
});
|
|
37332
|
+
return { pid: editor.pid ?? null, error: null };
|
|
37205
37333
|
}
|
|
37206
37334
|
async portsForAnEditor() {
|
|
37207
37335
|
const lsp = portFromEnvOrNull("GDHARNESS_LSP_PORT") ?? await freePort(DEFAULT_LSP_PORT);
|
|
@@ -37507,7 +37635,7 @@ class GodotServer {
|
|
|
37507
37635
|
return {
|
|
37508
37636
|
content: [
|
|
37509
37637
|
{ type: "text", text: `Screenshot captured: ${dimensions}` },
|
|
37510
|
-
{ type: "image", data:
|
|
37638
|
+
{ type: "image", data: readFileSync13(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
37511
37639
|
]
|
|
37512
37640
|
};
|
|
37513
37641
|
} finally {
|
|
@@ -37573,7 +37701,7 @@ async function runGodotServer() {
|
|
|
37573
37701
|
const server = new GodotServer;
|
|
37574
37702
|
await server.run();
|
|
37575
37703
|
}
|
|
37576
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run5, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, SLOWEST_FRAME_RATE = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, HEADLESS_OPERATIONS, PROJECT_INFO_SECTIONS, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
37704
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run5, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, SLOWEST_FRAME_RATE = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, HEADLESS_OPERATIONS, PROJECT_INFO_SECTIONS, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
37577
37705
|
var init_server3 = __esm(() => {
|
|
37578
37706
|
init_mcp();
|
|
37579
37707
|
init_stdio2();
|
|
@@ -38322,27 +38450,42 @@ function displayPath(harness, platform = process3.platform) {
|
|
|
38322
38450
|
}
|
|
38323
38451
|
return `${harness.scope === "home" ? "~/" : ""}${file.replaceAll("\\", "/")}`;
|
|
38324
38452
|
}
|
|
38325
|
-
|
|
38453
|
+
var OURS = ["GODOT_PATH", "GDHARNESS_PROJECT"];
|
|
38454
|
+
function environmentFor(launch, held) {
|
|
38455
|
+
const kept = {};
|
|
38456
|
+
if (typeof held === "object" && held !== null && !Array.isArray(held)) {
|
|
38457
|
+
for (const [name, value] of Object.entries(held)) {
|
|
38458
|
+
if (typeof value === "string" && !OURS.includes(name)) {
|
|
38459
|
+
kept[name] = value;
|
|
38460
|
+
}
|
|
38461
|
+
}
|
|
38462
|
+
}
|
|
38463
|
+
return { ...kept, ...launch.env };
|
|
38464
|
+
}
|
|
38465
|
+
function entryFor(shape, launch, held) {
|
|
38466
|
+
const before = typeof held === "object" && held !== null && !Array.isArray(held) ? { ...held } : {};
|
|
38326
38467
|
if (shape === "opencode") {
|
|
38327
38468
|
return {
|
|
38469
|
+
...before,
|
|
38328
38470
|
type: "local",
|
|
38329
38471
|
command: [launch.command, ...launch.args],
|
|
38330
|
-
environment:
|
|
38472
|
+
environment: environmentFor(launch, before["environment"]),
|
|
38331
38473
|
enabled: true
|
|
38332
38474
|
};
|
|
38333
38475
|
}
|
|
38334
38476
|
return {
|
|
38477
|
+
...before,
|
|
38335
38478
|
...shape === "typed" ? { type: "stdio" } : {},
|
|
38336
38479
|
command: launch.command,
|
|
38337
38480
|
args: [...launch.args],
|
|
38338
|
-
env:
|
|
38481
|
+
env: environmentFor(launch, before["env"])
|
|
38339
38482
|
};
|
|
38340
38483
|
}
|
|
38341
38484
|
function merged(existing, harness, launch) {
|
|
38342
38485
|
const root = typeof existing === "object" && existing !== null && !Array.isArray(existing) ? { ...existing } : {};
|
|
38343
38486
|
const held = root[harness.container];
|
|
38344
38487
|
const servers = typeof held === "object" && held !== null && !Array.isArray(held) ? { ...held } : {};
|
|
38345
|
-
servers[SERVER_KEY] = entryFor(harness.shape, launch);
|
|
38488
|
+
servers[SERVER_KEY] = entryFor(harness.shape, launch, servers[SERVER_KEY]);
|
|
38346
38489
|
root[harness.container] = servers;
|
|
38347
38490
|
return root;
|
|
38348
38491
|
}
|
|
@@ -18,9 +18,14 @@ const ANNOUNCE_PROTOCOL: int = 1
|
|
|
18
18
|
const RECONNECT_DELAY: float = 3.0
|
|
19
19
|
const MAX_RECONNECT_DELAY: float = 30.0
|
|
20
20
|
|
|
21
|
+
## What a server closes with when the editor that said hello belongs to another project. Kept in
|
|
22
|
+
## step with `OTHER_PROJECT_CLOSE_CODE` in src/godot-bridge.ts.
|
|
23
|
+
const ELSEWHERE_CLOSE_CODE: int = 4001
|
|
24
|
+
|
|
21
25
|
## Where Godot keeps the three ports an editor serves. None of them is per editor: the settings
|
|
22
|
-
## file is one for every editor on the machine, so two open at once want the
|
|
23
|
-
## second one binds nothing.
|
|
26
|
+
## file is one for every editor of this engine version on the machine, so two open at once want the
|
|
27
|
+
## same three and the second one binds nothing. Read here and never written, for the reason
|
|
28
|
+
## [method _serves] gives.
|
|
24
29
|
const LSP_SETTING: String = "network/language_server/remote_port"
|
|
25
30
|
const DAP_SETTING: String = "network/debug_adapter/remote_port"
|
|
26
31
|
const DEBUGGER_SETTING: String = "network/debug/remote_port"
|
|
@@ -28,6 +33,9 @@ const DEBUGGER_SETTING: String = "network/debug/remote_port"
|
|
|
28
33
|
## the command line. Kept in step with `editorArguments` in src/launch.ts.
|
|
29
34
|
const LSP_ASKED: String = "GDHARNESS_LSP_PORT"
|
|
30
35
|
const DAP_ASKED: String = "GDHARNESS_DAP_PORT"
|
|
36
|
+
## What a server that opened this editor says about itself, so that being opened by one is stated
|
|
37
|
+
## rather than guessed at from the ports. Kept in step with `OPENED_BY_A_SERVER` in src/launch.ts.
|
|
38
|
+
const OPENED_BY_A_SERVER: String = "GDHARNESS_OPENED_BY_A_SERVER"
|
|
31
39
|
|
|
32
40
|
## How long one attempt is given before the address is called a bad one.
|
|
33
41
|
##
|
|
@@ -67,13 +75,16 @@ var _since_looked: float = 0.0
|
|
|
67
75
|
## anything connects or the project names a different one.
|
|
68
76
|
var _refused_url: String = ""
|
|
69
77
|
var _tried_announced: bool = false
|
|
78
|
+
|
|
79
|
+
## The address this editor has already been told is another project's, so a reconnect loop says it
|
|
80
|
+
## once rather than every few seconds for the rest of the session.
|
|
81
|
+
var _told_about_url: String = ""
|
|
70
82
|
var _connecting_for: float = 0.0
|
|
71
83
|
|
|
72
84
|
|
|
73
85
|
func _ready() -> void:
|
|
74
86
|
_project_path = ProjectSettings.globalize_path("res://")
|
|
75
87
|
version_at_load = _loaded_version()
|
|
76
|
-
_keep_the_ports_this_editor_was_given()
|
|
77
88
|
|
|
78
89
|
_reconnect_timer = Timer.new()
|
|
79
90
|
_reconnect_timer.one_shot = true
|
|
@@ -265,39 +276,41 @@ func _handle_connect() -> void:
|
|
|
265
276
|
"editor_pid": OS.get_process_id(),
|
|
266
277
|
"lsp_port": _serves(LSP_ASKED, LSP_SETTING),
|
|
267
278
|
"dap_port": _serves(DAP_ASKED, DAP_SETTING),
|
|
268
|
-
"debug_port": _serving(DEBUGGER_SETTING)
|
|
279
|
+
"debug_port": _serving(DEBUGGER_SETTING),
|
|
280
|
+
# Whether a server started this editor, which is the only kind that can be started again
|
|
281
|
+
# as itself: the engine hands back none of the arguments it was given, so the arguments
|
|
282
|
+
# have to come from whoever wrote them. See `restart_editor` in play_tools.gd.
|
|
283
|
+
"opened_by_a_server": _opened_by_a_server()
|
|
269
284
|
}
|
|
270
285
|
)
|
|
271
286
|
|
|
272
287
|
connected.emit()
|
|
273
288
|
|
|
274
289
|
|
|
275
|
-
##
|
|
290
|
+
## Whether a gdharness server started this editor, which it says in the environment.
|
|
276
291
|
##
|
|
277
|
-
##
|
|
278
|
-
##
|
|
279
|
-
##
|
|
280
|
-
##
|
|
281
|
-
##
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
_keep_port(DAP_ASKED, DAP_SETTING)
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
func _keep_port(variable: String, setting: String) -> void:
|
|
290
|
-
var port: int = _asked_for(variable)
|
|
291
|
-
if port < 1 or port == _serving(setting):
|
|
292
|
-
return
|
|
293
|
-
var settings: EditorSettings = EditorInterface.get_editor_settings()
|
|
294
|
-
if settings != null:
|
|
295
|
-
settings.set_setting(setting, port)
|
|
292
|
+
## It decides who may restart this editor by starting it again. Only a server that wrote the
|
|
293
|
+
## arguments can write them a second time, and only an editor started that way needs it: one opened
|
|
294
|
+
## by hand is on the ports its own settings name and comes back on them by itself.
|
|
295
|
+
##
|
|
296
|
+
## Said outright rather than read off the two port variables, which a server also sets and anybody
|
|
297
|
+
## else may: an editor started from a shell that exports them is one somebody opened themselves,
|
|
298
|
+
## and taking it for a server's is taking an editor off somebody's desk and putting a new one there.
|
|
299
|
+
static func _opened_by_a_server() -> bool:
|
|
300
|
+
return not OS.get_environment(OPENED_BY_A_SERVER).is_empty()
|
|
296
301
|
|
|
297
302
|
|
|
298
|
-
## What this editor serves: what it was told to when a server opened it, and what its settings
|
|
299
|
-
##
|
|
300
|
-
##
|
|
303
|
+
## What this editor serves: what it was told to when a server opened it, and what its settings say
|
|
304
|
+
## otherwise. The first is the one that counts, because the settings are what the command line was
|
|
305
|
+
## overriding.
|
|
306
|
+
##
|
|
307
|
+
## The settings are read and never written. They were written for a while, because the engine keeps
|
|
308
|
+
## a command-line port override to itself and hands none of it back, so an editor restarting itself
|
|
309
|
+
## came up on whatever the settings said. But there is one settings file for every editor of this
|
|
310
|
+
## engine version on the machine, so writing a port chosen for one project made it the number every
|
|
311
|
+
## other project's editor came up on: an editor opened by hand for something else inherited it and
|
|
312
|
+
## collided with the editor it was moved away from. Restarting is done by starting the editor again
|
|
313
|
+
## with the same arguments now, which is `editor_launch restart`, so there is nothing to persist.
|
|
301
314
|
func _serves(variable: String, setting: String) -> int:
|
|
302
315
|
var asked: int = _asked_for(variable)
|
|
303
316
|
return asked if asked > 0 else _serving(setting)
|
|
@@ -343,12 +356,35 @@ func _loaded_version() -> String:
|
|
|
343
356
|
|
|
344
357
|
func _handle_disconnect() -> void:
|
|
345
358
|
_is_connected = false
|
|
359
|
+
_said_elsewhere()
|
|
346
360
|
disconnected.emit()
|
|
347
361
|
|
|
348
362
|
if _should_reconnect:
|
|
349
363
|
_schedule_reconnect()
|
|
350
364
|
|
|
351
365
|
|
|
366
|
+
## Says so when the server turned this editor away as another project's.
|
|
367
|
+
##
|
|
368
|
+
## The fallback address is tried by every project whose own server has not run yet and so has
|
|
369
|
+
## announced nothing, which means any server holding it took the connection and served it. It is
|
|
370
|
+
## refused now, and the refusal is worth a person's attention rather than a quiet backoff: nothing
|
|
371
|
+
## about waiting reaches a server that is not this project's, and the answer is to start one.
|
|
372
|
+
##
|
|
373
|
+
## Said once per address. This is a reconnect loop, and a line every few seconds for the rest of
|
|
374
|
+
## the session is a line nobody reads.
|
|
375
|
+
func _said_elsewhere() -> void:
|
|
376
|
+
if socket.get_close_code() != ELSEWHERE_CLOSE_CODE:
|
|
377
|
+
return
|
|
378
|
+
if _told_about_url == server_url:
|
|
379
|
+
return
|
|
380
|
+
_told_about_url = server_url
|
|
381
|
+
var said: String = (
|
|
382
|
+
"[gdharness] %s is another project's server, so this editor is not connected: %s."
|
|
383
|
+
+ " Start this project's own server, or point it at one with GDHARNESS_BRIDGE_PORT."
|
|
384
|
+
)
|
|
385
|
+
push_warning(said % [server_url, socket.get_close_reason()])
|
|
386
|
+
|
|
387
|
+
|
|
352
388
|
func _schedule_reconnect() -> void:
|
|
353
389
|
if _reconnect_timer == null:
|
|
354
390
|
return
|
|
@@ -77,6 +77,7 @@ func _init_tools() -> void:
|
|
|
77
77
|
# Animation tools
|
|
78
78
|
"play_scene": [_play_tools, "play_scene"],
|
|
79
79
|
"restart_editor": [_play_tools, "restart_editor"],
|
|
80
|
+
"quit_editor": [_play_tools, "quit_editor"],
|
|
80
81
|
"stop_playing": [_play_tools, "stop_playing"],
|
|
81
82
|
"playing_status": [_play_tools, "playing_status"],
|
|
82
83
|
"create_animation": [_animation_tools, "create_animation"],
|