gdharness 0.9.2 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js
CHANGED
|
@@ -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) {
|
|
@@ -15667,7 +15668,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15667
15668
|
},
|
|
15668
15669
|
{
|
|
15669
15670
|
name: "editor_run",
|
|
15670
|
-
description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until
|
|
15671
|
+
description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way.",
|
|
15671
15672
|
parameters: {
|
|
15672
15673
|
projectPath: PROJECT_PATH,
|
|
15673
15674
|
scene: { type: "string", description: "A scene to run instead of the main scene." },
|
|
@@ -15680,7 +15681,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15680
15681
|
},
|
|
15681
15682
|
requires: ["projectPath"],
|
|
15682
15683
|
operations: {
|
|
15683
|
-
start: { summary: "run the project until
|
|
15684
|
+
start: { summary: "run the project until it quits or is stopped", requires: [] },
|
|
15684
15685
|
stop: { summary: "end the run and answer with what it printed last", requires: [] },
|
|
15685
15686
|
check: { summary: "boot headless, quit after a few frames, and report the verdict", requires: [] }
|
|
15686
15687
|
},
|
|
@@ -15688,7 +15689,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15688
15689
|
},
|
|
15689
15690
|
{
|
|
15690
15691
|
name: "editor_output",
|
|
15691
|
-
description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. Answers with the counts and the verdict as well as the entries.",
|
|
15692
|
+
description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. Answers with the counts and the verdict as well as the entries. A run that has quit still answers here, with running false and its exit code, until the next one starts.",
|
|
15692
15693
|
parameters: {
|
|
15693
15694
|
severity: {
|
|
15694
15695
|
type: "string",
|
|
@@ -34499,6 +34500,7 @@ var init_godot_bridge = __esm(() => {
|
|
|
34499
34500
|
lspPort: this.connectionInfo?.lspPort,
|
|
34500
34501
|
dapPort: this.connectionInfo?.dapPort,
|
|
34501
34502
|
debugPort: this.connectionInfo?.debugPort,
|
|
34503
|
+
openedByAServer: this.connectionInfo?.openedByAServer,
|
|
34502
34504
|
pendingRequests: this.pendingRequests.size,
|
|
34503
34505
|
queuedResources: this.resourceQueues.size
|
|
34504
34506
|
};
|
|
@@ -34639,6 +34641,7 @@ var init_godot_bridge = __esm(() => {
|
|
|
34639
34641
|
this.connectionInfo.lspPort = servedPort(message.lsp_port);
|
|
34640
34642
|
this.connectionInfo.dapPort = servedPort(message.dap_port);
|
|
34641
34643
|
this.connectionInfo.debugPort = servedPort(message.debug_port);
|
|
34644
|
+
this.connectionInfo.openedByAServer = message.opened_by_a_server === true;
|
|
34642
34645
|
this.log("info", `Godot ready: ${message.project_path}`);
|
|
34643
34646
|
this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
|
|
34644
34647
|
}
|
|
@@ -35999,6 +36002,17 @@ import { promisify as promisify5 } from "node:util";
|
|
|
35999
36002
|
function patienceForFrames(frames, atLeast) {
|
|
36000
36003
|
return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
|
|
36001
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
|
+
}
|
|
36002
36016
|
function isOfType(value, kind) {
|
|
36003
36017
|
switch (kind) {
|
|
36004
36018
|
case "string":
|
|
@@ -36234,12 +36248,7 @@ class GodotServer {
|
|
|
36234
36248
|
if (announced === null || announced.pid === process.pid) {
|
|
36235
36249
|
return null;
|
|
36236
36250
|
}
|
|
36237
|
-
|
|
36238
|
-
process.kill(announced.pid, 0);
|
|
36239
|
-
return announced.pid;
|
|
36240
|
-
} catch {
|
|
36241
|
-
return null;
|
|
36242
|
-
}
|
|
36251
|
+
return alive(announced.pid) ? announced.pid : null;
|
|
36243
36252
|
}
|
|
36244
36253
|
stopWatchingForASuccessor() {
|
|
36245
36254
|
if (this.successorWatch !== null) {
|
|
@@ -36699,6 +36708,15 @@ class GodotServer {
|
|
|
36699
36708
|
}
|
|
36700
36709
|
async debuggedGame() {
|
|
36701
36710
|
const game = this.activeProcess;
|
|
36711
|
+
if (game !== null && game.exitCode !== null) {
|
|
36712
|
+
return {
|
|
36713
|
+
ok: false,
|
|
36714
|
+
response: this.createErrorResponse(`The last run has already finished (exit code ${game.exitCode}), so there is no debug session to answer for.`, [
|
|
36715
|
+
"editor_output still reads what it printed, until the next run starts",
|
|
36716
|
+
"editor_run start plays another, which the debugger can hold"
|
|
36717
|
+
])
|
|
36718
|
+
};
|
|
36719
|
+
}
|
|
36702
36720
|
if (game === null) {
|
|
36703
36721
|
return {
|
|
36704
36722
|
ok: false,
|
|
@@ -37191,7 +37209,7 @@ class GodotServer {
|
|
|
37191
37209
|
version: godotPath === null ? null : await this.godotVersion(godotPath)
|
|
37192
37210
|
},
|
|
37193
37211
|
game: {
|
|
37194
|
-
processActive: this.activeProcess
|
|
37212
|
+
processActive: this.activeProcess?.exitCode === null,
|
|
37195
37213
|
playingInEditor: playing,
|
|
37196
37214
|
runtimeConnected: games.some((game) => game.reachable),
|
|
37197
37215
|
runtimes: games
|
|
@@ -37206,7 +37224,8 @@ class GodotServer {
|
|
|
37206
37224
|
"editor_status says whether the bridge is up and what has reached it"
|
|
37207
37225
|
]);
|
|
37208
37226
|
}
|
|
37209
|
-
const
|
|
37227
|
+
const mine = before.openedByAServer === true && before.projectPath !== undefined;
|
|
37228
|
+
const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid) : await this.handleViaBridge("restart_editor", {});
|
|
37210
37229
|
if (asked.isError === true) {
|
|
37211
37230
|
return asked;
|
|
37212
37231
|
}
|
|
@@ -37234,6 +37253,26 @@ class GodotServer {
|
|
|
37234
37253
|
tookMs: Date.now() - began
|
|
37235
37254
|
});
|
|
37236
37255
|
}
|
|
37256
|
+
async startItAgain(projectPath, editorPid) {
|
|
37257
|
+
const engine = await this.engine();
|
|
37258
|
+
if (!engine.ok) {
|
|
37259
|
+
return engine.response;
|
|
37260
|
+
}
|
|
37261
|
+
const ports = {
|
|
37262
|
+
lsp: this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT),
|
|
37263
|
+
dap: this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT)
|
|
37264
|
+
};
|
|
37265
|
+
const asked = await this.handleViaBridge("quit_editor", {});
|
|
37266
|
+
if (asked.isError === true) {
|
|
37267
|
+
return asked;
|
|
37268
|
+
}
|
|
37269
|
+
await this.waitForBridge(() => !alive(editorPid), Date.now() + EDITOR_RESTART_TIMEOUT_MS);
|
|
37270
|
+
const opened = await this.openAnEditor(engine.value, projectPath, ports);
|
|
37271
|
+
if (opened.error !== null) {
|
|
37272
|
+
return this.createErrorResponse(`The editor was asked to go and could not be started again: ${opened.error}`, [`editor_launch opens one on ${projectPath}`]);
|
|
37273
|
+
}
|
|
37274
|
+
return this.jsonTextResponse({ restarting: true, pid: opened.pid });
|
|
37275
|
+
}
|
|
37237
37276
|
async waitForBridge(reached, deadline) {
|
|
37238
37277
|
for (;; ) {
|
|
37239
37278
|
if (reached()) {
|
|
@@ -37263,14 +37302,28 @@ class GodotServer {
|
|
|
37263
37302
|
}
|
|
37264
37303
|
this.logDebug(`Launching Godot editor for project: ${project.value.path}`);
|
|
37265
37304
|
const ports = await this.portsForAnEditor();
|
|
37266
|
-
const
|
|
37305
|
+
const opened = await this.openAnEditor(engine.value, project.value.path, ports);
|
|
37306
|
+
if (opened.error !== null) {
|
|
37307
|
+
return this.createErrorResponse(`Could not start the editor: ${opened.error}`);
|
|
37308
|
+
}
|
|
37309
|
+
return this.jsonTextResponse({
|
|
37310
|
+
launched: true,
|
|
37311
|
+
pid: opened.pid,
|
|
37312
|
+
projectPath: project.value.path,
|
|
37313
|
+
lspPort: ports.lsp,
|
|
37314
|
+
dapPort: ports.dap
|
|
37315
|
+
});
|
|
37316
|
+
}
|
|
37317
|
+
async openAnEditor(engine, projectPath, ports) {
|
|
37318
|
+
const editor = spawn(engine, editorArguments(projectPath, ports), {
|
|
37267
37319
|
stdio: "ignore",
|
|
37268
37320
|
detached: true,
|
|
37269
37321
|
env: {
|
|
37270
37322
|
...process.env,
|
|
37271
37323
|
GDHARNESS_RUNTIME_DIR: runtimeDirectory(),
|
|
37272
37324
|
GDHARNESS_LSP_PORT: String(ports.lsp),
|
|
37273
|
-
GDHARNESS_DAP_PORT: String(ports.dap)
|
|
37325
|
+
GDHARNESS_DAP_PORT: String(ports.dap),
|
|
37326
|
+
[OPENED_BY_A_SERVER]: "1"
|
|
37274
37327
|
}
|
|
37275
37328
|
});
|
|
37276
37329
|
const started = await new Promise((resolve) => {
|
|
@@ -37282,16 +37335,10 @@ class GodotServer {
|
|
|
37282
37335
|
});
|
|
37283
37336
|
});
|
|
37284
37337
|
if (started !== null) {
|
|
37285
|
-
return
|
|
37338
|
+
return { pid: null, error: started };
|
|
37286
37339
|
}
|
|
37287
37340
|
editor.unref();
|
|
37288
|
-
return
|
|
37289
|
-
launched: true,
|
|
37290
|
-
pid: editor.pid ?? null,
|
|
37291
|
-
projectPath: project.value.path,
|
|
37292
|
-
lspPort: ports.lsp,
|
|
37293
|
-
dapPort: ports.dap
|
|
37294
|
-
});
|
|
37341
|
+
return { pid: editor.pid ?? null, error: null };
|
|
37295
37342
|
}
|
|
37296
37343
|
async portsForAnEditor() {
|
|
37297
37344
|
const lsp = portFromEnvOrNull("GDHARNESS_LSP_PORT") ?? await freePort(DEFAULT_LSP_PORT);
|
|
@@ -37326,7 +37373,7 @@ class GodotServer {
|
|
|
37326
37373
|
if (op === "check") {
|
|
37327
37374
|
return await this.checkBoot(engine.value, project.value.path, sceneArgument, args);
|
|
37328
37375
|
}
|
|
37329
|
-
if (this.activeProcess) {
|
|
37376
|
+
if (this.activeProcess?.exitCode === null) {
|
|
37330
37377
|
this.logDebug("Ending the running game before starting another");
|
|
37331
37378
|
await this.endActiveGame();
|
|
37332
37379
|
}
|
|
@@ -37345,11 +37392,6 @@ class GodotServer {
|
|
|
37345
37392
|
this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
37346
37393
|
const started = this.spawnGame(engine.value, cmdArgs);
|
|
37347
37394
|
this.activeProcess = started;
|
|
37348
|
-
started.process.on("exit", () => {
|
|
37349
|
-
if (this.activeProcess === started) {
|
|
37350
|
-
this.activeProcess = null;
|
|
37351
|
-
}
|
|
37352
|
-
});
|
|
37353
37395
|
return this.jsonTextResponse({
|
|
37354
37396
|
started: true,
|
|
37355
37397
|
through: "gdharness",
|
|
@@ -38412,27 +38454,42 @@ function displayPath(harness, platform = process3.platform) {
|
|
|
38412
38454
|
}
|
|
38413
38455
|
return `${harness.scope === "home" ? "~/" : ""}${file.replaceAll("\\", "/")}`;
|
|
38414
38456
|
}
|
|
38415
|
-
|
|
38457
|
+
var OURS = ["GODOT_PATH", "GDHARNESS_PROJECT"];
|
|
38458
|
+
function environmentFor(launch, held) {
|
|
38459
|
+
const kept = {};
|
|
38460
|
+
if (typeof held === "object" && held !== null && !Array.isArray(held)) {
|
|
38461
|
+
for (const [name, value] of Object.entries(held)) {
|
|
38462
|
+
if (typeof value === "string" && !OURS.includes(name)) {
|
|
38463
|
+
kept[name] = value;
|
|
38464
|
+
}
|
|
38465
|
+
}
|
|
38466
|
+
}
|
|
38467
|
+
return { ...kept, ...launch.env };
|
|
38468
|
+
}
|
|
38469
|
+
function entryFor(shape, launch, held) {
|
|
38470
|
+
const before = typeof held === "object" && held !== null && !Array.isArray(held) ? { ...held } : {};
|
|
38416
38471
|
if (shape === "opencode") {
|
|
38417
38472
|
return {
|
|
38473
|
+
...before,
|
|
38418
38474
|
type: "local",
|
|
38419
38475
|
command: [launch.command, ...launch.args],
|
|
38420
|
-
environment:
|
|
38476
|
+
environment: environmentFor(launch, before["environment"]),
|
|
38421
38477
|
enabled: true
|
|
38422
38478
|
};
|
|
38423
38479
|
}
|
|
38424
38480
|
return {
|
|
38481
|
+
...before,
|
|
38425
38482
|
...shape === "typed" ? { type: "stdio" } : {},
|
|
38426
38483
|
command: launch.command,
|
|
38427
38484
|
args: [...launch.args],
|
|
38428
|
-
env:
|
|
38485
|
+
env: environmentFor(launch, before["env"])
|
|
38429
38486
|
};
|
|
38430
38487
|
}
|
|
38431
38488
|
function merged(existing, harness, launch) {
|
|
38432
38489
|
const root = typeof existing === "object" && existing !== null && !Array.isArray(existing) ? { ...existing } : {};
|
|
38433
38490
|
const held = root[harness.container];
|
|
38434
38491
|
const servers = typeof held === "object" && held !== null && !Array.isArray(held) ? { ...held } : {};
|
|
38435
|
-
servers[SERVER_KEY] = entryFor(harness.shape, launch);
|
|
38492
|
+
servers[SERVER_KEY] = entryFor(harness.shape, launch, servers[SERVER_KEY]);
|
|
38436
38493
|
root[harness.container] = servers;
|
|
38437
38494
|
return root;
|
|
38438
38495
|
}
|
|
@@ -38550,6 +38607,17 @@ function disconnect(harness, projectPath) {
|
|
|
38550
38607
|
`, "utf8");
|
|
38551
38608
|
return { harness, path, action: "removed" };
|
|
38552
38609
|
}
|
|
38610
|
+
function launchedBy(entry) {
|
|
38611
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
38612
|
+
return;
|
|
38613
|
+
}
|
|
38614
|
+
const held = entry;
|
|
38615
|
+
const whole = held["command"];
|
|
38616
|
+
const args = Array.isArray(held["args"]) ? held["args"] : [];
|
|
38617
|
+
const parts = Array.isArray(whole) ? whole : [whole, ...args];
|
|
38618
|
+
const said = parts.filter((part) => typeof part === "string");
|
|
38619
|
+
return said.length === 0 ? undefined : said.join(" ");
|
|
38620
|
+
}
|
|
38553
38621
|
function connect(harness, projectPath, launch) {
|
|
38554
38622
|
const path = configPath(harness, projectPath);
|
|
38555
38623
|
if (harness.snippet !== undefined) {
|
|
@@ -38574,8 +38642,11 @@ function connect(harness, projectPath, launch) {
|
|
|
38574
38642
|
}
|
|
38575
38643
|
const container = typeof existing === "object" && existing !== null && !Array.isArray(existing) ? existing[harness.container] : undefined;
|
|
38576
38644
|
const already = typeof container === "object" && container !== null && SERVER_KEY in container;
|
|
38577
|
-
|
|
38645
|
+
const written = put(harness, path, `${JSON.stringify(merged(existing, harness, launch), null, 2)}
|
|
38578
38646
|
`, already);
|
|
38647
|
+
const was = launchedBy(container?.[SERVER_KEY]);
|
|
38648
|
+
const now = launchedBy(entryFor(harness.shape, launch));
|
|
38649
|
+
return was === undefined || was === now ? written : { ...written, wasLaunchedBy: was };
|
|
38579
38650
|
}
|
|
38580
38651
|
function put(harness, path, contents, already) {
|
|
38581
38652
|
mkdirSync(dirname2(path), { recursive: true });
|
|
@@ -39262,6 +39333,10 @@ function reportConnection(group, launch, projectPath) {
|
|
|
39262
39333
|
return;
|
|
39263
39334
|
}
|
|
39264
39335
|
console.log(`${who}: ${written.action} ${written.path}`);
|
|
39336
|
+
if (written.wasLaunchedBy !== undefined) {
|
|
39337
|
+
console.log(` it was launched by ${written.wasLaunchedBy}`);
|
|
39338
|
+
console.log(` now by ${[launch.command, ...launch.args].join(" ")}`);
|
|
39339
|
+
}
|
|
39265
39340
|
for (const harness of group.harnesses) {
|
|
39266
39341
|
if (harness.manual !== undefined) {
|
|
39267
39342
|
console.log(` ${harness.name}: ${harness.manual}`);
|
|
@@ -23,8 +23,9 @@ const MAX_RECONNECT_DELAY: float = 30.0
|
|
|
23
23
|
const ELSEWHERE_CLOSE_CODE: int = 4001
|
|
24
24
|
|
|
25
25
|
## Where Godot keeps the three ports an editor serves. None of them is per editor: the settings
|
|
26
|
-
## file is one for every editor on the machine, so two open at once want the
|
|
27
|
-
## 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.
|
|
28
29
|
const LSP_SETTING: String = "network/language_server/remote_port"
|
|
29
30
|
const DAP_SETTING: String = "network/debug_adapter/remote_port"
|
|
30
31
|
const DEBUGGER_SETTING: String = "network/debug/remote_port"
|
|
@@ -32,6 +33,9 @@ const DEBUGGER_SETTING: String = "network/debug/remote_port"
|
|
|
32
33
|
## the command line. Kept in step with `editorArguments` in src/launch.ts.
|
|
33
34
|
const LSP_ASKED: String = "GDHARNESS_LSP_PORT"
|
|
34
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"
|
|
35
39
|
|
|
36
40
|
## How long one attempt is given before the address is called a bad one.
|
|
37
41
|
##
|
|
@@ -81,7 +85,6 @@ var _connecting_for: float = 0.0
|
|
|
81
85
|
func _ready() -> void:
|
|
82
86
|
_project_path = ProjectSettings.globalize_path("res://")
|
|
83
87
|
version_at_load = _loaded_version()
|
|
84
|
-
_keep_the_ports_this_editor_was_given()
|
|
85
88
|
|
|
86
89
|
_reconnect_timer = Timer.new()
|
|
87
90
|
_reconnect_timer.one_shot = true
|
|
@@ -273,39 +276,41 @@ func _handle_connect() -> void:
|
|
|
273
276
|
"editor_pid": OS.get_process_id(),
|
|
274
277
|
"lsp_port": _serves(LSP_ASKED, LSP_SETTING),
|
|
275
278
|
"dap_port": _serves(DAP_ASKED, DAP_SETTING),
|
|
276
|
-
"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()
|
|
277
284
|
}
|
|
278
285
|
)
|
|
279
286
|
|
|
280
287
|
connected.emit()
|
|
281
288
|
|
|
282
289
|
|
|
283
|
-
##
|
|
290
|
+
## Whether a gdharness server started this editor, which it says in the environment.
|
|
284
291
|
##
|
|
285
|
-
##
|
|
286
|
-
##
|
|
287
|
-
##
|
|
288
|
-
##
|
|
289
|
-
##
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
_keep_port(DAP_ASKED, DAP_SETTING)
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
func _keep_port(variable: String, setting: String) -> void:
|
|
298
|
-
var port: int = _asked_for(variable)
|
|
299
|
-
if port < 1 or port == _serving(setting):
|
|
300
|
-
return
|
|
301
|
-
var settings: EditorSettings = EditorInterface.get_editor_settings()
|
|
302
|
-
if settings != null:
|
|
303
|
-
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()
|
|
304
301
|
|
|
305
302
|
|
|
306
|
-
## What this editor serves: what it was told to when a server opened it, and what its settings
|
|
307
|
-
##
|
|
308
|
-
##
|
|
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.
|
|
309
314
|
func _serves(variable: String, setting: String) -> int:
|
|
310
315
|
var asked: int = _asked_for(variable)
|
|
311
316
|
return asked if asked > 0 else _serving(setting)
|
|
@@ -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"],
|
|
@@ -125,6 +125,33 @@ func restart_editor(_args: Dictionary) -> Dictionary:
|
|
|
125
125
|
return {"ok": true, "restarting": true, "saved": true}
|
|
126
126
|
|
|
127
127
|
|
|
128
|
+
## Ends this editor, having saved, so that whoever opened it can open it again.
|
|
129
|
+
##
|
|
130
|
+
## The editor's half of restarting one a server started. Godot's own restart cannot carry the ports
|
|
131
|
+
## such an editor was given, for the reason above: the engine consumes the arguments and hands none
|
|
132
|
+
## of them back. So the server starts it again itself, and this is the part only the editor can do.
|
|
133
|
+
##
|
|
134
|
+
## Saved first, exactly as the restart above saves. The editor is going either way, and unsaved
|
|
135
|
+
## scenes are not this tool's to lose.
|
|
136
|
+
func quit_editor(_args: Dictionary) -> Dictionary:
|
|
137
|
+
if EditorInterface.is_playing_scene():
|
|
138
|
+
EditorInterface.stop_playing_scene()
|
|
139
|
+
|
|
140
|
+
EditorInterface.save_all_scenes()
|
|
141
|
+
|
|
142
|
+
# Deferred so this answer is on its way out before the editor goes.
|
|
143
|
+
_leave.call_deferred()
|
|
144
|
+
|
|
145
|
+
return {"ok": true, "quitting": true, "saved": true}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
## Asked of the tree the editor's own window is in, because this object is not in one.
|
|
149
|
+
static func _leave() -> void:
|
|
150
|
+
var base: Control = EditorInterface.get_base_control()
|
|
151
|
+
if base != null and base.get_tree() != null:
|
|
152
|
+
base.get_tree().quit()
|
|
153
|
+
|
|
154
|
+
|
|
128
155
|
func playing_status(_args: Dictionary) -> Dictionary:
|
|
129
156
|
var playing: bool = EditorInterface.is_playing_scene()
|
|
130
157
|
# The debugger's port with it, read now rather than remembered: it is taken again before every
|
package/build/index.js
CHANGED
|
@@ -25718,6 +25718,7 @@ class GodotBridge extends EventEmitter {
|
|
|
25718
25718
|
lspPort: this.connectionInfo?.lspPort,
|
|
25719
25719
|
dapPort: this.connectionInfo?.dapPort,
|
|
25720
25720
|
debugPort: this.connectionInfo?.debugPort,
|
|
25721
|
+
openedByAServer: this.connectionInfo?.openedByAServer,
|
|
25721
25722
|
pendingRequests: this.pendingRequests.size,
|
|
25722
25723
|
queuedResources: this.resourceQueues.size
|
|
25723
25724
|
};
|
|
@@ -25858,6 +25859,7 @@ class GodotBridge extends EventEmitter {
|
|
|
25858
25859
|
this.connectionInfo.lspPort = servedPort(message.lsp_port);
|
|
25859
25860
|
this.connectionInfo.dapPort = servedPort(message.dap_port);
|
|
25860
25861
|
this.connectionInfo.debugPort = servedPort(message.debug_port);
|
|
25862
|
+
this.connectionInfo.openedByAServer = message.opened_by_a_server === true;
|
|
25861
25863
|
this.log("info", `Godot ready: ${message.project_path}`);
|
|
25862
25864
|
this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
|
|
25863
25865
|
}
|
|
@@ -26148,6 +26150,7 @@ function runArguments(options) {
|
|
|
26148
26150
|
function editorArguments(projectPath, ports) {
|
|
26149
26151
|
return ["-e", "--path", projectPath, "--lsp-port", String(ports.lsp), "--dap-port", String(ports.dap)];
|
|
26150
26152
|
}
|
|
26153
|
+
var OPENED_BY_A_SERVER = "GDHARNESS_OPENED_BY_A_SERVER";
|
|
26151
26154
|
function userDataIn(home, variables = process.env) {
|
|
26152
26155
|
const moved = ["APPDATA", "XDG_DATA_HOME"];
|
|
26153
26156
|
const carried = {};
|
|
@@ -28259,7 +28262,7 @@ var TOOL_SPECS = [
|
|
|
28259
28262
|
},
|
|
28260
28263
|
{
|
|
28261
28264
|
name: "editor_run",
|
|
28262
|
-
description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until
|
|
28265
|
+
description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way.",
|
|
28263
28266
|
parameters: {
|
|
28264
28267
|
projectPath: PROJECT_PATH,
|
|
28265
28268
|
scene: { type: "string", description: "A scene to run instead of the main scene." },
|
|
@@ -28272,7 +28275,7 @@ var TOOL_SPECS = [
|
|
|
28272
28275
|
},
|
|
28273
28276
|
requires: ["projectPath"],
|
|
28274
28277
|
operations: {
|
|
28275
|
-
start: { summary: "run the project until
|
|
28278
|
+
start: { summary: "run the project until it quits or is stopped", requires: [] },
|
|
28276
28279
|
stop: { summary: "end the run and answer with what it printed last", requires: [] },
|
|
28277
28280
|
check: { summary: "boot headless, quit after a few frames, and report the verdict", requires: [] }
|
|
28278
28281
|
},
|
|
@@ -28280,7 +28283,7 @@ var TOOL_SPECS = [
|
|
|
28280
28283
|
},
|
|
28281
28284
|
{
|
|
28282
28285
|
name: "editor_output",
|
|
28283
|
-
description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. Answers with the counts and the verdict as well as the entries.",
|
|
28286
|
+
description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. Answers with the counts and the verdict as well as the entries. A run that has quit still answers here, with running false and its exit code, until the next one starts.",
|
|
28284
28287
|
parameters: {
|
|
28285
28288
|
severity: {
|
|
28286
28289
|
type: "string",
|
|
@@ -28717,6 +28720,17 @@ var SLOWEST_FRAME_RATE = 20;
|
|
|
28717
28720
|
function patienceForFrames(frames, atLeast) {
|
|
28718
28721
|
return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
|
|
28719
28722
|
}
|
|
28723
|
+
function alive(pid) {
|
|
28724
|
+
if (pid === undefined) {
|
|
28725
|
+
return false;
|
|
28726
|
+
}
|
|
28727
|
+
try {
|
|
28728
|
+
process.kill(pid, 0);
|
|
28729
|
+
return true;
|
|
28730
|
+
} catch {
|
|
28731
|
+
return false;
|
|
28732
|
+
}
|
|
28733
|
+
}
|
|
28720
28734
|
function isOfType(value, kind) {
|
|
28721
28735
|
switch (kind) {
|
|
28722
28736
|
case "string":
|
|
@@ -29014,12 +29028,7 @@ class GodotServer {
|
|
|
29014
29028
|
if (announced === null || announced.pid === process.pid) {
|
|
29015
29029
|
return null;
|
|
29016
29030
|
}
|
|
29017
|
-
|
|
29018
|
-
process.kill(announced.pid, 0);
|
|
29019
|
-
return announced.pid;
|
|
29020
|
-
} catch {
|
|
29021
|
-
return null;
|
|
29022
|
-
}
|
|
29031
|
+
return alive(announced.pid) ? announced.pid : null;
|
|
29023
29032
|
}
|
|
29024
29033
|
stopWatchingForASuccessor() {
|
|
29025
29034
|
if (this.successorWatch !== null) {
|
|
@@ -29479,6 +29488,15 @@ class GodotServer {
|
|
|
29479
29488
|
}
|
|
29480
29489
|
async debuggedGame() {
|
|
29481
29490
|
const game = this.activeProcess;
|
|
29491
|
+
if (game !== null && game.exitCode !== null) {
|
|
29492
|
+
return {
|
|
29493
|
+
ok: false,
|
|
29494
|
+
response: this.createErrorResponse(`The last run has already finished (exit code ${game.exitCode}), so there is no debug session to answer for.`, [
|
|
29495
|
+
"editor_output still reads what it printed, until the next run starts",
|
|
29496
|
+
"editor_run start plays another, which the debugger can hold"
|
|
29497
|
+
])
|
|
29498
|
+
};
|
|
29499
|
+
}
|
|
29482
29500
|
if (game === null) {
|
|
29483
29501
|
return {
|
|
29484
29502
|
ok: false,
|
|
@@ -29971,7 +29989,7 @@ class GodotServer {
|
|
|
29971
29989
|
version: godotPath === null ? null : await this.godotVersion(godotPath)
|
|
29972
29990
|
},
|
|
29973
29991
|
game: {
|
|
29974
|
-
processActive: this.activeProcess
|
|
29992
|
+
processActive: this.activeProcess?.exitCode === null,
|
|
29975
29993
|
playingInEditor: playing,
|
|
29976
29994
|
runtimeConnected: games.some((game) => game.reachable),
|
|
29977
29995
|
runtimes: games
|
|
@@ -29986,7 +30004,8 @@ class GodotServer {
|
|
|
29986
30004
|
"editor_status says whether the bridge is up and what has reached it"
|
|
29987
30005
|
]);
|
|
29988
30006
|
}
|
|
29989
|
-
const
|
|
30007
|
+
const mine = before.openedByAServer === true && before.projectPath !== undefined;
|
|
30008
|
+
const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid) : await this.handleViaBridge("restart_editor", {});
|
|
29990
30009
|
if (asked.isError === true) {
|
|
29991
30010
|
return asked;
|
|
29992
30011
|
}
|
|
@@ -30014,6 +30033,26 @@ class GodotServer {
|
|
|
30014
30033
|
tookMs: Date.now() - began
|
|
30015
30034
|
});
|
|
30016
30035
|
}
|
|
30036
|
+
async startItAgain(projectPath, editorPid) {
|
|
30037
|
+
const engine = await this.engine();
|
|
30038
|
+
if (!engine.ok) {
|
|
30039
|
+
return engine.response;
|
|
30040
|
+
}
|
|
30041
|
+
const ports = {
|
|
30042
|
+
lsp: this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT),
|
|
30043
|
+
dap: this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT)
|
|
30044
|
+
};
|
|
30045
|
+
const asked = await this.handleViaBridge("quit_editor", {});
|
|
30046
|
+
if (asked.isError === true) {
|
|
30047
|
+
return asked;
|
|
30048
|
+
}
|
|
30049
|
+
await this.waitForBridge(() => !alive(editorPid), Date.now() + EDITOR_RESTART_TIMEOUT_MS);
|
|
30050
|
+
const opened = await this.openAnEditor(engine.value, projectPath, ports);
|
|
30051
|
+
if (opened.error !== null) {
|
|
30052
|
+
return this.createErrorResponse(`The editor was asked to go and could not be started again: ${opened.error}`, [`editor_launch opens one on ${projectPath}`]);
|
|
30053
|
+
}
|
|
30054
|
+
return this.jsonTextResponse({ restarting: true, pid: opened.pid });
|
|
30055
|
+
}
|
|
30017
30056
|
async waitForBridge(reached, deadline) {
|
|
30018
30057
|
for (;; ) {
|
|
30019
30058
|
if (reached()) {
|
|
@@ -30043,14 +30082,28 @@ class GodotServer {
|
|
|
30043
30082
|
}
|
|
30044
30083
|
this.logDebug(`Launching Godot editor for project: ${project.value.path}`);
|
|
30045
30084
|
const ports = await this.portsForAnEditor();
|
|
30046
|
-
const
|
|
30085
|
+
const opened = await this.openAnEditor(engine.value, project.value.path, ports);
|
|
30086
|
+
if (opened.error !== null) {
|
|
30087
|
+
return this.createErrorResponse(`Could not start the editor: ${opened.error}`);
|
|
30088
|
+
}
|
|
30089
|
+
return this.jsonTextResponse({
|
|
30090
|
+
launched: true,
|
|
30091
|
+
pid: opened.pid,
|
|
30092
|
+
projectPath: project.value.path,
|
|
30093
|
+
lspPort: ports.lsp,
|
|
30094
|
+
dapPort: ports.dap
|
|
30095
|
+
});
|
|
30096
|
+
}
|
|
30097
|
+
async openAnEditor(engine, projectPath, ports) {
|
|
30098
|
+
const editor = spawn(engine, editorArguments(projectPath, ports), {
|
|
30047
30099
|
stdio: "ignore",
|
|
30048
30100
|
detached: true,
|
|
30049
30101
|
env: {
|
|
30050
30102
|
...process.env,
|
|
30051
30103
|
GDHARNESS_RUNTIME_DIR: runtimeDirectory(),
|
|
30052
30104
|
GDHARNESS_LSP_PORT: String(ports.lsp),
|
|
30053
|
-
GDHARNESS_DAP_PORT: String(ports.dap)
|
|
30105
|
+
GDHARNESS_DAP_PORT: String(ports.dap),
|
|
30106
|
+
[OPENED_BY_A_SERVER]: "1"
|
|
30054
30107
|
}
|
|
30055
30108
|
});
|
|
30056
30109
|
const started = await new Promise((resolve) => {
|
|
@@ -30062,16 +30115,10 @@ class GodotServer {
|
|
|
30062
30115
|
});
|
|
30063
30116
|
});
|
|
30064
30117
|
if (started !== null) {
|
|
30065
|
-
return
|
|
30118
|
+
return { pid: null, error: started };
|
|
30066
30119
|
}
|
|
30067
30120
|
editor.unref();
|
|
30068
|
-
return
|
|
30069
|
-
launched: true,
|
|
30070
|
-
pid: editor.pid ?? null,
|
|
30071
|
-
projectPath: project.value.path,
|
|
30072
|
-
lspPort: ports.lsp,
|
|
30073
|
-
dapPort: ports.dap
|
|
30074
|
-
});
|
|
30121
|
+
return { pid: editor.pid ?? null, error: null };
|
|
30075
30122
|
}
|
|
30076
30123
|
async portsForAnEditor() {
|
|
30077
30124
|
const lsp = portFromEnvOrNull("GDHARNESS_LSP_PORT") ?? await freePort(DEFAULT_LSP_PORT);
|
|
@@ -30106,7 +30153,7 @@ class GodotServer {
|
|
|
30106
30153
|
if (op === "check") {
|
|
30107
30154
|
return await this.checkBoot(engine.value, project.value.path, sceneArgument, args);
|
|
30108
30155
|
}
|
|
30109
|
-
if (this.activeProcess) {
|
|
30156
|
+
if (this.activeProcess?.exitCode === null) {
|
|
30110
30157
|
this.logDebug("Ending the running game before starting another");
|
|
30111
30158
|
await this.endActiveGame();
|
|
30112
30159
|
}
|
|
@@ -30125,11 +30172,6 @@ class GodotServer {
|
|
|
30125
30172
|
this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
30126
30173
|
const started = this.spawnGame(engine.value, cmdArgs);
|
|
30127
30174
|
this.activeProcess = started;
|
|
30128
|
-
started.process.on("exit", () => {
|
|
30129
|
-
if (this.activeProcess === started) {
|
|
30130
|
-
this.activeProcess = null;
|
|
30131
|
-
}
|
|
30132
|
-
});
|
|
30133
30175
|
return this.jsonTextResponse({
|
|
30134
30176
|
started: true,
|
|
30135
30177
|
through: "gdharness",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.4",
|
|
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",
|