gdharness 0.13.38 → 0.13.40
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 +618 -184
- package/build/godot/addons/gdharness_editor/bridge_client.gd +53 -0
- package/build/index.js +690 -265
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -15373,7 +15373,7 @@ function buildToolDefinitions() {
|
|
|
15373
15373
|
};
|
|
15374
15374
|
});
|
|
15375
15375
|
}
|
|
15376
|
-
var PROJECT_INFO_SECTIONS, PROJECT_PATH, RUNNING_PROJECT_PATH, SCENE_PATH, SCRIPT_PATH, RESOURCE_PATH, NODE_PATH, PROPERTIES, XY, ANIMATION_TRACK, SCRIPT_MODIFICATIONS, INPUT_EVENTS, TILESET_SOURCES, TILEMAP_CELLS, COLOUR, TOOL_SPECS, SPECS_BY_NAME;
|
|
15376
|
+
var PROJECT_INFO_SECTIONS, PROJECT_PATH, RUNNING_PROJECT_PATH, RUNNING_PID, SCENE_PATH, SCRIPT_PATH, RESOURCE_PATH, NODE_PATH, PROPERTIES, XY, ANIMATION_TRACK, SCRIPT_MODIFICATIONS, INPUT_EVENTS, TILESET_SOURCES, TILEMAP_CELLS, COLOUR, TOOL_SPECS, SPECS_BY_NAME;
|
|
15377
15377
|
var init_tool_definitions = __esm(() => {
|
|
15378
15378
|
PROJECT_INFO_SECTIONS = [
|
|
15379
15379
|
"autoloads",
|
|
@@ -15391,6 +15391,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
15391
15391
|
type: "string",
|
|
15392
15392
|
description: "Which game, when more than one is running: the project directory it was started from. Not needed with one game."
|
|
15393
15393
|
};
|
|
15394
|
+
RUNNING_PID = {
|
|
15395
|
+
type: "number",
|
|
15396
|
+
description: "Which game, when several are running from one project, such as a bench and its workers: its process id, as editor_status lists under runtimes. Not needed with one game per project."
|
|
15397
|
+
};
|
|
15394
15398
|
SCENE_PATH = {
|
|
15395
15399
|
type: "string",
|
|
15396
15400
|
description: 'Scene file inside the project, such as "scenes/main.tscn" or "res://scenes/main.tscn".'
|
|
@@ -16029,7 +16033,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16029
16033
|
},
|
|
16030
16034
|
{
|
|
16031
16035
|
name: "editor_output",
|
|
16032
|
-
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. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering.",
|
|
16036
|
+
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. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering.",
|
|
16033
16037
|
parameters: {
|
|
16034
16038
|
severity: {
|
|
16035
16039
|
type: "string",
|
|
@@ -16057,7 +16061,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16057
16061
|
},
|
|
16058
16062
|
{
|
|
16059
16063
|
name: "editor_status",
|
|
16060
|
-
description:
|
|
16064
|
+
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it.",
|
|
16061
16065
|
parameters: {},
|
|
16062
16066
|
requires: []
|
|
16063
16067
|
},
|
|
@@ -16114,6 +16118,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16114
16118
|
description: "Questions about the running game: what is written on the screen, the scene tree, the nodes matching a query, where one node is on screen, what one property reads, or the performance metrics. Needs the game running with the runtime addon.",
|
|
16115
16119
|
parameters: {
|
|
16116
16120
|
projectPath: RUNNING_PROJECT_PATH,
|
|
16121
|
+
pid: RUNNING_PID,
|
|
16117
16122
|
nodePath: {
|
|
16118
16123
|
type: "string",
|
|
16119
16124
|
ops: ["tree", "text", "find", "rect", "property"],
|
|
@@ -16192,6 +16197,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16192
16197
|
description: "Sets a property or calls a method on a node in the running game. Needs the game running with the runtime addon.",
|
|
16193
16198
|
parameters: {
|
|
16194
16199
|
projectPath: RUNNING_PROJECT_PATH,
|
|
16200
|
+
pid: RUNNING_PID,
|
|
16195
16201
|
nodePath: { type: "string", description: 'Absolute node path, such as "/root/Main/Player".' },
|
|
16196
16202
|
property: {
|
|
16197
16203
|
type: "string",
|
|
@@ -16231,6 +16237,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16231
16237
|
description: "A picture of the running game: the whole screen or one viewport, as an image. Needs the game running with a window.",
|
|
16232
16238
|
parameters: {
|
|
16233
16239
|
projectPath: RUNNING_PROJECT_PATH,
|
|
16240
|
+
pid: RUNNING_PID,
|
|
16234
16241
|
viewportPath: {
|
|
16235
16242
|
type: "string",
|
|
16236
16243
|
ops: ["viewport"],
|
|
@@ -16251,6 +16258,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16251
16258
|
description: "Input to the running game: a whole click on a Control or a 3D node named by path, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it.",
|
|
16252
16259
|
parameters: {
|
|
16253
16260
|
projectPath: RUNNING_PROJECT_PATH,
|
|
16261
|
+
pid: RUNNING_PID,
|
|
16254
16262
|
nodePath: {
|
|
16255
16263
|
type: "string",
|
|
16256
16264
|
ops: ["click", "choose"],
|
|
@@ -16347,6 +16355,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16347
16355
|
description: "Lets the running game get on with it and answers when something has happened: a number of frames, a signal, a property reaching a value, or words appearing on a screen. Needs the game running with the runtime addon.",
|
|
16348
16356
|
parameters: {
|
|
16349
16357
|
projectPath: RUNNING_PROJECT_PATH,
|
|
16358
|
+
pid: RUNNING_PID,
|
|
16350
16359
|
frames: {
|
|
16351
16360
|
type: "number",
|
|
16352
16361
|
ops: ["frames"],
|
|
@@ -16382,7 +16391,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16382
16391
|
},
|
|
16383
16392
|
{
|
|
16384
16393
|
name: "debug_breakpoint",
|
|
16385
|
-
description: "Sets or removes a breakpoint through the editor's debug adapter. Needs the editor, not a running game: set them first, then editor_run, and the game stops where you asked.",
|
|
16394
|
+
description: "Sets or removes a breakpoint through the editor's debug adapter. Needs the editor, not a running game: set them first, then editor_run, and the game stops where you asked. The editor keeps a breakpoint set this way for one play, so every breakpoint this server holds is sent again before each play editor_run starts, and the start answer lists them under breakpoints. The set is also kept in the project, for the server that comes after a reconnect: a breakpoint stays until it is removed here, whichever server set it. Every answer lists the whole set under held, and the breakpoints the user set in the editor's own gutter under setInEditor; setting one here never takes those away, and removing a line here clears it whoever set it. Godot clears every breakpoint in the editor when a debug session opens unless network/debug_adapter/sync_breakpoints is on, which the addon turns on as it loads; an editor running an older addon is said so under breakpointsAtRisk, here and in editor_status.",
|
|
16386
16395
|
parameters: {
|
|
16387
16396
|
projectPath: PROJECT_PATH,
|
|
16388
16397
|
scriptPath: SCRIPT_PATH,
|
|
@@ -16407,7 +16416,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16407
16416
|
},
|
|
16408
16417
|
{
|
|
16409
16418
|
name: "debug_state",
|
|
16410
|
-
description: "Where the debugged game is stopped: the stack trace, what is in scope at a frame with the values, or the debug adapter's console output so far.",
|
|
16419
|
+
description: "Where the debugged game is stopped: the stack trace, what is in scope at a frame with the values, or the debug adapter's console output so far. A stop is reported by the editor's adapter only to the sessions connected when it happened, so a server that connected afterwards, which is every server after a reconnect, is shown a thread and no frames about a game still sitting at its breakpoint. That server asks the runtime instead: a held game accepts a connection and never answers, and the refusal then says held rather than running, and that the stack cannot be read from here. debug_control continue lets it go, and the next stop is one this session is told about, with its stack.",
|
|
16411
16420
|
parameters: {
|
|
16412
16421
|
frameId: {
|
|
16413
16422
|
type: "number",
|
|
@@ -31179,15 +31188,77 @@ var init_stdio2 = __esm(() => {
|
|
|
31179
31188
|
init_stdio();
|
|
31180
31189
|
});
|
|
31181
31190
|
|
|
31182
|
-
// src/
|
|
31191
|
+
// src/breakpoint-note.ts
|
|
31183
31192
|
import { mkdirSync as mkdirSync6, readFileSync as readFileSync10, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "node:fs";
|
|
31184
|
-
import { dirname as dirname7, join as join12 } from "node:path";
|
|
31193
|
+
import { dirname as dirname7, isAbsolute as isAbsolute2, join as join12, relative as relative2 } from "node:path";
|
|
31194
|
+
function breakpointNotePath(projectPath) {
|
|
31195
|
+
return join12(projectPath, ".godot", "gdharness-breakpoints.json");
|
|
31196
|
+
}
|
|
31197
|
+
function writeBreakpointNote(projectPath, held) {
|
|
31198
|
+
const path = breakpointNotePath(projectPath);
|
|
31199
|
+
const root = realPathOr(projectPath);
|
|
31200
|
+
const inside = held.map((one) => ({ scriptPath: realPathOr(one.scriptPath), lines: one.lines })).filter((one) => one.lines.length > 0 && withinProject(root, one.scriptPath));
|
|
31201
|
+
try {
|
|
31202
|
+
if (inside.length === 0) {
|
|
31203
|
+
rmSync6(path, { force: true });
|
|
31204
|
+
return;
|
|
31205
|
+
}
|
|
31206
|
+
const note = {
|
|
31207
|
+
breakpoints: inside.map((one) => ({
|
|
31208
|
+
script: relative2(root, one.scriptPath).replaceAll("\\", "/"),
|
|
31209
|
+
lines: [...one.lines]
|
|
31210
|
+
}))
|
|
31211
|
+
};
|
|
31212
|
+
mkdirSync6(dirname7(path), { recursive: true });
|
|
31213
|
+
writeFileSync8(path, `${JSON.stringify(note, null, 2)}
|
|
31214
|
+
`, "utf8");
|
|
31215
|
+
} catch (error) {
|
|
31216
|
+
console.error(`[SERVER] Could not record the breakpoints at ${path}: ${errorMessage(error)}`);
|
|
31217
|
+
}
|
|
31218
|
+
}
|
|
31219
|
+
function readBreakpointNote(projectPath) {
|
|
31220
|
+
try {
|
|
31221
|
+
const parsed = JSON.parse(readFileSync10(breakpointNotePath(projectPath), "utf8"));
|
|
31222
|
+
if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.breakpoints)) {
|
|
31223
|
+
return [];
|
|
31224
|
+
}
|
|
31225
|
+
const held = [];
|
|
31226
|
+
for (const entry of parsed.breakpoints) {
|
|
31227
|
+
if (typeof entry !== "object" || entry === null) {
|
|
31228
|
+
continue;
|
|
31229
|
+
}
|
|
31230
|
+
const { script, lines: listed } = entry;
|
|
31231
|
+
if (typeof script !== "string" || isAbsolute2(script) || script.split("/").includes("..")) {
|
|
31232
|
+
continue;
|
|
31233
|
+
}
|
|
31234
|
+
const lines = Array.isArray(listed) ? listed.filter((line) => Number.isInteger(line) && line > 0) : [];
|
|
31235
|
+
if (lines.length > 0) {
|
|
31236
|
+
held.push({ scriptPath: realPathOr(join12(projectPath, script)), lines });
|
|
31237
|
+
}
|
|
31238
|
+
}
|
|
31239
|
+
return held;
|
|
31240
|
+
} catch {
|
|
31241
|
+
return [];
|
|
31242
|
+
}
|
|
31243
|
+
}
|
|
31244
|
+
function withinProject(projectPath, scriptPath) {
|
|
31245
|
+
const rel = relative2(projectPath, scriptPath);
|
|
31246
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute2(rel);
|
|
31247
|
+
}
|
|
31248
|
+
var init_breakpoint_note = __esm(() => {
|
|
31249
|
+
init_errors();
|
|
31250
|
+
init_paths();
|
|
31251
|
+
});
|
|
31252
|
+
|
|
31253
|
+
// src/bridge-announce.ts
|
|
31254
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync11, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
|
|
31255
|
+
import { dirname as dirname8, join as join13 } from "node:path";
|
|
31185
31256
|
function announcementPath(projectPath) {
|
|
31186
|
-
return
|
|
31257
|
+
return join13(projectPath, ".godot", "gdharness-bridge.json");
|
|
31187
31258
|
}
|
|
31188
31259
|
function readAnnouncement(path) {
|
|
31189
31260
|
try {
|
|
31190
|
-
const parsed = JSON.parse(
|
|
31261
|
+
const parsed = JSON.parse(readFileSync11(path, "utf8"));
|
|
31191
31262
|
if (typeof parsed !== "object" || parsed === null) {
|
|
31192
31263
|
return null;
|
|
31193
31264
|
}
|
|
@@ -31206,7 +31277,7 @@ function announcementIsOurs(path) {
|
|
|
31206
31277
|
function announceBridge(projectPath, bridge) {
|
|
31207
31278
|
const path = announcementPath(projectPath);
|
|
31208
31279
|
try {
|
|
31209
|
-
|
|
31280
|
+
mkdirSync7(dirname8(path), { recursive: true });
|
|
31210
31281
|
const announcement = {
|
|
31211
31282
|
protocol: BRIDGE_ANNOUNCE_PROTOCOL,
|
|
31212
31283
|
host: bridge.host,
|
|
@@ -31215,7 +31286,7 @@ function announceBridge(projectPath, bridge) {
|
|
|
31215
31286
|
version: bridge.version,
|
|
31216
31287
|
startedAt: new Date().toISOString()
|
|
31217
31288
|
};
|
|
31218
|
-
|
|
31289
|
+
writeFileSync9(path, `${JSON.stringify(announcement, null, 2)}
|
|
31219
31290
|
`, "utf8");
|
|
31220
31291
|
return path;
|
|
31221
31292
|
} catch (error) {
|
|
@@ -31228,7 +31299,7 @@ function withdrawBridge(path) {
|
|
|
31228
31299
|
return;
|
|
31229
31300
|
}
|
|
31230
31301
|
try {
|
|
31231
|
-
|
|
31302
|
+
rmSync7(path, { force: true });
|
|
31232
31303
|
} catch (error) {
|
|
31233
31304
|
console.error(`[SERVER] Could not withdraw the editor bridge announcement: ${errorMessage(error)}`);
|
|
31234
31305
|
}
|
|
@@ -31239,16 +31310,16 @@ var init_bridge_announce = __esm(() => {
|
|
|
31239
31310
|
});
|
|
31240
31311
|
|
|
31241
31312
|
// src/config-pin.ts
|
|
31242
|
-
import { existsSync as existsSync10, readFileSync as
|
|
31243
|
-
import { join as
|
|
31313
|
+
import { existsSync as existsSync10, readFileSync as readFileSync12 } from "node:fs";
|
|
31314
|
+
import { join as join14 } from "node:path";
|
|
31244
31315
|
function versionsNamedInConfig(projectPath) {
|
|
31245
|
-
const config =
|
|
31316
|
+
const config = join14(projectPath, ".mcp.json");
|
|
31246
31317
|
if (!existsSync10(config)) {
|
|
31247
31318
|
return [];
|
|
31248
31319
|
}
|
|
31249
31320
|
let text;
|
|
31250
31321
|
try {
|
|
31251
|
-
text =
|
|
31322
|
+
text = readFileSync12(config, "utf8");
|
|
31252
31323
|
JSON.parse(text);
|
|
31253
31324
|
} catch {
|
|
31254
31325
|
return [];
|
|
@@ -31417,7 +31488,10 @@ class GodotDAPClient {
|
|
|
31417
31488
|
attached = false;
|
|
31418
31489
|
lastThreadId = 1;
|
|
31419
31490
|
halt = null;
|
|
31491
|
+
holdKnown = false;
|
|
31420
31492
|
breakpoints = new Map;
|
|
31493
|
+
inEditor = new Map;
|
|
31494
|
+
onBreakpointsChanged = null;
|
|
31421
31495
|
constructor(port = portFromEnv("GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT), host = "127.0.0.1") {
|
|
31422
31496
|
this.port = port;
|
|
31423
31497
|
this.host = host;
|
|
@@ -31462,11 +31536,7 @@ class GodotDAPClient {
|
|
|
31462
31536
|
this.failPendingRequests(error);
|
|
31463
31537
|
});
|
|
31464
31538
|
socket.on("close", () => {
|
|
31465
|
-
this.
|
|
31466
|
-
this.initialized = false;
|
|
31467
|
-
this.attached = false;
|
|
31468
|
-
this.halt = null;
|
|
31469
|
-
this.socket = null;
|
|
31539
|
+
this.forgetConnection();
|
|
31470
31540
|
this.failPendingRequests(new Error("DAP connection closed"));
|
|
31471
31541
|
});
|
|
31472
31542
|
resolve();
|
|
@@ -31482,10 +31552,7 @@ class GodotDAPClient {
|
|
|
31482
31552
|
}
|
|
31483
31553
|
async abandon() {
|
|
31484
31554
|
if (!this.socket) {
|
|
31485
|
-
this.
|
|
31486
|
-
this.initialized = false;
|
|
31487
|
-
this.attached = false;
|
|
31488
|
-
this.halt = null;
|
|
31555
|
+
this.forgetConnection();
|
|
31489
31556
|
return;
|
|
31490
31557
|
}
|
|
31491
31558
|
await new Promise((resolve) => {
|
|
@@ -31505,11 +31572,16 @@ class GodotDAPClient {
|
|
|
31505
31572
|
resolve();
|
|
31506
31573
|
}, 500);
|
|
31507
31574
|
});
|
|
31575
|
+
this.forgetConnection();
|
|
31576
|
+
}
|
|
31577
|
+
forgetConnection() {
|
|
31508
31578
|
this.socket = null;
|
|
31509
31579
|
this.connected = false;
|
|
31510
31580
|
this.initialized = false;
|
|
31511
31581
|
this.attached = false;
|
|
31512
31582
|
this.halt = null;
|
|
31583
|
+
this.holdKnown = false;
|
|
31584
|
+
this.inEditor.clear();
|
|
31513
31585
|
}
|
|
31514
31586
|
async ensureConnected() {
|
|
31515
31587
|
if (!this.connected) {
|
|
@@ -31598,6 +31670,37 @@ class GodotDAPClient {
|
|
|
31598
31670
|
description: said(body, "description"),
|
|
31599
31671
|
text: said(body, "text")
|
|
31600
31672
|
};
|
|
31673
|
+
this.holdKnown = true;
|
|
31674
|
+
return;
|
|
31675
|
+
}
|
|
31676
|
+
if (eventName === "continued") {
|
|
31677
|
+
this.halt = null;
|
|
31678
|
+
this.holdKnown = true;
|
|
31679
|
+
return;
|
|
31680
|
+
}
|
|
31681
|
+
if (eventName === "breakpoint") {
|
|
31682
|
+
const body = event.body;
|
|
31683
|
+
const breakpoint = body?.["breakpoint"];
|
|
31684
|
+
const source = typeof breakpoint === "object" && breakpoint !== null ? breakpoint["source"] : undefined;
|
|
31685
|
+
const path = typeof source === "object" && source !== null ? source["path"] : undefined;
|
|
31686
|
+
const line = typeof breakpoint === "object" && breakpoint !== null ? breakpoint["line"] : undefined;
|
|
31687
|
+
if (typeof path !== "string" || typeof line !== "number") {
|
|
31688
|
+
return;
|
|
31689
|
+
}
|
|
31690
|
+
if (said(body, "reason") === "removed") {
|
|
31691
|
+
this.inEditor.get(path)?.delete(line);
|
|
31692
|
+
const held = this.breakpoints.get(path);
|
|
31693
|
+
if (held?.delete(line) === true) {
|
|
31694
|
+
if (held.size === 0) {
|
|
31695
|
+
this.breakpoints.delete(path);
|
|
31696
|
+
}
|
|
31697
|
+
this.onBreakpointsChanged?.(this.breakpointsHeld());
|
|
31698
|
+
}
|
|
31699
|
+
return;
|
|
31700
|
+
}
|
|
31701
|
+
const known = this.inEditor.get(path) ?? new Set;
|
|
31702
|
+
known.add(line);
|
|
31703
|
+
this.inEditor.set(path, known);
|
|
31601
31704
|
return;
|
|
31602
31705
|
}
|
|
31603
31706
|
if (eventName === "terminated" || eventName === "exited") {
|
|
@@ -31636,6 +31739,43 @@ class GodotDAPClient {
|
|
|
31636
31739
|
await this.sendRequest("attach", {});
|
|
31637
31740
|
await this.sendRequest("configurationDone", {});
|
|
31638
31741
|
this.attached = true;
|
|
31742
|
+
if (!this.holdKnown) {
|
|
31743
|
+
await this.learnWhetherHeld();
|
|
31744
|
+
}
|
|
31745
|
+
}
|
|
31746
|
+
async learnWhetherHeld() {
|
|
31747
|
+
try {
|
|
31748
|
+
const threads = await this.sendRequest("threads");
|
|
31749
|
+
const listed = threads["threads"];
|
|
31750
|
+
const first = Array.isArray(listed) ? listed[0]?.["id"] : undefined;
|
|
31751
|
+
if (typeof first !== "number") {
|
|
31752
|
+
return;
|
|
31753
|
+
}
|
|
31754
|
+
this.lastThreadId = first;
|
|
31755
|
+
const response = await this.sendRequest("stackTrace", { threadId: first, startFrame: 0, levels: 1 });
|
|
31756
|
+
if (this.holdKnown) {
|
|
31757
|
+
return;
|
|
31758
|
+
}
|
|
31759
|
+
const frames = response["stackFrames"];
|
|
31760
|
+
if (Array.isArray(frames) && frames.length > 0) {
|
|
31761
|
+
this.halt = {
|
|
31762
|
+
reason: "attached",
|
|
31763
|
+
description: "held when this session attached; the stop that held it was reported to an earlier one",
|
|
31764
|
+
text: ""
|
|
31765
|
+
};
|
|
31766
|
+
this.holdKnown = true;
|
|
31767
|
+
}
|
|
31768
|
+
} catch {}
|
|
31769
|
+
}
|
|
31770
|
+
holdIsKnown() {
|
|
31771
|
+
return this.holdKnown;
|
|
31772
|
+
}
|
|
31773
|
+
learnedRunning() {
|
|
31774
|
+
if (this.holdKnown) {
|
|
31775
|
+
return;
|
|
31776
|
+
}
|
|
31777
|
+
this.halt = null;
|
|
31778
|
+
this.holdKnown = true;
|
|
31639
31779
|
}
|
|
31640
31780
|
setOutputSink(sink) {
|
|
31641
31781
|
this.onOutput = sink;
|
|
@@ -31648,38 +31788,96 @@ class GodotDAPClient {
|
|
|
31648
31788
|
return lines;
|
|
31649
31789
|
}
|
|
31650
31790
|
async setBreakpoint(filePath, line) {
|
|
31651
|
-
|
|
31652
|
-
|
|
31653
|
-
|
|
31654
|
-
fileBreakpoints.add(line);
|
|
31655
|
-
this.breakpoints.set(filePath, fileBreakpoints);
|
|
31656
|
-
const lines = Array.from(fileBreakpoints).sort((a, b) => a - b);
|
|
31657
|
-
return await this.sendRequest("setBreakpoints", {
|
|
31658
|
-
source: { path: filePath },
|
|
31659
|
-
breakpoints: lines.map((breakpointLine) => ({ line: breakpointLine }))
|
|
31660
|
-
});
|
|
31791
|
+
const wanted = new Set(this.breakpoints.get(filePath) ?? []);
|
|
31792
|
+
wanted.add(line);
|
|
31793
|
+
return await this.sendBreakpoints(filePath, wanted);
|
|
31661
31794
|
}
|
|
31662
31795
|
async removeBreakpoint(filePath, line) {
|
|
31796
|
+
const wanted = new Set(this.breakpoints.get(filePath) ?? []);
|
|
31797
|
+
wanted.delete(line);
|
|
31798
|
+
return await this.sendBreakpoints(filePath, wanted, line);
|
|
31799
|
+
}
|
|
31800
|
+
async sendBreakpoints(filePath, mine, clearing) {
|
|
31663
31801
|
await this.ensureConnected();
|
|
31664
31802
|
await this.initialize();
|
|
31665
|
-
const
|
|
31666
|
-
|
|
31667
|
-
|
|
31668
|
-
this.breakpoints.delete(filePath);
|
|
31669
|
-
} else {
|
|
31670
|
-
this.breakpoints.set(filePath, fileBreakpoints);
|
|
31803
|
+
const lines = new Set([...mine, ...this.inEditor.get(filePath) ?? []]);
|
|
31804
|
+
if (clearing !== undefined) {
|
|
31805
|
+
lines.delete(clearing);
|
|
31671
31806
|
}
|
|
31672
|
-
const
|
|
31673
|
-
|
|
31807
|
+
const sorted = Array.from(lines).sort((a, b) => a - b);
|
|
31808
|
+
const answer = await this.sendRequest("setBreakpoints", {
|
|
31674
31809
|
source: { path: filePath },
|
|
31675
|
-
breakpoints:
|
|
31810
|
+
breakpoints: sorted.map((breakpointLine) => ({ line: breakpointLine }))
|
|
31676
31811
|
});
|
|
31812
|
+
if (mine.size === 0) {
|
|
31813
|
+
this.breakpoints.delete(filePath);
|
|
31814
|
+
} else {
|
|
31815
|
+
this.breakpoints.set(filePath, new Set(mine));
|
|
31816
|
+
}
|
|
31817
|
+
if (lines.size === 0) {
|
|
31818
|
+
this.inEditor.delete(filePath);
|
|
31819
|
+
} else {
|
|
31820
|
+
this.inEditor.set(filePath, lines);
|
|
31821
|
+
}
|
|
31822
|
+
return answer;
|
|
31823
|
+
}
|
|
31824
|
+
setBreakpointsSink(sink) {
|
|
31825
|
+
this.onBreakpointsChanged = sink;
|
|
31826
|
+
}
|
|
31827
|
+
breakpointsInEditor() {
|
|
31828
|
+
const theirs = [];
|
|
31829
|
+
for (const [scriptPath, lines] of this.inEditor) {
|
|
31830
|
+
const mine = this.breakpoints.get(scriptPath);
|
|
31831
|
+
const rest = Array.from(lines).filter((line) => !mine?.has(line)).sort((a, b) => a - b);
|
|
31832
|
+
if (rest.length > 0) {
|
|
31833
|
+
theirs.push({ scriptPath, lines: rest });
|
|
31834
|
+
}
|
|
31835
|
+
}
|
|
31836
|
+
return theirs;
|
|
31837
|
+
}
|
|
31838
|
+
async reapplyBreakpoints() {
|
|
31839
|
+
const applied = [];
|
|
31840
|
+
const refused = [];
|
|
31841
|
+
for (const [filePath, lines] of Array.from(this.breakpoints)) {
|
|
31842
|
+
try {
|
|
31843
|
+
await this.sendBreakpoints(filePath, lines);
|
|
31844
|
+
applied.push({ scriptPath: filePath, lines: Array.from(lines).sort((a, b) => a - b) });
|
|
31845
|
+
} catch (error) {
|
|
31846
|
+
this.breakpoints.delete(filePath);
|
|
31847
|
+
refused.push({
|
|
31848
|
+
scriptPath: filePath,
|
|
31849
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
31850
|
+
});
|
|
31851
|
+
}
|
|
31852
|
+
}
|
|
31853
|
+
if (refused.length > 0) {
|
|
31854
|
+
this.onBreakpointsChanged?.(this.breakpointsHeld());
|
|
31855
|
+
}
|
|
31856
|
+
return { applied, refused };
|
|
31857
|
+
}
|
|
31858
|
+
breakpointsHeld() {
|
|
31859
|
+
return Array.from(this.breakpoints, ([scriptPath, lines]) => ({
|
|
31860
|
+
scriptPath,
|
|
31861
|
+
lines: Array.from(lines).sort((a, b) => a - b)
|
|
31862
|
+
}));
|
|
31863
|
+
}
|
|
31864
|
+
holdBreakpoints(held) {
|
|
31865
|
+
for (const { scriptPath, lines } of held) {
|
|
31866
|
+
const fileBreakpoints = this.breakpoints.get(scriptPath) ?? new Set;
|
|
31867
|
+
for (const line of lines) {
|
|
31868
|
+
fileBreakpoints.add(line);
|
|
31869
|
+
}
|
|
31870
|
+
if (fileBreakpoints.size > 0) {
|
|
31871
|
+
this.breakpoints.set(scriptPath, fileBreakpoints);
|
|
31872
|
+
}
|
|
31873
|
+
}
|
|
31677
31874
|
}
|
|
31678
31875
|
async continue(threadId) {
|
|
31679
31876
|
await this.attach();
|
|
31680
31877
|
const resolvedThreadId = await this.resolveThreadId(threadId);
|
|
31681
31878
|
await this.sendRequest("continue", { threadId: resolvedThreadId });
|
|
31682
31879
|
this.halt = null;
|
|
31880
|
+
this.holdKnown = true;
|
|
31683
31881
|
}
|
|
31684
31882
|
async stepOver(threadId) {
|
|
31685
31883
|
await this.attach();
|
|
@@ -31778,11 +31976,7 @@ class GodotDAPClient {
|
|
|
31778
31976
|
}
|
|
31779
31977
|
failOversizedStream(detail) {
|
|
31780
31978
|
const socket = this.socket;
|
|
31781
|
-
this.
|
|
31782
|
-
this.connected = false;
|
|
31783
|
-
this.initialized = false;
|
|
31784
|
-
this.attached = false;
|
|
31785
|
-
this.halt = null;
|
|
31979
|
+
this.forgetConnection();
|
|
31786
31980
|
this.reader = new FrameReader;
|
|
31787
31981
|
socket?.destroy();
|
|
31788
31982
|
this.failPendingRequests(new Error(`Godot DAP ${detail}. The connection was dropped.`));
|
|
@@ -31861,14 +32055,10 @@ async function handleDAPTool(client, toolName, args) {
|
|
|
31861
32055
|
}
|
|
31862
32056
|
} catch (error) {
|
|
31863
32057
|
const message = error instanceof Error ? error.message : String(error);
|
|
32058
|
+
const cure = toolName === "dap_set_breakpoint" || toolName === "dap_remove_breakpoint" ? "Breakpoints need the editor open, not a running game: editor_status says whether one is connected, and the path is the script as the project spells it." : "The debug tools answer for a game the editor is playing: start one with editor_run.";
|
|
31864
32059
|
return {
|
|
31865
32060
|
isError: true,
|
|
31866
|
-
content: [
|
|
31867
|
-
{
|
|
31868
|
-
type: "text",
|
|
31869
|
-
text: `DAP tool '${toolName}' failed: ${message}. ` + "The debug tools answer for a game the editor is playing: start one with editor_run."
|
|
31870
|
-
}
|
|
31871
|
-
]
|
|
32061
|
+
content: [{ type: "text", text: `DAP tool '${toolName}' failed: ${message}. ${cure}` }]
|
|
31872
32062
|
};
|
|
31873
32063
|
}
|
|
31874
32064
|
}
|
|
@@ -34945,6 +35135,7 @@ var init_godot_bridge = __esm(() => {
|
|
|
34945
35135
|
dapPort: this.connectionInfo?.dapPort,
|
|
34946
35136
|
debugPort: this.connectionInfo?.debugPort,
|
|
34947
35137
|
openedByAServer: this.connectionInfo?.openedByAServer,
|
|
35138
|
+
syncsBreakpoints: this.connectionInfo?.syncsBreakpoints,
|
|
34948
35139
|
pendingRequests: this.pendingRequests.size,
|
|
34949
35140
|
queuedResources: this.resourceQueues.size,
|
|
34950
35141
|
listeningSince: this.listeningSince ?? undefined
|
|
@@ -35087,6 +35278,7 @@ var init_godot_bridge = __esm(() => {
|
|
|
35087
35278
|
this.connectionInfo.dapPort = servedPort(message.dap_port);
|
|
35088
35279
|
this.connectionInfo.debugPort = servedPort(message.debug_port);
|
|
35089
35280
|
this.connectionInfo.openedByAServer = message.opened_by_a_server === true;
|
|
35281
|
+
this.connectionInfo.syncsBreakpoints = typeof message.syncs_breakpoints === "boolean" ? message.syncs_breakpoints : undefined;
|
|
35090
35282
|
this.log("info", `Godot ready: ${message.project_path}`);
|
|
35091
35283
|
this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
|
|
35092
35284
|
}
|
|
@@ -35688,7 +35880,7 @@ var init_junit = __esm(() => {
|
|
|
35688
35880
|
import { realpathSync as realpathSync2 } from "node:fs";
|
|
35689
35881
|
import { readFile, realpath } from "node:fs/promises";
|
|
35690
35882
|
import { createConnection as createConnection2 } from "node:net";
|
|
35691
|
-
import { dirname as
|
|
35883
|
+
import { dirname as dirname9, resolve as resolve3 } from "node:path";
|
|
35692
35884
|
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
|
|
35693
35885
|
function diagnosticsKey(uri) {
|
|
35694
35886
|
let decoded;
|
|
@@ -35816,7 +36008,7 @@ class GodotLSPClient {
|
|
|
35816
36008
|
if (this.initialized) {
|
|
35817
36009
|
return;
|
|
35818
36010
|
}
|
|
35819
|
-
const rootPath = this.rootPath ??
|
|
36011
|
+
const rootPath = this.rootPath ?? dirname9(resolve3(filePath));
|
|
35820
36012
|
await this.initialize(rootPath);
|
|
35821
36013
|
}
|
|
35822
36014
|
async sendRequest(method, params) {
|
|
@@ -36261,13 +36453,13 @@ var init_process_time = __esm(() => {
|
|
|
36261
36453
|
});
|
|
36262
36454
|
|
|
36263
36455
|
// src/project-scan.ts
|
|
36264
|
-
import { existsSync as existsSync12, readdirSync as readdirSync4, readFileSync as
|
|
36265
|
-
import { join as
|
|
36456
|
+
import { existsSync as existsSync12, readdirSync as readdirSync4, readFileSync as readFileSync13 } from "node:fs";
|
|
36457
|
+
import { join as join15 } from "node:path";
|
|
36266
36458
|
function skipped(name) {
|
|
36267
36459
|
return name.startsWith(".") || SKIPPED.has(name);
|
|
36268
36460
|
}
|
|
36269
36461
|
function steppedOver(directory) {
|
|
36270
|
-
return existsSync12(
|
|
36462
|
+
return existsSync12(join15(directory, ".gdignore"));
|
|
36271
36463
|
}
|
|
36272
36464
|
function projectStructure(projectPath) {
|
|
36273
36465
|
const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
|
|
@@ -36280,7 +36472,7 @@ function projectStructure(projectPath) {
|
|
|
36280
36472
|
continue;
|
|
36281
36473
|
}
|
|
36282
36474
|
if (entry.isDirectory()) {
|
|
36283
|
-
visit(
|
|
36475
|
+
visit(join15(directory, entry.name));
|
|
36284
36476
|
} else if (entry.isFile()) {
|
|
36285
36477
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
36286
36478
|
if (extension === "tscn") {
|
|
@@ -36310,14 +36502,14 @@ function scriptsWithoutUid(projectPath) {
|
|
|
36310
36502
|
}
|
|
36311
36503
|
const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
36312
36504
|
if (entry.isDirectory()) {
|
|
36313
|
-
visit(
|
|
36505
|
+
visit(join15(directory, entry.name), spelled);
|
|
36314
36506
|
continue;
|
|
36315
36507
|
}
|
|
36316
36508
|
if (!entry.isFile()) {
|
|
36317
36509
|
continue;
|
|
36318
36510
|
}
|
|
36319
36511
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
36320
|
-
if (CARRIES_A_UID.has(extension) && !existsSync12(
|
|
36512
|
+
if (CARRIES_A_UID.has(extension) && !existsSync12(join15(directory, `${entry.name}.uid`))) {
|
|
36321
36513
|
found.push(spelled);
|
|
36322
36514
|
}
|
|
36323
36515
|
}
|
|
@@ -36352,7 +36544,7 @@ function searchProject(projectPath, options) {
|
|
|
36352
36544
|
if (skipped(entry.name)) {
|
|
36353
36545
|
continue;
|
|
36354
36546
|
}
|
|
36355
|
-
const entryPath =
|
|
36547
|
+
const entryPath = join15(directory, entry.name);
|
|
36356
36548
|
if (entry.isDirectory()) {
|
|
36357
36549
|
visit(entryPath);
|
|
36358
36550
|
continue;
|
|
@@ -36363,7 +36555,7 @@ function searchProject(projectPath, options) {
|
|
|
36363
36555
|
}
|
|
36364
36556
|
result.summary.files_searched += 1;
|
|
36365
36557
|
const matches = [];
|
|
36366
|
-
for (const [index, line] of
|
|
36558
|
+
for (const [index, line] of readFileSync13(entryPath, "utf8").split(`
|
|
36367
36559
|
`).entries()) {
|
|
36368
36560
|
if (full()) {
|
|
36369
36561
|
break;
|
|
@@ -36392,16 +36584,16 @@ var init_project_scan = __esm(() => {
|
|
|
36392
36584
|
});
|
|
36393
36585
|
|
|
36394
36586
|
// src/restart-note.ts
|
|
36395
|
-
import { mkdirSync as
|
|
36396
|
-
import { dirname as
|
|
36587
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync14, rmSync as rmSync8, writeFileSync as writeFileSync10 } from "node:fs";
|
|
36588
|
+
import { dirname as dirname10, join as join16 } from "node:path";
|
|
36397
36589
|
function restartNotePath(projectPath) {
|
|
36398
|
-
return
|
|
36590
|
+
return join16(projectPath, ".godot", "gdharness-restart.json");
|
|
36399
36591
|
}
|
|
36400
36592
|
function noteRestartBegun(note) {
|
|
36401
36593
|
const path = restartNotePath(note.projectPath);
|
|
36402
36594
|
try {
|
|
36403
|
-
|
|
36404
|
-
|
|
36595
|
+
mkdirSync8(dirname10(path), { recursive: true });
|
|
36596
|
+
writeFileSync10(path, `${JSON.stringify(note, null, 2)}
|
|
36405
36597
|
`, "utf8");
|
|
36406
36598
|
} catch (error) {
|
|
36407
36599
|
console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
|
|
@@ -36409,7 +36601,7 @@ function noteRestartBegun(note) {
|
|
|
36409
36601
|
}
|
|
36410
36602
|
function restartOwed(projectPath) {
|
|
36411
36603
|
try {
|
|
36412
|
-
const parsed = JSON.parse(
|
|
36604
|
+
const parsed = JSON.parse(readFileSync14(restartNotePath(projectPath), "utf8"));
|
|
36413
36605
|
if (typeof parsed !== "object" || parsed === null) {
|
|
36414
36606
|
return null;
|
|
36415
36607
|
}
|
|
@@ -36424,7 +36616,7 @@ function restartOwed(projectPath) {
|
|
|
36424
36616
|
}
|
|
36425
36617
|
function restartSettled(projectPath) {
|
|
36426
36618
|
try {
|
|
36427
|
-
|
|
36619
|
+
rmSync8(restartNotePath(projectPath), { force: true });
|
|
36428
36620
|
} catch (error) {
|
|
36429
36621
|
console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
|
|
36430
36622
|
}
|
|
@@ -36434,10 +36626,10 @@ var init_restart_note = __esm(() => {
|
|
|
36434
36626
|
});
|
|
36435
36627
|
|
|
36436
36628
|
// src/runtime-client.ts
|
|
36437
|
-
import { existsSync as existsSync13, readdirSync as readdirSync5, readFileSync as
|
|
36629
|
+
import { existsSync as existsSync13, readdirSync as readdirSync5, readFileSync as readFileSync15, unlinkSync } from "node:fs";
|
|
36438
36630
|
import { createConnection as createConnection3 } from "node:net";
|
|
36439
36631
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
36440
|
-
import { join as
|
|
36632
|
+
import { join as join17, resolve as resolve4 } from "node:path";
|
|
36441
36633
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
36442
36634
|
function runtimeDirectory(variables = process.env) {
|
|
36443
36635
|
const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
|
|
@@ -36445,13 +36637,13 @@ function runtimeDirectory(variables = process.env) {
|
|
|
36445
36637
|
return explicit;
|
|
36446
36638
|
}
|
|
36447
36639
|
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
36448
|
-
return
|
|
36640
|
+
return join17(perUser ?? tmpdir4(), "gdharness");
|
|
36449
36641
|
}
|
|
36450
36642
|
function runtimeDirectories(variables = process.env) {
|
|
36451
36643
|
const candidates = [runtimeDirectory(variables)];
|
|
36452
36644
|
const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
|
|
36453
36645
|
for (const base of fallbacks) {
|
|
36454
|
-
candidates.push(
|
|
36646
|
+
candidates.push(join17(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
|
|
36455
36647
|
}
|
|
36456
36648
|
return [...new Set(candidates.map((path) => resolve4(path)))];
|
|
36457
36649
|
}
|
|
@@ -36466,7 +36658,7 @@ function processAlive(pid) {
|
|
|
36466
36658
|
function parseAnnouncement(file, pid) {
|
|
36467
36659
|
let fields;
|
|
36468
36660
|
try {
|
|
36469
|
-
fields = asParams(JSON.parse(
|
|
36661
|
+
fields = asParams(JSON.parse(readFileSync15(file, "utf8")));
|
|
36470
36662
|
} catch {
|
|
36471
36663
|
return { kind: "rubbish" };
|
|
36472
36664
|
}
|
|
@@ -36507,7 +36699,7 @@ function discoverRuntimes(directories = runtimeDirectories()) {
|
|
|
36507
36699
|
function wentAway(file, pid) {
|
|
36508
36700
|
let project = "";
|
|
36509
36701
|
try {
|
|
36510
|
-
const fields = asParams(JSON.parse(
|
|
36702
|
+
const fields = asParams(JSON.parse(readFileSync15(file, "utf8")));
|
|
36511
36703
|
project = readString(readParams(fields, "project") ?? {}, "path") ?? "";
|
|
36512
36704
|
} catch {
|
|
36513
36705
|
return;
|
|
@@ -36536,7 +36728,7 @@ function announcedIn(directory) {
|
|
|
36536
36728
|
if (!match) {
|
|
36537
36729
|
continue;
|
|
36538
36730
|
}
|
|
36539
|
-
const file =
|
|
36731
|
+
const file = join17(directory, entry);
|
|
36540
36732
|
const pid = Number.parseInt(match[1] ?? "", 10);
|
|
36541
36733
|
const alive = processAlive(pid);
|
|
36542
36734
|
const announced = alive ? parseAnnouncement(file, pid) : { kind: "rubbish" };
|
|
@@ -36577,7 +36769,25 @@ function tooNew(unspoken) {
|
|
|
36577
36769
|
const half = unspoken.some((game) => game.protocol > RUNTIME_PROTOCOL) ? "this server is the older half: reconnect it so it spawns the installed version" : "the addon is the older half: reinstall it and restart the game";
|
|
36578
36770
|
return `A game is running, in a protocol this server does not speak ${named}. This server speaks ${RUNTIME_PROTOCOL}, so ${half}.`;
|
|
36579
36771
|
}
|
|
36580
|
-
function chooseRuntime(endpoints, projectPath, unspoken = []) {
|
|
36772
|
+
function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
|
|
36773
|
+
if (pid !== undefined) {
|
|
36774
|
+
const named = endpoints.find((endpoint) => endpoint.pid === pid);
|
|
36775
|
+
if (named !== undefined) {
|
|
36776
|
+
if (projectPath !== undefined && resolve4(named.project.path) !== resolve4(projectPath)) {
|
|
36777
|
+
return {
|
|
36778
|
+
problem: `The game with pid ${pid} is running ${named.project.path}, not ${resolve4(projectPath)}. Running: ${endpoints.map(describe2).join("; ")}.`
|
|
36779
|
+
};
|
|
36780
|
+
}
|
|
36781
|
+
return { endpoint: named };
|
|
36782
|
+
}
|
|
36783
|
+
const tooNewToo = unspoken.find((game) => game.pid === pid);
|
|
36784
|
+
if (tooNewToo !== undefined) {
|
|
36785
|
+
return { problem: tooNew([tooNewToo]) };
|
|
36786
|
+
}
|
|
36787
|
+
return {
|
|
36788
|
+
problem: endpoints.length === 0 ? `No game with the runtime addon is running, so there is none with pid ${pid}. Start one with editor_run, or play the project from the editor with the addon enabled.` : `No running game has pid ${pid}. Running: ${endpoints.map(describe2).join("; ")}. editor_status lists them under runtimes as they come and go.`
|
|
36789
|
+
};
|
|
36790
|
+
}
|
|
36581
36791
|
if (endpoints.length === 0) {
|
|
36582
36792
|
if (unspoken.length > 0) {
|
|
36583
36793
|
return { problem: tooNew(unspoken) };
|
|
@@ -36608,14 +36818,14 @@ function chooseRuntime(endpoints, projectPath, unspoken = []) {
|
|
|
36608
36818
|
return { endpoint: matching[0] };
|
|
36609
36819
|
}
|
|
36610
36820
|
return {
|
|
36611
|
-
problem: `Several games are running from ${wanted}: ${matching.map(describe2).join("; ")}.
|
|
36821
|
+
problem: `Several games are running from ${wanted}: ${matching.map(describe2).join("; ")}. Pass pid to choose one.`
|
|
36612
36822
|
};
|
|
36613
36823
|
}
|
|
36614
36824
|
if (endpoints.length === 1 && endpoints[0]) {
|
|
36615
36825
|
return { endpoint: endpoints[0] };
|
|
36616
36826
|
}
|
|
36617
36827
|
return {
|
|
36618
|
-
problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass projectPath to choose one.`
|
|
36828
|
+
problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass projectPath to choose one, or pid when they are from one project.`
|
|
36619
36829
|
};
|
|
36620
36830
|
}
|
|
36621
36831
|
function runtimeRequest(endpoint, command, params, timeoutMs) {
|
|
@@ -36713,36 +36923,36 @@ var init_runtime_client = __esm(() => {
|
|
|
36713
36923
|
|
|
36714
36924
|
// src/run-record.ts
|
|
36715
36925
|
import { execFileSync } from "node:child_process";
|
|
36716
|
-
import { mkdirSync as
|
|
36717
|
-
import { basename as basename2, join as
|
|
36926
|
+
import { mkdirSync as mkdirSync9, openSync, readdirSync as readdirSync6, readFileSync as readFileSync16, rmSync as rmSync9, statSync as statSync3, writeFileSync as writeFileSync11 } from "node:fs";
|
|
36927
|
+
import { basename as basename2, join as join18 } from "node:path";
|
|
36718
36928
|
function runsDirectory() {
|
|
36719
|
-
return
|
|
36929
|
+
return join18(runtimeDirectory(), "runs");
|
|
36720
36930
|
}
|
|
36721
36931
|
function recordPath() {
|
|
36722
|
-
return
|
|
36932
|
+
return join18(runsDirectory(), "run.json");
|
|
36723
36933
|
}
|
|
36724
36934
|
function recordPaths() {
|
|
36725
|
-
return runtimeDirectories().map((directory) =>
|
|
36935
|
+
return runtimeDirectories().map((directory) => join18(directory, "runs", "run.json"));
|
|
36726
36936
|
}
|
|
36727
36937
|
function openTranscript(startedAt) {
|
|
36728
|
-
|
|
36729
|
-
const path =
|
|
36938
|
+
mkdirSync9(runsDirectory(), { recursive: true });
|
|
36939
|
+
const path = join18(runsDirectory(), `run-${startedAt}.log`);
|
|
36730
36940
|
return { path, fd: openSync(path, "a") };
|
|
36731
36941
|
}
|
|
36732
36942
|
function writeRunRecord(record) {
|
|
36733
|
-
|
|
36734
|
-
|
|
36943
|
+
mkdirSync9(runsDirectory(), { recursive: true });
|
|
36944
|
+
writeFileSync11(recordPath(), JSON.stringify(record, null, 2), "utf8");
|
|
36735
36945
|
}
|
|
36736
36946
|
function editorNotePath() {
|
|
36737
|
-
return
|
|
36947
|
+
return join18(runsDirectory(), "editor-run.json");
|
|
36738
36948
|
}
|
|
36739
36949
|
function writeEditorRunNote(note) {
|
|
36740
|
-
|
|
36741
|
-
|
|
36950
|
+
mkdirSync9(runsDirectory(), { recursive: true });
|
|
36951
|
+
writeFileSync11(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
|
|
36742
36952
|
}
|
|
36743
36953
|
function readEditorRunNote() {
|
|
36744
36954
|
for (const directory of runtimeDirectories()) {
|
|
36745
|
-
const note = editorNoteAt(
|
|
36955
|
+
const note = editorNoteAt(join18(directory, "runs", "editor-run.json"));
|
|
36746
36956
|
if (note !== null) {
|
|
36747
36957
|
return note;
|
|
36748
36958
|
}
|
|
@@ -36752,7 +36962,7 @@ function readEditorRunNote() {
|
|
|
36752
36962
|
function editorNoteAt(path) {
|
|
36753
36963
|
let parsed;
|
|
36754
36964
|
try {
|
|
36755
|
-
parsed = JSON.parse(
|
|
36965
|
+
parsed = JSON.parse(readFileSync16(path, "utf8"));
|
|
36756
36966
|
} catch {
|
|
36757
36967
|
return null;
|
|
36758
36968
|
}
|
|
@@ -36775,7 +36985,7 @@ function recordRunEnded(pid, exitCode) {
|
|
|
36775
36985
|
return;
|
|
36776
36986
|
}
|
|
36777
36987
|
try {
|
|
36778
|
-
|
|
36988
|
+
writeFileSync11(path, JSON.stringify({ ...record, exitCode }, null, 2), "utf8");
|
|
36779
36989
|
} catch {}
|
|
36780
36990
|
}
|
|
36781
36991
|
function readRunRecord() {
|
|
@@ -36790,7 +37000,7 @@ function readRunRecord() {
|
|
|
36790
37000
|
function recordAt(path) {
|
|
36791
37001
|
let parsed;
|
|
36792
37002
|
try {
|
|
36793
|
-
parsed = JSON.parse(
|
|
37003
|
+
parsed = JSON.parse(readFileSync16(path, "utf8"));
|
|
36794
37004
|
} catch {
|
|
36795
37005
|
return null;
|
|
36796
37006
|
}
|
|
@@ -36835,7 +37045,7 @@ function runningAs(pid) {
|
|
|
36835
37045
|
const began = startedAt(pid, null);
|
|
36836
37046
|
const when = began === null ? {} : { startedAt: began };
|
|
36837
37047
|
if (process.platform === "linux") {
|
|
36838
|
-
const raw =
|
|
37048
|
+
const raw = readFileSync16(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
36839
37049
|
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
36840
37050
|
}
|
|
36841
37051
|
const args = execFileSync("ps", ["-p", String(pid), "-o", "args="], {
|
|
@@ -36955,7 +37165,7 @@ function clearRunRecord(ours = () => true) {
|
|
|
36955
37165
|
continue;
|
|
36956
37166
|
}
|
|
36957
37167
|
try {
|
|
36958
|
-
|
|
37168
|
+
rmSync9(path);
|
|
36959
37169
|
} catch {}
|
|
36960
37170
|
}
|
|
36961
37171
|
}
|
|
@@ -36979,12 +37189,12 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
36979
37189
|
if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
|
|
36980
37190
|
continue;
|
|
36981
37191
|
}
|
|
36982
|
-
const path =
|
|
37192
|
+
const path = join18(directory, entry);
|
|
36983
37193
|
if (path === keep || ageMs(path, now) < keepMs) {
|
|
36984
37194
|
continue;
|
|
36985
37195
|
}
|
|
36986
37196
|
try {
|
|
36987
|
-
|
|
37197
|
+
rmSync9(path);
|
|
36988
37198
|
} catch {}
|
|
36989
37199
|
}
|
|
36990
37200
|
}
|
|
@@ -37002,21 +37212,41 @@ import {
|
|
|
37002
37212
|
closeSync,
|
|
37003
37213
|
existsSync as existsSync14,
|
|
37004
37214
|
fstatSync,
|
|
37005
|
-
mkdirSync as
|
|
37215
|
+
mkdirSync as mkdirSync10,
|
|
37006
37216
|
mkdtempSync as mkdtempSync3,
|
|
37007
37217
|
openSync as openSync2,
|
|
37008
37218
|
readdirSync as readdirSync7,
|
|
37009
|
-
readFileSync as
|
|
37219
|
+
readFileSync as readFileSync17,
|
|
37010
37220
|
readSync
|
|
37011
37221
|
} from "node:fs";
|
|
37012
37222
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
37013
|
-
import { basename as basename3, dirname as
|
|
37223
|
+
import { basename as basename3, dirname as dirname11, join as join19, normalize as normalize3, relative as relative3 } from "node:path";
|
|
37014
37224
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
37015
37225
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
37016
37226
|
import { promisify as promisify6 } from "node:util";
|
|
37017
37227
|
function patienceForFrames(frames, atLeast) {
|
|
37018
37228
|
return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
|
|
37019
37229
|
}
|
|
37230
|
+
function projectSpelling(projectPath, absolutePath) {
|
|
37231
|
+
return `res://${relative3(realPathOr(projectPath), realPathOr(absolutePath)).replaceAll("\\", "/")}`;
|
|
37232
|
+
}
|
|
37233
|
+
function whichGame(args) {
|
|
37234
|
+
return { projectPath: args["projectPath"], pid: args["pid"] };
|
|
37235
|
+
}
|
|
37236
|
+
function describeHalt(halt) {
|
|
37237
|
+
switch (halt.reason) {
|
|
37238
|
+
case "breakpoint":
|
|
37239
|
+
return "at a breakpoint";
|
|
37240
|
+
case "step":
|
|
37241
|
+
return "after a step";
|
|
37242
|
+
case "exception":
|
|
37243
|
+
return halt.text === "" ? "on an error" : `on an error: ${halt.text}`;
|
|
37244
|
+
case "attached":
|
|
37245
|
+
return "held when this session attached";
|
|
37246
|
+
default:
|
|
37247
|
+
return `stopped with reason "${halt.reason}"`;
|
|
37248
|
+
}
|
|
37249
|
+
}
|
|
37020
37250
|
function stillRunning(run) {
|
|
37021
37251
|
if (run?.exitCode !== null || run.endedUnwatched === true) {
|
|
37022
37252
|
return false;
|
|
@@ -37038,6 +37268,19 @@ function runIsUp(run, editorSays) {
|
|
|
37038
37268
|
}
|
|
37039
37269
|
return editorSays ?? stillRunning(run);
|
|
37040
37270
|
}
|
|
37271
|
+
function endedPreviousRun(ended) {
|
|
37272
|
+
if (ended === null) {
|
|
37273
|
+
return;
|
|
37274
|
+
}
|
|
37275
|
+
return ended.pid ?? true;
|
|
37276
|
+
}
|
|
37277
|
+
function endedToStartThis(ended) {
|
|
37278
|
+
if (ended === null) {
|
|
37279
|
+
return "";
|
|
37280
|
+
}
|
|
37281
|
+
const which = ended.pid === null ? "The game the editor was playing" : `The run that was going, pid ${ended.pid},`;
|
|
37282
|
+
return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.`;
|
|
37283
|
+
}
|
|
37041
37284
|
function runtimeVerdict(endpoint, after) {
|
|
37042
37285
|
if (endpoint !== null) {
|
|
37043
37286
|
return { listening: true, pid: endpoint.pid, port: endpoint.port };
|
|
@@ -37165,7 +37408,7 @@ function aboveTheRunner(entry) {
|
|
|
37165
37408
|
}
|
|
37166
37409
|
function elsewhereIn(projectPath, asked) {
|
|
37167
37410
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
37168
|
-
if (wanted === "" || existsSync14(
|
|
37411
|
+
if (wanted === "" || existsSync14(join19(projectPath, wanted))) {
|
|
37169
37412
|
return "";
|
|
37170
37413
|
}
|
|
37171
37414
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
@@ -37179,11 +37422,11 @@ function elsewhereIn(projectPath, asked) {
|
|
|
37179
37422
|
return near.length === 0 ? "" : ` The project does have ${there}.`;
|
|
37180
37423
|
}
|
|
37181
37424
|
function hasMainScene(projectFile) {
|
|
37182
|
-
const scene = parseProjectGodot(
|
|
37425
|
+
const scene = parseProjectGodot(readFileSync17(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
37183
37426
|
return typeof scene === "string" && scene !== "";
|
|
37184
37427
|
}
|
|
37185
37428
|
function editorPlaysHeadless(projectFile) {
|
|
37186
|
-
const runArgs = parseProjectGodot(
|
|
37429
|
+
const runArgs = parseProjectGodot(readFileSync17(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
37187
37430
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
37188
37431
|
}
|
|
37189
37432
|
function camelCased(params) {
|
|
@@ -37199,7 +37442,7 @@ function camelCased(params) {
|
|
|
37199
37442
|
class GodotServer {
|
|
37200
37443
|
mcp;
|
|
37201
37444
|
locator = new GodotLocator2;
|
|
37202
|
-
operationsScript =
|
|
37445
|
+
operationsScript = join19(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
37203
37446
|
godotBridge;
|
|
37204
37447
|
tools = buildToolDefinitions();
|
|
37205
37448
|
activeProcess = null;
|
|
@@ -37211,6 +37454,7 @@ class GodotServer {
|
|
|
37211
37454
|
lastGodotVersion = null;
|
|
37212
37455
|
lspClient = null;
|
|
37213
37456
|
dapClient = null;
|
|
37457
|
+
breakpointsSeededOn = null;
|
|
37214
37458
|
bridgeStartupError = null;
|
|
37215
37459
|
bridgeRetry = null;
|
|
37216
37460
|
successorWatch = null;
|
|
@@ -37735,7 +37979,7 @@ class GodotServer {
|
|
|
37735
37979
|
switch (op) {
|
|
37736
37980
|
case "tree":
|
|
37737
37981
|
return await this.handleRuntimeCommand("get_tree", {
|
|
37738
|
-
|
|
37982
|
+
...whichGame(args),
|
|
37739
37983
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
37740
37984
|
depth: readPositiveNumber(args, "depth") ?? 3,
|
|
37741
37985
|
include_properties: readBoolean(args, "includeProperties") ?? false
|
|
@@ -37744,36 +37988,36 @@ class GodotServer {
|
|
|
37744
37988
|
return await this.handleFindRuntimeNodes(args);
|
|
37745
37989
|
case "text":
|
|
37746
37990
|
return await this.handleRuntimeCommand("read_text", {
|
|
37747
|
-
|
|
37991
|
+
...whichGame(args),
|
|
37748
37992
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
37749
37993
|
include_hidden: readBoolean(args, "includeHidden") ?? false,
|
|
37750
37994
|
limit: readPositiveNumber(args, "limit") ?? 500
|
|
37751
37995
|
});
|
|
37752
37996
|
case "rect":
|
|
37753
37997
|
return await this.handleRuntimeCommand("get_rect", {
|
|
37754
|
-
|
|
37998
|
+
...whichGame(args),
|
|
37755
37999
|
path: readNonEmptyString(args, "nodePath") ?? ""
|
|
37756
38000
|
});
|
|
37757
38001
|
case "property":
|
|
37758
38002
|
return await this.handleRuntimeCommand("get_property", {
|
|
37759
|
-
|
|
38003
|
+
...whichGame(args),
|
|
37760
38004
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
37761
38005
|
property: readNonEmptyString(args, "property") ?? ""
|
|
37762
38006
|
});
|
|
37763
38007
|
default:
|
|
37764
38008
|
return await this.handleRuntimeCommand("get_metrics", {
|
|
37765
|
-
|
|
38009
|
+
...whichGame(args),
|
|
37766
38010
|
metrics: readArray(args, "metrics") ?? []
|
|
37767
38011
|
});
|
|
37768
38012
|
}
|
|
37769
38013
|
case "runtime_invoke":
|
|
37770
38014
|
return op === "set" ? await this.handleRuntimeCommand("set_property", {
|
|
37771
|
-
|
|
38015
|
+
...whichGame(args),
|
|
37772
38016
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
37773
38017
|
property: readString(args, "property") ?? "",
|
|
37774
38018
|
value: args["value"]
|
|
37775
38019
|
}) : await this.handleRuntimeCommand("call_method", {
|
|
37776
|
-
|
|
38020
|
+
...whichGame(args),
|
|
37777
38021
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
37778
38022
|
method: readString(args, "method") ?? "",
|
|
37779
38023
|
args: readArray(args, "args") ?? []
|
|
@@ -37783,7 +38027,7 @@ class GodotServer {
|
|
|
37783
38027
|
case "runtime_input":
|
|
37784
38028
|
if (op === "click") {
|
|
37785
38029
|
return await this.handleRuntimeCommand("click", {
|
|
37786
|
-
|
|
38030
|
+
...whichGame(args),
|
|
37787
38031
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
37788
38032
|
button: readString(args, "button") ?? "left",
|
|
37789
38033
|
double: readBoolean(args, "doubleClick") ?? false
|
|
@@ -37791,7 +38035,7 @@ class GodotServer {
|
|
|
37791
38035
|
}
|
|
37792
38036
|
if (op === "choose") {
|
|
37793
38037
|
return await this.handleRuntimeCommand("choose", {
|
|
37794
|
-
|
|
38038
|
+
...whichGame(args),
|
|
37795
38039
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
37796
38040
|
text: readString(args, "text") ?? "",
|
|
37797
38041
|
...args["index"] === undefined ? {} : { index: args["index"] }
|
|
@@ -37809,17 +38053,30 @@ class GodotServer {
|
|
|
37809
38053
|
if (!located.ok) {
|
|
37810
38054
|
return this.createErrorResponse(located.reason, PATH_SOLUTIONS);
|
|
37811
38055
|
}
|
|
37812
|
-
|
|
38056
|
+
const session = this.dapHoldingBreakpointsOf(project.value.path);
|
|
38057
|
+
const answered = await this.handleDAP(op === "set" ? "dap_set_breakpoint" : "dap_remove_breakpoint", {
|
|
37813
38058
|
...args,
|
|
37814
38059
|
scriptPath: realPathOr(located.absolutePath)
|
|
37815
38060
|
});
|
|
38061
|
+
if (answered.isError === true) {
|
|
38062
|
+
return answered;
|
|
38063
|
+
}
|
|
38064
|
+
const held = session.breakpointsHeld();
|
|
38065
|
+
writeBreakpointNote(project.value.path, held);
|
|
38066
|
+
return this.jsonTextResponse({
|
|
38067
|
+
...asParams(JSON.parse(answered.content[0]?.text ?? "{}")),
|
|
38068
|
+
held: this.breakpointsAsProjectSpells(project.value.path, held),
|
|
38069
|
+
setInEditor: this.breakpointsAsProjectSpells(project.value.path, session.breakpointsInEditor()),
|
|
38070
|
+
note: "The editor keeps a breakpoint set this way for one play. Every breakpoint held here is sent again before each play editor_run starts, and kept in the project for the server after a reconnect.",
|
|
38071
|
+
...this.breakpointsAtRisk()
|
|
38072
|
+
});
|
|
37816
38073
|
}
|
|
37817
38074
|
case "debug_control":
|
|
37818
38075
|
case "debug_state": {
|
|
37819
38076
|
if (tool === "debug_state" && op === "output") {
|
|
37820
38077
|
return await this.handleDAP("dap_get_output", args);
|
|
37821
38078
|
}
|
|
37822
|
-
const held = await this.debuggedGame();
|
|
38079
|
+
const held = await this.debuggedGame(tool === "debug_control" ? "move" : "read");
|
|
37823
38080
|
if (!held.ok) {
|
|
37824
38081
|
return held.response;
|
|
37825
38082
|
}
|
|
@@ -37842,7 +38099,7 @@ class GodotServer {
|
|
|
37842
38099
|
if (path === undefined) {
|
|
37843
38100
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
37844
38101
|
}
|
|
37845
|
-
const file =
|
|
38102
|
+
const file = join19(path, "project.godot");
|
|
37846
38103
|
if (!existsSync14(file)) {
|
|
37847
38104
|
return {
|
|
37848
38105
|
ok: false,
|
|
@@ -37868,7 +38125,7 @@ class GodotServer {
|
|
|
37868
38125
|
}
|
|
37869
38126
|
return { ok: true, value: given };
|
|
37870
38127
|
}
|
|
37871
|
-
async debuggedGame() {
|
|
38128
|
+
async debuggedGame(intent) {
|
|
37872
38129
|
const game = this.activeProcess;
|
|
37873
38130
|
if (game !== null && game.exitCode !== null) {
|
|
37874
38131
|
return {
|
|
@@ -37885,6 +38142,16 @@ class GodotServer {
|
|
|
37885
38142
|
response: this.createErrorResponse("No game is running, so there is no debug session to answer for.", ["Start one with editor_run start, which has the editor play it"])
|
|
37886
38143
|
};
|
|
37887
38144
|
}
|
|
38145
|
+
const going = await this.runStillGoing(game);
|
|
38146
|
+
if (!going) {
|
|
38147
|
+
return {
|
|
38148
|
+
ok: false,
|
|
38149
|
+
response: this.createErrorResponse("The last run has already ended, so there is no debug session to answer for.", [
|
|
38150
|
+
"editor_output still reads what it printed, until the next run starts",
|
|
38151
|
+
"editor_run start plays another, which the debugger can hold"
|
|
38152
|
+
])
|
|
38153
|
+
};
|
|
38154
|
+
}
|
|
37888
38155
|
if (!game.throughEditor) {
|
|
37889
38156
|
return {
|
|
37890
38157
|
ok: false,
|
|
@@ -37906,6 +38173,19 @@ class GodotServer {
|
|
|
37906
38173
|
};
|
|
37907
38174
|
}
|
|
37908
38175
|
if (!this.dap().isStopped()) {
|
|
38176
|
+
const hold = await this.holdOf(game, going);
|
|
38177
|
+
if (hold.heldAt !== null) {
|
|
38178
|
+
if (intent === "move") {
|
|
38179
|
+
return { ok: true, value: game };
|
|
38180
|
+
}
|
|
38181
|
+
return {
|
|
38182
|
+
ok: false,
|
|
38183
|
+
response: this.createErrorResponse(hold.heldAt === undefined ? `Whether the game is held cannot be told from here, so there is no stack to read. ${hold.heldNote ?? ""}` : `The game is held, and this server cannot read its stack: ${hold.heldAt.description}.`, [
|
|
38184
|
+
"debug_control continue lets a held game go, and the next stop is one this session is told about",
|
|
38185
|
+
"editor_output says what it printed on the way to where it is"
|
|
38186
|
+
])
|
|
38187
|
+
};
|
|
38188
|
+
}
|
|
37909
38189
|
return {
|
|
37910
38190
|
ok: false,
|
|
37911
38191
|
response: this.createErrorResponse("The game is running, not stopped, so it has no stack and no scope to read.", [
|
|
@@ -38043,7 +38323,7 @@ class GodotServer {
|
|
|
38043
38323
|
if (!engine.ok) {
|
|
38044
38324
|
return engine.response;
|
|
38045
38325
|
}
|
|
38046
|
-
const application = parseProjectGodot(
|
|
38326
|
+
const application = parseProjectGodot(readFileSync17(project.value.file, "utf8"))["application"] ?? {};
|
|
38047
38327
|
const name = application["config/name"];
|
|
38048
38328
|
const mainScene = application["run/main_scene"];
|
|
38049
38329
|
const info = {
|
|
@@ -38104,18 +38384,18 @@ class GodotServer {
|
|
|
38104
38384
|
if (!engine.ok) {
|
|
38105
38385
|
return engine.response;
|
|
38106
38386
|
}
|
|
38107
|
-
const folder =
|
|
38387
|
+
const folder = dirname11(output.absolutePath);
|
|
38108
38388
|
try {
|
|
38109
|
-
|
|
38389
|
+
mkdirSync10(folder, { recursive: true });
|
|
38110
38390
|
} catch (error) {
|
|
38111
|
-
return this.createErrorResponse(`Cannot create ${
|
|
38391
|
+
return this.createErrorResponse(`Cannot create ${dirname11(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
38112
38392
|
}
|
|
38113
38393
|
const debug = readBoolean(args, "debug") ?? false;
|
|
38114
|
-
const exportLogs = mkdtempSync3(
|
|
38394
|
+
const exportLogs = mkdtempSync3(join19(tmpdir5(), "gdharness-export-"));
|
|
38115
38395
|
const exportArgs = [
|
|
38116
38396
|
"--headless",
|
|
38117
38397
|
"--log-file",
|
|
38118
|
-
|
|
38398
|
+
join19(exportLogs, "engine.log"),
|
|
38119
38399
|
"--path",
|
|
38120
38400
|
project.value.path,
|
|
38121
38401
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -38173,7 +38453,7 @@ class GodotServer {
|
|
|
38173
38453
|
return contained.response;
|
|
38174
38454
|
}
|
|
38175
38455
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
38176
|
-
if (!existsSync14(
|
|
38456
|
+
if (!existsSync14(join19(project.value.path, runner))) {
|
|
38177
38457
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
38178
38458
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
38179
38459
|
]);
|
|
@@ -38208,7 +38488,7 @@ class GodotServer {
|
|
|
38208
38488
|
];
|
|
38209
38489
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
38210
38490
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
38211
|
-
const userData = mkdtempSync3(
|
|
38491
|
+
const userData = mkdtempSync3(join19(tmpdir5(), "gdharness-tests-"));
|
|
38212
38492
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
38213
38493
|
const hung = await new Promise((resolve) => {
|
|
38214
38494
|
const timer = setTimeout(() => {
|
|
@@ -38224,14 +38504,14 @@ class GodotServer {
|
|
|
38224
38504
|
resolve(false);
|
|
38225
38505
|
});
|
|
38226
38506
|
});
|
|
38227
|
-
const reportsDir =
|
|
38507
|
+
const reportsDir = join19(project.value.path, ".godot", ...ours.split("/"));
|
|
38228
38508
|
let report = null;
|
|
38229
38509
|
let reportProblem = null;
|
|
38230
38510
|
try {
|
|
38231
38511
|
const written = existsSync14(reportsDir) ? readdirSync7(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
38232
38512
|
const newest = written.at(-1);
|
|
38233
38513
|
if (newest !== undefined) {
|
|
38234
|
-
report = parseJUnit(
|
|
38514
|
+
report = parseJUnit(readFileSync17(join19(reportsDir, newest, "results.xml"), "utf8"));
|
|
38235
38515
|
}
|
|
38236
38516
|
} catch (error) {
|
|
38237
38517
|
reportProblem = errorMessage(error);
|
|
@@ -38371,7 +38651,7 @@ class GodotServer {
|
|
|
38371
38651
|
return null;
|
|
38372
38652
|
}
|
|
38373
38653
|
try {
|
|
38374
|
-
return
|
|
38654
|
+
return readFileSync17(contained.absolutePath, "utf8");
|
|
38375
38655
|
} catch {
|
|
38376
38656
|
return null;
|
|
38377
38657
|
}
|
|
@@ -38413,7 +38693,7 @@ class GodotServer {
|
|
|
38413
38693
|
return null;
|
|
38414
38694
|
}
|
|
38415
38695
|
try {
|
|
38416
|
-
return
|
|
38696
|
+
return readFileSync17(contained.absolutePath, "utf8");
|
|
38417
38697
|
} catch {
|
|
38418
38698
|
return null;
|
|
38419
38699
|
}
|
|
@@ -38459,9 +38739,38 @@ class GodotServer {
|
|
|
38459
38739
|
this.dapClient.setOutputSink((line) => {
|
|
38460
38740
|
this.writeToTranscript(line);
|
|
38461
38741
|
});
|
|
38742
|
+
this.dapClient.setBreakpointsSink((held) => {
|
|
38743
|
+
const project = this.godotBridge.getStatus().projectPath ?? this.ownProject;
|
|
38744
|
+
if (project !== null) {
|
|
38745
|
+
writeBreakpointNote(project, held);
|
|
38746
|
+
}
|
|
38747
|
+
});
|
|
38462
38748
|
}
|
|
38463
38749
|
return this.dapClient;
|
|
38464
38750
|
}
|
|
38751
|
+
dapHoldingBreakpointsOf(projectPath) {
|
|
38752
|
+
const client = this.dap();
|
|
38753
|
+
if (this.breakpointsSeededOn !== client) {
|
|
38754
|
+
client.holdBreakpoints(readBreakpointNote(projectPath));
|
|
38755
|
+
this.breakpointsSeededOn = client;
|
|
38756
|
+
}
|
|
38757
|
+
return client;
|
|
38758
|
+
}
|
|
38759
|
+
breakpointsAtRisk() {
|
|
38760
|
+
const status = this.godotBridge.getStatus();
|
|
38761
|
+
if (!status.connected || status.syncsBreakpoints === true) {
|
|
38762
|
+
return {};
|
|
38763
|
+
}
|
|
38764
|
+
return {
|
|
38765
|
+
breakpointsAtRisk: status.syncsBreakpoints === false ? "This editor clears every breakpoint in its script editor whenever a debug session opens on its adapter, which this server does for its first breakpoint or stack read: network/debug_adapter/sync_breakpoints is off. The installed addon turns it on as it loads, so the editor is running an older one; editor_launch restart puts the installed addon in, or turn the setting on in Editor Settings > Network > Debug Adapter." : "Whether this editor keeps its breakpoints when a debug session opens cannot be told: the addon it is running predates the report. Godot clears them unless network/debug_adapter/sync_breakpoints is on, which the installed addon turns on as it loads; editor_launch restart puts the installed addon in."
|
|
38766
|
+
};
|
|
38767
|
+
}
|
|
38768
|
+
breakpointsAsProjectSpells(projectPath, held) {
|
|
38769
|
+
return held.map((one) => ({
|
|
38770
|
+
scriptPath: projectSpelling(projectPath, one.scriptPath),
|
|
38771
|
+
lines: one.lines
|
|
38772
|
+
}));
|
|
38773
|
+
}
|
|
38465
38774
|
writeToTranscript(line) {
|
|
38466
38775
|
const run = this.activeProcess;
|
|
38467
38776
|
if (run?.throughEditor !== true || run.transcript === null) {
|
|
@@ -38518,6 +38827,7 @@ class GodotServer {
|
|
|
38518
38827
|
...status.connected ? this.whatTheLaunchedEditorDropped() : {},
|
|
38519
38828
|
startupError: this.bridgeStartupError,
|
|
38520
38829
|
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
|
|
38830
|
+
...this.breakpointsAtRisk(),
|
|
38521
38831
|
retryingBridge: this.bridgeRetry === null ? undefined : true,
|
|
38522
38832
|
announcedAt: this.announcedAt ?? undefined,
|
|
38523
38833
|
projectIs: this.projectHasMovedOn() ?? undefined,
|
|
@@ -38551,8 +38861,12 @@ class GodotServer {
|
|
|
38551
38861
|
async handleEditorStatus() {
|
|
38552
38862
|
const located = await this.locator.find();
|
|
38553
38863
|
const announced = runtimesAnnounced();
|
|
38554
|
-
const
|
|
38555
|
-
const
|
|
38864
|
+
const pinged = Promise.all(announced.running.map(async (endpoint) => {
|
|
38865
|
+
const held = this.knownHoldOn(endpoint);
|
|
38866
|
+
const reply = held === null ? await runtimeRequest(endpoint, "ping", {}, HOLD_PING_MS) : {
|
|
38867
|
+
ok: false,
|
|
38868
|
+
message: `The game is held by the editor's debugger, ${describeHalt(held)}, and answers no runtime call while it is. debug_control continue lets it go.`
|
|
38869
|
+
};
|
|
38556
38870
|
return {
|
|
38557
38871
|
pid: endpoint.pid,
|
|
38558
38872
|
port: endpoint.port,
|
|
@@ -38561,8 +38875,18 @@ class GodotServer {
|
|
|
38561
38875
|
problem: reply.ok ? null : reply.message
|
|
38562
38876
|
};
|
|
38563
38877
|
}));
|
|
38564
|
-
const
|
|
38565
|
-
|
|
38878
|
+
const asked = (async () => {
|
|
38879
|
+
const playing = await this.editorPlayingState();
|
|
38880
|
+
await this.pickUpWhatTheEditorIsPlaying();
|
|
38881
|
+
const current = this.currentRun();
|
|
38882
|
+
const active = current === null ? false : await this.runStillGoing(current, playing?.playing ?? null);
|
|
38883
|
+
return {
|
|
38884
|
+
playing,
|
|
38885
|
+
active,
|
|
38886
|
+
hold: current === null ? {} : await this.holdOf(current, active)
|
|
38887
|
+
};
|
|
38888
|
+
})();
|
|
38889
|
+
const [games, { playing, active, hold }] = await Promise.all([pinged, asked]);
|
|
38566
38890
|
return this.jsonTextResponse({
|
|
38567
38891
|
editor: {
|
|
38568
38892
|
...this.getEditorStatusPayload(),
|
|
@@ -38574,8 +38898,9 @@ class GodotServer {
|
|
|
38574
38898
|
problem: located.ok ? undefined : GodotLocator2.refusal(located).message
|
|
38575
38899
|
},
|
|
38576
38900
|
game: {
|
|
38577
|
-
processActive:
|
|
38901
|
+
processActive: active,
|
|
38578
38902
|
playingInEditor: playing,
|
|
38903
|
+
...hold,
|
|
38579
38904
|
runtimeConnected: games.some((game) => game.reachable),
|
|
38580
38905
|
runtimes: games,
|
|
38581
38906
|
unreadable: announced.unspoken.map((game) => ({
|
|
@@ -38593,7 +38918,7 @@ class GodotServer {
|
|
|
38593
38918
|
return null;
|
|
38594
38919
|
}
|
|
38595
38920
|
try {
|
|
38596
|
-
return settingKeys(
|
|
38921
|
+
return settingKeys(readFileSync17(join19(projectPath, "project.godot"), "utf8"));
|
|
38597
38922
|
} catch {
|
|
38598
38923
|
return null;
|
|
38599
38924
|
}
|
|
@@ -38786,10 +39111,12 @@ class GodotServer {
|
|
|
38786
39111
|
if (op === "check") {
|
|
38787
39112
|
return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value);
|
|
38788
39113
|
}
|
|
39114
|
+
await this.pickUpWhatTheEditorIsPlaying();
|
|
38789
39115
|
let ended = null;
|
|
38790
|
-
|
|
39116
|
+
const before = this.currentRun();
|
|
39117
|
+
if (before !== null && await this.runStillGoing(before)) {
|
|
38791
39118
|
this.logDebug("Ending the running game before starting another");
|
|
38792
|
-
ended = this.
|
|
39119
|
+
ended = { pid: before.pid ?? this.announcedPidOf(before) ?? null };
|
|
38793
39120
|
await this.endActiveGame("editor_run start, which ends the run that was going before it starts another");
|
|
38794
39121
|
}
|
|
38795
39122
|
const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
|
|
@@ -38813,8 +39140,8 @@ class GodotServer {
|
|
|
38813
39140
|
const started = this.spawnKeptGame(engine.value, cmdArgs, project.value.path);
|
|
38814
39141
|
this.activeProcess = started;
|
|
38815
39142
|
const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && given.value.length > 0 ? " The editor cannot be handed arguments for a game it plays, so this one was started " + "here: the debug_* tools answer only for a game the editor is playing." : "";
|
|
38816
|
-
const endedForThis = ended
|
|
38817
|
-
const stranded = wereHereFirst.filter((one) => one.pid !== ended && alive(one.pid));
|
|
39143
|
+
const endedForThis = endedToStartThis(ended);
|
|
39144
|
+
const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
|
|
38818
39145
|
const named = stranded.map((one) => one.protocol === null ? `pid ${one.pid}` : `pid ${one.pid} on protocol ${one.protocol}`).join(", ");
|
|
38819
39146
|
const alsoRunning = stranded.length === 0 ? "" : ` A game of this project was already running and still is: ${named}. Starting this one did not end it and nothing here can, because this server did not start it. End it yourself, or bring the halves to one version where a protocol is named.`;
|
|
38820
39147
|
return this.jsonTextResponse({
|
|
@@ -38825,23 +39152,38 @@ class GodotServer {
|
|
|
38825
39152
|
arguments: cmdArgs,
|
|
38826
39153
|
transcript: started.transcript,
|
|
38827
39154
|
refreshedClasses: refreshed.value,
|
|
38828
|
-
endedPreviousRun: ended
|
|
39155
|
+
endedPreviousRun: endedPreviousRun(ended),
|
|
38829
39156
|
runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
|
|
38830
39157
|
message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}${alsoRunning}`
|
|
38831
39158
|
});
|
|
38832
39159
|
}
|
|
38833
|
-
async gameIsUp() {
|
|
38834
|
-
const run = this.currentRun();
|
|
38835
|
-
const editorSays = run?.throughEditor === true ? (await this.editorPlayingState())?.playing ?? null : null;
|
|
38836
|
-
return runIsUp(run, editorSays);
|
|
38837
|
-
}
|
|
38838
39160
|
async runtimeUp(projectPath, before, budgetMs, withArgs) {
|
|
38839
|
-
if (!existsSync14(
|
|
39161
|
+
if (!existsSync14(join19(projectPath, RUNTIME_AUTOLOAD2.path))) {
|
|
38840
39162
|
return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
|
|
38841
39163
|
}
|
|
39164
|
+
let asking = null;
|
|
39165
|
+
let editorAskedAt = 0;
|
|
39166
|
+
let editorSaysGoing = true;
|
|
38842
39167
|
const endpoint = await announcedSince(projectPath, before, {
|
|
38843
39168
|
budgetMs,
|
|
38844
|
-
giveUp: () =>
|
|
39169
|
+
giveUp: () => {
|
|
39170
|
+
if (this.dapClient?.isStopped() === true) {
|
|
39171
|
+
return true;
|
|
39172
|
+
}
|
|
39173
|
+
const run = this.currentRun();
|
|
39174
|
+
if (run === null || !run.throughEditor) {
|
|
39175
|
+
return !stillRunning(run);
|
|
39176
|
+
}
|
|
39177
|
+
if (asking === null && Date.now() - editorAskedAt >= RUN_POLL_MS) {
|
|
39178
|
+
editorAskedAt = Date.now();
|
|
39179
|
+
asking = this.runStillGoing(run).then((going) => {
|
|
39180
|
+
editorSaysGoing = going;
|
|
39181
|
+
}).finally(() => {
|
|
39182
|
+
asking = null;
|
|
39183
|
+
});
|
|
39184
|
+
}
|
|
39185
|
+
return !editorSaysGoing;
|
|
39186
|
+
}
|
|
38845
39187
|
});
|
|
38846
39188
|
const going = this.currentRun();
|
|
38847
39189
|
if (endpoint !== null && going?.throughEditor === true && going.announcedPid === undefined) {
|
|
@@ -38851,7 +39193,7 @@ class GodotServer {
|
|
|
38851
39193
|
addon: true,
|
|
38852
39194
|
budgetMs,
|
|
38853
39195
|
heldAt: this.dapClient?.whereItStopped() ?? null,
|
|
38854
|
-
running: await this.
|
|
39196
|
+
running: endpoint !== null || going !== null && await this.runStillGoing(going),
|
|
38855
39197
|
withArgs
|
|
38856
39198
|
});
|
|
38857
39199
|
}
|
|
@@ -38874,6 +39216,7 @@ class GodotServer {
|
|
|
38874
39216
|
"Closing the other editor, or moving its debug adapter port in its editor settings, frees this one"
|
|
38875
39217
|
]);
|
|
38876
39218
|
}
|
|
39219
|
+
const breakpoints = await this.dapHoldingBreakpointsOf(projectPath).reapplyBreakpoints();
|
|
38877
39220
|
const answer = await this.handleViaBridge("play_scene", scene === null ? {} : { scenePath: `res://${scene}` });
|
|
38878
39221
|
if (answer.isError === true) {
|
|
38879
39222
|
return answer;
|
|
@@ -38891,7 +39234,8 @@ class GodotServer {
|
|
|
38891
39234
|
startedAt,
|
|
38892
39235
|
exitCode: null,
|
|
38893
39236
|
throughEditor: true,
|
|
38894
|
-
brokeOn: null
|
|
39237
|
+
brokeOn: null,
|
|
39238
|
+
announcedBefore: alreadyPlaying
|
|
38895
39239
|
};
|
|
38896
39240
|
this.activeProcess = played;
|
|
38897
39241
|
writeEditorRunNote({ projectPath, transcript: transcript.path, startedAt });
|
|
@@ -38902,9 +39246,14 @@ class GodotServer {
|
|
|
38902
39246
|
scene: scene === null ? "the main scene" : `res://${scene}`,
|
|
38903
39247
|
refreshedClasses,
|
|
38904
39248
|
debugPort: readNumber(asParams(JSON.parse(answer.content[0]?.text ?? "{}")), "debugPort"),
|
|
38905
|
-
endedPreviousRun: ended
|
|
39249
|
+
endedPreviousRun: endedPreviousRun(ended),
|
|
39250
|
+
breakpoints: breakpoints.applied.length === 0 ? undefined : this.breakpointsAsProjectSpells(projectPath, breakpoints.applied),
|
|
39251
|
+
breakpointsRefused: breakpoints.refused.length === 0 ? undefined : breakpoints.refused.map((one) => ({
|
|
39252
|
+
scriptPath: projectSpelling(projectPath, one.scriptPath),
|
|
39253
|
+
reason: one.reason
|
|
39254
|
+
})),
|
|
38906
39255
|
runtime: await this.runtimeUp(projectPath, alreadyPlaying, runtimeWaitMs, false),
|
|
38907
|
-
message: "The editor is playing it, so its debugger holds it: the debug_* tools can reach it, " + "editor_output reads its console through the debug adapter, and editor_run stop ends it." + (ended
|
|
39256
|
+
message: "The editor is playing it, so its debugger holds it: the debug_* tools can reach it, " + "editor_output reads its console through the debug adapter, and editor_run stop ends it." + endedToStartThis(ended)
|
|
38908
39257
|
});
|
|
38909
39258
|
}
|
|
38910
39259
|
drainEditorOutput(game) {
|
|
@@ -39100,7 +39449,7 @@ class GodotServer {
|
|
|
39100
39449
|
return;
|
|
39101
39450
|
}
|
|
39102
39451
|
try {
|
|
39103
|
-
await this.dap().
|
|
39452
|
+
await this.dap().attach();
|
|
39104
39453
|
} catch (error) {
|
|
39105
39454
|
this.logDebug(`Picked up an editor-played run without its adapter: ${errorMessage(error)}`);
|
|
39106
39455
|
}
|
|
@@ -39122,6 +39471,18 @@ class GodotServer {
|
|
|
39122
39471
|
};
|
|
39123
39472
|
this.activeProcess = picked;
|
|
39124
39473
|
}
|
|
39474
|
+
announcedPidOf(run) {
|
|
39475
|
+
if (!run.throughEditor || run.announcedPid !== undefined) {
|
|
39476
|
+
return run.announcedPid;
|
|
39477
|
+
}
|
|
39478
|
+
const before = run.announcedBefore ?? new Set;
|
|
39479
|
+
const fresh = this.allAnnouncedForOurProject(runtimesAnnounced().running).filter((one) => !before.has(one.pid));
|
|
39480
|
+
const theOne = fresh.length === 1 ? fresh[0] : undefined;
|
|
39481
|
+
if (theOne !== undefined) {
|
|
39482
|
+
run.announcedPid = theOne.pid;
|
|
39483
|
+
}
|
|
39484
|
+
return run.announcedPid;
|
|
39485
|
+
}
|
|
39125
39486
|
adoptRecordedRun() {
|
|
39126
39487
|
const record = readRunRecord();
|
|
39127
39488
|
if (record === null || !this.couldBeOurs(record.projectPath)) {
|
|
@@ -39250,12 +39611,22 @@ class GodotServer {
|
|
|
39250
39611
|
}
|
|
39251
39612
|
const budgetMs = readPositiveNumber(args, "timeoutMs") ?? WAIT_FOR_RUN_MS;
|
|
39252
39613
|
const until = Date.now() + budgetMs;
|
|
39253
|
-
while (
|
|
39614
|
+
while (await this.runStillGoing(run) && Date.now() < until) {
|
|
39254
39615
|
this.drainEditorOutput(run);
|
|
39255
39616
|
this.drainTranscript(run);
|
|
39256
39617
|
await delay3(RUN_POLL_MS);
|
|
39257
39618
|
}
|
|
39258
|
-
return await this.handleGetDebugOutput(args,
|
|
39619
|
+
return await this.handleGetDebugOutput(args, await this.runStillGoing(run) ? budgetMs : undefined);
|
|
39620
|
+
}
|
|
39621
|
+
async runStillGoing(run, editorSays) {
|
|
39622
|
+
if (!run.throughEditor) {
|
|
39623
|
+
return stillRunning(run);
|
|
39624
|
+
}
|
|
39625
|
+
const announced = this.announcedPidOf(run);
|
|
39626
|
+
if (announced !== undefined && !alive(announced)) {
|
|
39627
|
+
return false;
|
|
39628
|
+
}
|
|
39629
|
+
return runIsUp(run, editorSays === undefined ? (await this.editorPlayingState())?.playing ?? null : editorSays);
|
|
39259
39630
|
}
|
|
39260
39631
|
async handleGetDebugOutput(args, waitedMs) {
|
|
39261
39632
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
@@ -39265,7 +39636,8 @@ class GodotServer {
|
|
|
39265
39636
|
}
|
|
39266
39637
|
this.drainEditorOutput(run);
|
|
39267
39638
|
this.drainTranscript(run);
|
|
39268
|
-
|
|
39639
|
+
const going = await this.runStillGoing(run);
|
|
39640
|
+
if (run.endedUnwatched !== true && !going) {
|
|
39269
39641
|
run.log.finish();
|
|
39270
39642
|
run.endedUnwatched = run.exitCode === null;
|
|
39271
39643
|
}
|
|
@@ -39276,12 +39648,13 @@ class GodotServer {
|
|
|
39276
39648
|
contains: readNonEmptyString(args, "contains"),
|
|
39277
39649
|
limit: readPositiveNumber(args, "limit") ?? 200
|
|
39278
39650
|
});
|
|
39279
|
-
const
|
|
39651
|
+
const hold = await this.holdOf(run, going);
|
|
39280
39652
|
const wanted = readBoolean(args, "cpu") ?? false;
|
|
39281
|
-
const
|
|
39653
|
+
const processToAsk = run.pid ?? this.announcedPidOf(run) ?? null;
|
|
39654
|
+
const cpuSeconds = wanted && processToAsk !== null && stillRunning(run) ? await cpuSecondsOf(processToAsk) : undefined;
|
|
39282
39655
|
const notes = [];
|
|
39283
39656
|
if (wanted && cpuSeconds === undefined && stillRunning(run)) {
|
|
39284
|
-
notes.push(
|
|
39657
|
+
notes.push(processToAsk === null ? "cpu was asked for and there is no process to ask: the editor is playing this run and holds it, and the game announced no runtime, so nothing here has its process id. A game with the runtime addon announces one; editor_run start without a connected editor, or with args, starts the game here instead and that run answers cpuSeconds either way." : "cpu was asked for and this platform would not say what the run has used.");
|
|
39285
39658
|
}
|
|
39286
39659
|
if (waitedMs !== undefined) {
|
|
39287
39660
|
notes.push(`The wait of ${waitedMs}ms ran out and the run is still going, so this is what it had printed by then rather than everything it will print. editor_run wait again to keep waiting.`);
|
|
@@ -39305,15 +39678,15 @@ class GodotServer {
|
|
|
39305
39678
|
notes.push(`Everything this run has printed is in ${run.transcript}, uncapped and still being written. Read it for output, not for whether the run is alive: a transcript that stops growing is a run between prints. running above is asked of the operating system while there is a process to ask about, and for an editor-played run that announced no runtime it is the editor's answer instead, which can lag after the game has gone.`);
|
|
39306
39679
|
}
|
|
39307
39680
|
return this.jsonTextResponse({
|
|
39308
|
-
running:
|
|
39681
|
+
running: going,
|
|
39309
39682
|
exitCode: run.exitCode,
|
|
39310
39683
|
through: run.throughEditor ? "editor" : "gdharness",
|
|
39311
|
-
pid: run.pid,
|
|
39684
|
+
pid: run.pid ?? this.announcedPidOf(run) ?? null,
|
|
39312
39685
|
errors: run.log.count("error"),
|
|
39313
39686
|
warnings: run.log.count("warning"),
|
|
39314
39687
|
clean: run.consoleLost === true ? undefined : run.log.count("error") === 0,
|
|
39315
39688
|
consoleLost: run.consoleLost === true ? true : undefined,
|
|
39316
|
-
|
|
39689
|
+
...hold,
|
|
39317
39690
|
endedUnwatched: run.endedUnwatched === true ? true : undefined,
|
|
39318
39691
|
endedBy: stillRunning(run) ? undefined : run.endedHere ?? null,
|
|
39319
39692
|
project: !run.throughEditor && run.process === null ? run.projectPath ?? undefined : undefined,
|
|
@@ -39334,18 +39707,19 @@ class GodotServer {
|
|
|
39334
39707
|
}
|
|
39335
39708
|
this.drainEditorOutput(stopped);
|
|
39336
39709
|
this.drainTranscript(stopped);
|
|
39710
|
+
const wasRunning = await this.runStillGoing(stopped);
|
|
39337
39711
|
this.logDebug("Stopping the running game");
|
|
39338
39712
|
await this.endActiveGame("editor_run stop");
|
|
39339
39713
|
return this.jsonTextResponse({
|
|
39340
39714
|
stopped: true,
|
|
39341
39715
|
through: stopped.throughEditor ? "editor" : "gdharness",
|
|
39342
|
-
endedPid: stopped.pid,
|
|
39343
|
-
exitedBeforeStop: stopped.exitCode !== null,
|
|
39716
|
+
endedPid: stopped.pid ?? this.announcedPidOf(stopped) ?? null,
|
|
39717
|
+
exitedBeforeStop: stopped.exitCode !== null || !wasRunning,
|
|
39344
39718
|
exitCode: stopped.exitCode,
|
|
39345
39719
|
errors: stopped.log.count("error"),
|
|
39346
39720
|
warnings: stopped.log.count("warning"),
|
|
39347
39721
|
clean: stopped.log.count("error") === 0,
|
|
39348
|
-
note: stopped.throughEditor ? "The editor was asked to stop the scene it is playing. Anything that game started for itself, with OS.create_process or otherwise, is not the editor's to stop and is still running." : "The process named under endedPid was ended. Anything that game started for itself, with OS.create_process or otherwise, is a separate process and is still running.",
|
|
39722
|
+
note: !wasRunning ? "This run was over before the stop, so nothing was ended here: editor_output has what it printed and how it ended. Anything that game started for itself, with OS.create_process or otherwise, is a separate process and may still be running." : stopped.throughEditor ? "The editor was asked to stop the scene it is playing. Anything that game started for itself, with OS.create_process or otherwise, is not the editor's to stop and is still running." : "The process named under endedPid was ended. Anything that game started for itself, with OS.create_process or otherwise, is a separate process and is still running.",
|
|
39349
39723
|
entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
39350
39724
|
});
|
|
39351
39725
|
}
|
|
@@ -39496,16 +39870,68 @@ class GodotServer {
|
|
|
39496
39870
|
const override = Number.parseInt(envValue("GDHARNESS_RUNTIME_TIMEOUT_MS") ?? "", 10);
|
|
39497
39871
|
return Number.isInteger(override) && override > 0 ? override : 1e4;
|
|
39498
39872
|
}
|
|
39873
|
+
knownHoldOn(endpoint) {
|
|
39874
|
+
const run = this.activeProcess;
|
|
39875
|
+
const session = this.dapClient;
|
|
39876
|
+
if (run === null || !run.throughEditor || !stillRunning(run) || session === null) {
|
|
39877
|
+
return null;
|
|
39878
|
+
}
|
|
39879
|
+
if (this.announcedPidOf(run) !== endpoint.pid || !session.holdIsKnown()) {
|
|
39880
|
+
return null;
|
|
39881
|
+
}
|
|
39882
|
+
return session.whereItStopped();
|
|
39883
|
+
}
|
|
39884
|
+
async holdOf(run, going) {
|
|
39885
|
+
if (!run.throughEditor || !(going ?? await this.runStillGoing(run))) {
|
|
39886
|
+
return { heldAt: null };
|
|
39887
|
+
}
|
|
39888
|
+
const session = this.dapClient;
|
|
39889
|
+
if (session?.holdIsKnown()) {
|
|
39890
|
+
return { heldAt: session.whereItStopped() };
|
|
39891
|
+
}
|
|
39892
|
+
const its = this.announcedPidOf(run);
|
|
39893
|
+
const endpoint = its === undefined ? undefined : runtimesAnnounced().running.find((one) => one.pid === its);
|
|
39894
|
+
if (endpoint === undefined) {
|
|
39895
|
+
return {
|
|
39896
|
+
heldAt: undefined,
|
|
39897
|
+
heldUnknown: true,
|
|
39898
|
+
heldNote: "Whether this run is held at a breakpoint cannot be told from here: this server connected to the debugger after any stop, the adapter reports a stop only to the sessions connected when it happened, and the run has announced no runtime to ask instead. A game that draws nothing and answers nothing is held; debug_control continue lets a held game go."
|
|
39899
|
+
};
|
|
39900
|
+
}
|
|
39901
|
+
const reply = await runtimeRequest(endpoint, "ping", {}, HOLD_PING_MS);
|
|
39902
|
+
if (reply.ok) {
|
|
39903
|
+
session?.learnedRunning();
|
|
39904
|
+
return { heldAt: session?.whereItStopped() ?? null };
|
|
39905
|
+
}
|
|
39906
|
+
if (reply.reason === "busy") {
|
|
39907
|
+
return {
|
|
39908
|
+
heldAt: {
|
|
39909
|
+
reason: "unanswered",
|
|
39910
|
+
description: `the game accepted a runtime connection and did not answer a ping within ${HOLD_PING_MS}ms, which is what a game held at a breakpoint does, and what one stuck in a long frame does. This server connected to the debugger after the stop, so the adapter will not show it the stack; debug_control continue lets a held game go`,
|
|
39911
|
+
text: ""
|
|
39912
|
+
}
|
|
39913
|
+
};
|
|
39914
|
+
}
|
|
39915
|
+
return { heldAt: undefined, heldUnknown: true, heldNote: reply.message };
|
|
39916
|
+
}
|
|
39499
39917
|
async handleRuntimeCommand(command, args, timeoutMs = this.runtimeTimeoutMs()) {
|
|
39500
|
-
const { op: _op, projectPath, ...params } = asParams(args);
|
|
39918
|
+
const { op: _op, projectPath, pid, ...params } = asParams(args);
|
|
39501
39919
|
const announced = runtimesAnnounced();
|
|
39502
|
-
const choice = chooseRuntime(announced.running, typeof projectPath === "string" ? projectPath : this.ownProject ?? undefined, announced.unspoken);
|
|
39920
|
+
const choice = chooseRuntime(announced.running, typeof projectPath === "string" ? projectPath : this.ownProject ?? undefined, announced.unspoken, typeof pid === "number" ? pid : undefined);
|
|
39503
39921
|
if ("problem" in choice) {
|
|
39504
39922
|
return this.createErrorResponse(choice.problem);
|
|
39505
39923
|
}
|
|
39924
|
+
const held = this.knownHoldOn(choice.endpoint);
|
|
39925
|
+
if (held !== null) {
|
|
39926
|
+
return this.createErrorResponse(`The game (pid ${choice.endpoint.pid}) is held by the editor's debugger, ${describeHalt(held)}, and answers no runtime call while it is.`, [
|
|
39927
|
+
"debug_control continue lets it go, after which this call answers",
|
|
39928
|
+
"debug_state stack reads where it is held, and debug_state scopes what it is holding there",
|
|
39929
|
+
"editor_output says heldAt for the run, and editor_run stop ends it"
|
|
39930
|
+
]);
|
|
39931
|
+
}
|
|
39506
39932
|
const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
|
|
39507
|
-
const screenshotDir = expectsScreenshot ? mkdtempSync3(
|
|
39508
|
-
const screenshotPath = screenshotDir ?
|
|
39933
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync3(join19(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
|
|
39934
|
+
const screenshotPath = screenshotDir ? join19(screenshotDir, "capture.png") : null;
|
|
39509
39935
|
try {
|
|
39510
39936
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
|
|
39511
39937
|
if (!reply.ok) {
|
|
@@ -39526,7 +39952,7 @@ class GodotServer {
|
|
|
39526
39952
|
return {
|
|
39527
39953
|
content: [
|
|
39528
39954
|
{ type: "text", text: `Screenshot captured: ${dimensions}` },
|
|
39529
|
-
{ type: "image", data:
|
|
39955
|
+
{ type: "image", data: readFileSync17(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
39530
39956
|
]
|
|
39531
39957
|
};
|
|
39532
39958
|
} finally {
|
|
@@ -39557,7 +39983,7 @@ class GodotServer {
|
|
|
39557
39983
|
const includeHidden = readBoolean(args, "includeHidden");
|
|
39558
39984
|
return await this.handleRuntimeCommand("find_nodes", {
|
|
39559
39985
|
...filters,
|
|
39560
|
-
|
|
39986
|
+
...whichGame(args),
|
|
39561
39987
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
39562
39988
|
limit: readPositiveNumber(args, "limit") ?? 100,
|
|
39563
39989
|
...property === undefined ? {} : { property },
|
|
@@ -39568,18 +39994,18 @@ class GodotServer {
|
|
|
39568
39994
|
if (op === "frames") {
|
|
39569
39995
|
const frames = readPositiveNumber(args, "frames") ?? 1;
|
|
39570
39996
|
const waited = patienceForFrames(frames, this.runtimeTimeoutMs());
|
|
39571
|
-
return await this.handleRuntimeCommand("wait_frames", {
|
|
39997
|
+
return await this.handleRuntimeCommand("wait_frames", { ...whichGame(args), frames }, waited);
|
|
39572
39998
|
}
|
|
39573
39999
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 5000;
|
|
39574
40000
|
const nodePath = readNonEmptyString(args, "nodePath") ?? "";
|
|
39575
40001
|
const patience = Math.max(this.runtimeTimeoutMs(), timeoutMs + 5000);
|
|
39576
40002
|
return op === "signal" ? await this.handleRuntimeCommand("wait_signal", {
|
|
39577
|
-
|
|
40003
|
+
...whichGame(args),
|
|
39578
40004
|
path: nodePath,
|
|
39579
40005
|
signal: readString(args, "signal") ?? "",
|
|
39580
40006
|
timeout_ms: timeoutMs
|
|
39581
40007
|
}, patience) : await this.handleRuntimeCommand("wait_until", {
|
|
39582
|
-
|
|
40008
|
+
...whichGame(args),
|
|
39583
40009
|
path: nodePath,
|
|
39584
40010
|
property: readString(args, "property") ?? "",
|
|
39585
40011
|
value: args["value"],
|
|
@@ -39592,11 +40018,12 @@ async function runGodotServer() {
|
|
|
39592
40018
|
const server = new GodotServer;
|
|
39593
40019
|
await server.run();
|
|
39594
40020
|
}
|
|
39595
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run6, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, SLOWEST_FRAME_RATE = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CACHE_WRITE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
40021
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run6, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CACHE_WRITE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
39596
40022
|
var init_server3 = __esm(() => {
|
|
39597
40023
|
init_mcp();
|
|
39598
40024
|
init_stdio2();
|
|
39599
40025
|
init_types();
|
|
40026
|
+
init_breakpoint_note();
|
|
39600
40027
|
init_bridge_announce();
|
|
39601
40028
|
init_class_cache();
|
|
39602
40029
|
init_config_pin();
|
|
@@ -39624,7 +40051,7 @@ var init_server3 = __esm(() => {
|
|
|
39624
40051
|
init_tool_definitions();
|
|
39625
40052
|
init_update_check();
|
|
39626
40053
|
run6 = promisify6(execFile6);
|
|
39627
|
-
__dirname2 =
|
|
40054
|
+
__dirname2 = dirname11(fileURLToPath4(import.meta.url));
|
|
39628
40055
|
VALUE_NAMES = {
|
|
39629
40056
|
string: "a string",
|
|
39630
40057
|
number: "a number",
|
|
@@ -39663,7 +40090,7 @@ var init_server3 = __esm(() => {
|
|
|
39663
40090
|
|
|
39664
40091
|
// src/cli.ts
|
|
39665
40092
|
import { existsSync as existsSync15 } from "fs";
|
|
39666
|
-
import { join as
|
|
40093
|
+
import { join as join20, resolve as resolve5 } from "path";
|
|
39667
40094
|
|
|
39668
40095
|
// src/errors.ts
|
|
39669
40096
|
class Refusal extends Error {
|
|
@@ -41133,7 +41560,8 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
41133
41560
|
## Start here
|
|
41134
41561
|
|
|
41135
41562
|
- ${projectPathSentence()}. The \`runtime_*\` and \`debug_*\` ones pick between running
|
|
41136
|
-
games instead
|
|
41563
|
+
games instead, by \`projectPath\`, or by \`pid\` when several run from one project, as a bench
|
|
41564
|
+
and its workers do; the others answer about the editor this server is connected to. There is no
|
|
41137
41565
|
ambient project, and an argument a tool does not declare is refused rather than ignored.
|
|
41138
41566
|
- \`editor_status\` says whether an editor is connected and whether its addon matches the server.
|
|
41139
41567
|
\`addonIsStale\` means they differ, and \`staleNote\` says which half is behind: an editor that
|
|
@@ -41205,6 +41633,12 @@ The autoload reaches an export. \`gdharness runtime off\` in the project before
|
|
|
41205
41633
|
move. There is no pause and no step out: Godot's debug adapter implements neither, so use a
|
|
41206
41634
|
breakpoint on the line you want instead.
|
|
41207
41635
|
|
|
41636
|
+
A held game draws nothing and answers no \`runtime_*\` call. The server says so at once when it
|
|
41637
|
+
knows, naming why the game is held and \`debug_control continue\` as what lets it go, and
|
|
41638
|
+
\`editor_output\` and \`editor_status\` carry the same under \`heldAt\`. A breakpoint set with
|
|
41639
|
+
\`debug_breakpoint\` holds for every start until \`debug_breakpoint remove\`, across a reconnect,
|
|
41640
|
+
so take it off when the question it was for is answered.
|
|
41641
|
+
|
|
41208
41642
|
## Editing
|
|
41209
41643
|
|
|
41210
41644
|
\`scene_tree\` for what a scene holds, rather than what the \`.tscn\` text implies. \`scene_node\`,
|
|
@@ -41353,7 +41787,7 @@ class UsageError extends Refusal {
|
|
|
41353
41787
|
function projectArgument(at) {
|
|
41354
41788
|
const named = args.slice(at).find((value) => !value.startsWith("--"));
|
|
41355
41789
|
const projectPath = resolve5(named ?? ".");
|
|
41356
|
-
if (!existsSync15(
|
|
41790
|
+
if (!existsSync15(join20(projectPath, "project.godot"))) {
|
|
41357
41791
|
throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
|
|
41358
41792
|
}
|
|
41359
41793
|
return projectPath;
|
|
@@ -41392,7 +41826,7 @@ async function registerRuntime(godot, projectPath) {
|
|
|
41392
41826
|
return;
|
|
41393
41827
|
}
|
|
41394
41828
|
console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
|
|
41395
|
-
if (!existsSync15(
|
|
41829
|
+
if (!existsSync15(join20(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
|
|
41396
41830
|
console.log(` that file is not in this project, so it boots with a missing script: restore it, or point the entry at res://${RUNTIME_AUTOLOAD.path}`);
|
|
41397
41831
|
}
|
|
41398
41832
|
}
|