gdharness 0.13.39 → 0.13.41
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/index.js
CHANGED
|
@@ -25248,6 +25248,8 @@ class GodotDAPClient {
|
|
|
25248
25248
|
halt = null;
|
|
25249
25249
|
holdKnown = false;
|
|
25250
25250
|
breakpoints = new Map;
|
|
25251
|
+
inEditor = new Map;
|
|
25252
|
+
onBreakpointsChanged = null;
|
|
25251
25253
|
constructor(port = portFromEnv("GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT), host = "127.0.0.1") {
|
|
25252
25254
|
this.port = port;
|
|
25253
25255
|
this.host = host;
|
|
@@ -25337,6 +25339,7 @@ class GodotDAPClient {
|
|
|
25337
25339
|
this.attached = false;
|
|
25338
25340
|
this.halt = null;
|
|
25339
25341
|
this.holdKnown = false;
|
|
25342
|
+
this.inEditor.clear();
|
|
25340
25343
|
}
|
|
25341
25344
|
async ensureConnected() {
|
|
25342
25345
|
if (!this.connected) {
|
|
@@ -25433,6 +25436,31 @@ class GodotDAPClient {
|
|
|
25433
25436
|
this.holdKnown = true;
|
|
25434
25437
|
return;
|
|
25435
25438
|
}
|
|
25439
|
+
if (eventName === "breakpoint") {
|
|
25440
|
+
const body = event.body;
|
|
25441
|
+
const breakpoint = body?.["breakpoint"];
|
|
25442
|
+
const source = typeof breakpoint === "object" && breakpoint !== null ? breakpoint["source"] : undefined;
|
|
25443
|
+
const path = typeof source === "object" && source !== null ? source["path"] : undefined;
|
|
25444
|
+
const line = typeof breakpoint === "object" && breakpoint !== null ? breakpoint["line"] : undefined;
|
|
25445
|
+
if (typeof path !== "string" || typeof line !== "number") {
|
|
25446
|
+
return;
|
|
25447
|
+
}
|
|
25448
|
+
if (said(body, "reason") === "removed") {
|
|
25449
|
+
this.inEditor.get(path)?.delete(line);
|
|
25450
|
+
const held = this.breakpoints.get(path);
|
|
25451
|
+
if (held?.delete(line) === true) {
|
|
25452
|
+
if (held.size === 0) {
|
|
25453
|
+
this.breakpoints.delete(path);
|
|
25454
|
+
}
|
|
25455
|
+
this.onBreakpointsChanged?.(this.breakpointsHeld());
|
|
25456
|
+
}
|
|
25457
|
+
return;
|
|
25458
|
+
}
|
|
25459
|
+
const known = this.inEditor.get(path) ?? new Set;
|
|
25460
|
+
known.add(line);
|
|
25461
|
+
this.inEditor.set(path, known);
|
|
25462
|
+
return;
|
|
25463
|
+
}
|
|
25436
25464
|
if (eventName === "terminated" || eventName === "exited") {
|
|
25437
25465
|
this.attached = false;
|
|
25438
25466
|
this.halt = null;
|
|
@@ -25525,23 +25553,46 @@ class GodotDAPClient {
|
|
|
25525
25553
|
async removeBreakpoint(filePath, line) {
|
|
25526
25554
|
const wanted = new Set(this.breakpoints.get(filePath) ?? []);
|
|
25527
25555
|
wanted.delete(line);
|
|
25528
|
-
return await this.sendBreakpoints(filePath, wanted);
|
|
25556
|
+
return await this.sendBreakpoints(filePath, wanted, line);
|
|
25529
25557
|
}
|
|
25530
|
-
async sendBreakpoints(filePath,
|
|
25558
|
+
async sendBreakpoints(filePath, mine, clearing) {
|
|
25531
25559
|
await this.ensureConnected();
|
|
25532
25560
|
await this.initialize();
|
|
25561
|
+
const lines = new Set([...mine, ...this.inEditor.get(filePath) ?? []]);
|
|
25562
|
+
if (clearing !== undefined) {
|
|
25563
|
+
lines.delete(clearing);
|
|
25564
|
+
}
|
|
25533
25565
|
const sorted = Array.from(lines).sort((a, b) => a - b);
|
|
25534
25566
|
const answer = await this.sendRequest("setBreakpoints", {
|
|
25535
25567
|
source: { path: filePath },
|
|
25536
25568
|
breakpoints: sorted.map((breakpointLine) => ({ line: breakpointLine }))
|
|
25537
25569
|
});
|
|
25538
|
-
if (
|
|
25570
|
+
if (mine.size === 0) {
|
|
25539
25571
|
this.breakpoints.delete(filePath);
|
|
25540
25572
|
} else {
|
|
25541
|
-
this.breakpoints.set(filePath, new Set(
|
|
25573
|
+
this.breakpoints.set(filePath, new Set(mine));
|
|
25574
|
+
}
|
|
25575
|
+
if (lines.size === 0) {
|
|
25576
|
+
this.inEditor.delete(filePath);
|
|
25577
|
+
} else {
|
|
25578
|
+
this.inEditor.set(filePath, lines);
|
|
25542
25579
|
}
|
|
25543
25580
|
return answer;
|
|
25544
25581
|
}
|
|
25582
|
+
setBreakpointsSink(sink) {
|
|
25583
|
+
this.onBreakpointsChanged = sink;
|
|
25584
|
+
}
|
|
25585
|
+
breakpointsInEditor() {
|
|
25586
|
+
const theirs = [];
|
|
25587
|
+
for (const [scriptPath, lines] of this.inEditor) {
|
|
25588
|
+
const mine = this.breakpoints.get(scriptPath);
|
|
25589
|
+
const rest = Array.from(lines).filter((line) => !mine?.has(line)).sort((a, b) => a - b);
|
|
25590
|
+
if (rest.length > 0) {
|
|
25591
|
+
theirs.push({ scriptPath, lines: rest });
|
|
25592
|
+
}
|
|
25593
|
+
}
|
|
25594
|
+
return theirs;
|
|
25595
|
+
}
|
|
25545
25596
|
async reapplyBreakpoints() {
|
|
25546
25597
|
const applied = [];
|
|
25547
25598
|
const refused = [];
|
|
@@ -25557,6 +25608,9 @@ class GodotDAPClient {
|
|
|
25557
25608
|
});
|
|
25558
25609
|
}
|
|
25559
25610
|
}
|
|
25611
|
+
if (refused.length > 0) {
|
|
25612
|
+
this.onBreakpointsChanged?.(this.breakpointsHeld());
|
|
25613
|
+
}
|
|
25560
25614
|
return { applied, refused };
|
|
25561
25615
|
}
|
|
25562
25616
|
breakpointsHeld() {
|
|
@@ -25759,14 +25813,10 @@ async function handleDAPTool(client, toolName, args) {
|
|
|
25759
25813
|
}
|
|
25760
25814
|
} catch (error) {
|
|
25761
25815
|
const message = error instanceof Error ? error.message : String(error);
|
|
25816
|
+
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.";
|
|
25762
25817
|
return {
|
|
25763
25818
|
isError: true,
|
|
25764
|
-
content: [
|
|
25765
|
-
{
|
|
25766
|
-
type: "text",
|
|
25767
|
-
text: `DAP tool '${toolName}' failed: ${message}. ` + "The debug tools answer for a game the editor is playing: start one with editor_run."
|
|
25768
|
-
}
|
|
25769
|
-
]
|
|
25819
|
+
content: [{ type: "text", text: `DAP tool '${toolName}' failed: ${message}. ${cure}` }]
|
|
25770
25820
|
};
|
|
25771
25821
|
}
|
|
25772
25822
|
}
|
|
@@ -26071,6 +26121,7 @@ class GodotBridge extends EventEmitter {
|
|
|
26071
26121
|
dapPort: this.connectionInfo?.dapPort,
|
|
26072
26122
|
debugPort: this.connectionInfo?.debugPort,
|
|
26073
26123
|
openedByAServer: this.connectionInfo?.openedByAServer,
|
|
26124
|
+
syncsBreakpoints: this.connectionInfo?.syncsBreakpoints,
|
|
26074
26125
|
pendingRequests: this.pendingRequests.size,
|
|
26075
26126
|
queuedResources: this.resourceQueues.size,
|
|
26076
26127
|
listeningSince: this.listeningSince ?? undefined
|
|
@@ -26213,6 +26264,7 @@ class GodotBridge extends EventEmitter {
|
|
|
26213
26264
|
this.connectionInfo.dapPort = servedPort(message.dap_port);
|
|
26214
26265
|
this.connectionInfo.debugPort = servedPort(message.debug_port);
|
|
26215
26266
|
this.connectionInfo.openedByAServer = message.opened_by_a_server === true;
|
|
26267
|
+
this.connectionInfo.syncsBreakpoints = typeof message.syncs_breakpoints === "boolean" ? message.syncs_breakpoints : undefined;
|
|
26216
26268
|
this.log("info", `Godot ready: ${message.project_path}`);
|
|
26217
26269
|
this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
|
|
26218
26270
|
}
|
|
@@ -27661,7 +27713,7 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
27661
27713
|
import { execFile as execFile3 } from "node:child_process";
|
|
27662
27714
|
import { promisify as promisify3 } from "node:util";
|
|
27663
27715
|
var run3 = promisify3(execFile3);
|
|
27664
|
-
var ASK_TIMEOUT_MS =
|
|
27716
|
+
var ASK_TIMEOUT_MS = 6000;
|
|
27665
27717
|
async function cpuSecondsOf(pid) {
|
|
27666
27718
|
try {
|
|
27667
27719
|
return process.platform === "win32" ? await askWindows(pid) : await askPosix(pid);
|
|
@@ -28218,7 +28270,18 @@ function parseAnnouncement(file, pid) {
|
|
|
28218
28270
|
if (protocol !== RUNTIME_PROTOCOL) {
|
|
28219
28271
|
return { kind: "unspoken", unspoken: { pid, protocol: protocol ?? 0, project: named } };
|
|
28220
28272
|
}
|
|
28221
|
-
|
|
28273
|
+
const editorPid = readNumber(fields, "editor_pid");
|
|
28274
|
+
return {
|
|
28275
|
+
kind: "runtime",
|
|
28276
|
+
endpoint: {
|
|
28277
|
+
pid,
|
|
28278
|
+
port,
|
|
28279
|
+
address,
|
|
28280
|
+
project: named,
|
|
28281
|
+
file,
|
|
28282
|
+
...editorPid !== undefined && Number.isInteger(editorPid) && editorPid > 0 ? { editorPid } : {}
|
|
28283
|
+
}
|
|
28284
|
+
};
|
|
28222
28285
|
}
|
|
28223
28286
|
function runtimesAnnounced(directories = runtimeDirectories()) {
|
|
28224
28287
|
const running = new Map;
|
|
@@ -28297,7 +28360,7 @@ async function announcedSince(projectPath, before, waiting = {}) {
|
|
|
28297
28360
|
const directories = waiting.directories ?? runtimeDirectories();
|
|
28298
28361
|
const until = Date.now() + Math.max(waiting.budgetMs ?? ANNOUNCE_BUDGET_MS, 0);
|
|
28299
28362
|
for (;; ) {
|
|
28300
|
-
const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && resolve4(endpoint.project.path) === wanted);
|
|
28363
|
+
const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && resolve4(endpoint.project.path) === wanted && (waiting.accept?.(endpoint) ?? true));
|
|
28301
28364
|
if (fresh !== undefined) {
|
|
28302
28365
|
return fresh;
|
|
28303
28366
|
}
|
|
@@ -28316,7 +28379,25 @@ function tooNew(unspoken) {
|
|
|
28316
28379
|
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";
|
|
28317
28380
|
return `A game is running, in a protocol this server does not speak ${named}. This server speaks ${RUNTIME_PROTOCOL}, so ${half}.`;
|
|
28318
28381
|
}
|
|
28319
|
-
function chooseRuntime(endpoints, projectPath, unspoken = []) {
|
|
28382
|
+
function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
|
|
28383
|
+
if (pid !== undefined) {
|
|
28384
|
+
const named = endpoints.find((endpoint) => endpoint.pid === pid);
|
|
28385
|
+
if (named !== undefined) {
|
|
28386
|
+
if (projectPath !== undefined && resolve4(named.project.path) !== resolve4(projectPath)) {
|
|
28387
|
+
return {
|
|
28388
|
+
problem: `The game with pid ${pid} is running ${named.project.path}, not ${resolve4(projectPath)}. Running: ${endpoints.map(describe2).join("; ")}.`
|
|
28389
|
+
};
|
|
28390
|
+
}
|
|
28391
|
+
return { endpoint: named };
|
|
28392
|
+
}
|
|
28393
|
+
const tooNewToo = unspoken.find((game) => game.pid === pid);
|
|
28394
|
+
if (tooNewToo !== undefined) {
|
|
28395
|
+
return { problem: tooNew([tooNewToo]) };
|
|
28396
|
+
}
|
|
28397
|
+
return {
|
|
28398
|
+
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.`
|
|
28399
|
+
};
|
|
28400
|
+
}
|
|
28320
28401
|
if (endpoints.length === 0) {
|
|
28321
28402
|
if (unspoken.length > 0) {
|
|
28322
28403
|
return { problem: tooNew(unspoken) };
|
|
@@ -28347,14 +28428,14 @@ function chooseRuntime(endpoints, projectPath, unspoken = []) {
|
|
|
28347
28428
|
return { endpoint: matching[0] };
|
|
28348
28429
|
}
|
|
28349
28430
|
return {
|
|
28350
|
-
problem: `Several games are running from ${wanted}: ${matching.map(describe2).join("; ")}.
|
|
28431
|
+
problem: `Several games are running from ${wanted}: ${matching.map(describe2).join("; ")}. Pass pid to choose one.`
|
|
28351
28432
|
};
|
|
28352
28433
|
}
|
|
28353
28434
|
if (endpoints.length === 1 && endpoints[0]) {
|
|
28354
28435
|
return { endpoint: endpoints[0] };
|
|
28355
28436
|
}
|
|
28356
28437
|
return {
|
|
28357
|
-
problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass projectPath to choose one.`
|
|
28438
|
+
problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass projectPath to choose one, or pid when they are from one project.`
|
|
28358
28439
|
};
|
|
28359
28440
|
}
|
|
28360
28441
|
var nextRequestId = 1;
|
|
@@ -28765,6 +28846,10 @@ var RUNNING_PROJECT_PATH = {
|
|
|
28765
28846
|
type: "string",
|
|
28766
28847
|
description: "Which game, when more than one is running: the project directory it was started from. Not needed with one game."
|
|
28767
28848
|
};
|
|
28849
|
+
var RUNNING_PID = {
|
|
28850
|
+
type: "number",
|
|
28851
|
+
description: "Which game, when several are running from one project, such as a bench and its workers: its process id, as editor_status lists under runtimes. Not needed with one game per project."
|
|
28852
|
+
};
|
|
28768
28853
|
var SCENE_PATH = {
|
|
28769
28854
|
type: "string",
|
|
28770
28855
|
description: 'Scene file inside the project, such as "scenes/main.tscn" or "res://scenes/main.tscn".'
|
|
@@ -29431,7 +29516,7 @@ var TOOL_SPECS = [
|
|
|
29431
29516
|
},
|
|
29432
29517
|
{
|
|
29433
29518
|
name: "editor_status",
|
|
29434
|
-
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.",
|
|
29519
|
+
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. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. 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.",
|
|
29435
29520
|
parameters: {},
|
|
29436
29521
|
requires: []
|
|
29437
29522
|
},
|
|
@@ -29488,6 +29573,7 @@ var TOOL_SPECS = [
|
|
|
29488
29573
|
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.",
|
|
29489
29574
|
parameters: {
|
|
29490
29575
|
projectPath: RUNNING_PROJECT_PATH,
|
|
29576
|
+
pid: RUNNING_PID,
|
|
29491
29577
|
nodePath: {
|
|
29492
29578
|
type: "string",
|
|
29493
29579
|
ops: ["tree", "text", "find", "rect", "property"],
|
|
@@ -29566,6 +29652,7 @@ var TOOL_SPECS = [
|
|
|
29566
29652
|
description: "Sets a property or calls a method on a node in the running game. Needs the game running with the runtime addon.",
|
|
29567
29653
|
parameters: {
|
|
29568
29654
|
projectPath: RUNNING_PROJECT_PATH,
|
|
29655
|
+
pid: RUNNING_PID,
|
|
29569
29656
|
nodePath: { type: "string", description: 'Absolute node path, such as "/root/Main/Player".' },
|
|
29570
29657
|
property: {
|
|
29571
29658
|
type: "string",
|
|
@@ -29605,6 +29692,7 @@ var TOOL_SPECS = [
|
|
|
29605
29692
|
description: "A picture of the running game: the whole screen or one viewport, as an image. Needs the game running with a window.",
|
|
29606
29693
|
parameters: {
|
|
29607
29694
|
projectPath: RUNNING_PROJECT_PATH,
|
|
29695
|
+
pid: RUNNING_PID,
|
|
29608
29696
|
viewportPath: {
|
|
29609
29697
|
type: "string",
|
|
29610
29698
|
ops: ["viewport"],
|
|
@@ -29625,6 +29713,7 @@ var TOOL_SPECS = [
|
|
|
29625
29713
|
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.",
|
|
29626
29714
|
parameters: {
|
|
29627
29715
|
projectPath: RUNNING_PROJECT_PATH,
|
|
29716
|
+
pid: RUNNING_PID,
|
|
29628
29717
|
nodePath: {
|
|
29629
29718
|
type: "string",
|
|
29630
29719
|
ops: ["click", "choose"],
|
|
@@ -29721,6 +29810,7 @@ var TOOL_SPECS = [
|
|
|
29721
29810
|
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.",
|
|
29722
29811
|
parameters: {
|
|
29723
29812
|
projectPath: RUNNING_PROJECT_PATH,
|
|
29813
|
+
pid: RUNNING_PID,
|
|
29724
29814
|
frames: {
|
|
29725
29815
|
type: "number",
|
|
29726
29816
|
ops: ["frames"],
|
|
@@ -29756,7 +29846,7 @@ var TOOL_SPECS = [
|
|
|
29756
29846
|
},
|
|
29757
29847
|
{
|
|
29758
29848
|
name: "debug_breakpoint",
|
|
29759
|
-
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.",
|
|
29849
|
+
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.",
|
|
29760
29850
|
parameters: {
|
|
29761
29851
|
projectPath: PROJECT_PATH,
|
|
29762
29852
|
scriptPath: SCRIPT_PATH,
|
|
@@ -29858,6 +29948,9 @@ function patienceForFrames(frames, atLeast) {
|
|
|
29858
29948
|
function projectSpelling(projectPath, absolutePath) {
|
|
29859
29949
|
return `res://${relative3(realPathOr(projectPath), realPathOr(absolutePath)).replaceAll("\\", "/")}`;
|
|
29860
29950
|
}
|
|
29951
|
+
function whichGame(args) {
|
|
29952
|
+
return { projectPath: args["projectPath"], pid: args["pid"] };
|
|
29953
|
+
}
|
|
29861
29954
|
function describeHalt(halt) {
|
|
29862
29955
|
switch (halt.reason) {
|
|
29863
29956
|
case "breakpoint":
|
|
@@ -29893,6 +29986,19 @@ function runIsUp(run, editorSays) {
|
|
|
29893
29986
|
}
|
|
29894
29987
|
return editorSays ?? stillRunning(run);
|
|
29895
29988
|
}
|
|
29989
|
+
function endedPreviousRun(ended) {
|
|
29990
|
+
if (ended === null) {
|
|
29991
|
+
return;
|
|
29992
|
+
}
|
|
29993
|
+
return ended.pid ?? true;
|
|
29994
|
+
}
|
|
29995
|
+
function endedToStartThis(ended) {
|
|
29996
|
+
if (ended === null) {
|
|
29997
|
+
return "";
|
|
29998
|
+
}
|
|
29999
|
+
const which = ended.pid === null ? "The game the editor was playing" : `The run that was going, pid ${ended.pid},`;
|
|
30000
|
+
return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.`;
|
|
30001
|
+
}
|
|
29896
30002
|
function runtimeVerdict(endpoint, after) {
|
|
29897
30003
|
if (endpoint !== null) {
|
|
29898
30004
|
return { listening: true, pid: endpoint.pid, port: endpoint.port };
|
|
@@ -30079,6 +30185,16 @@ function hasMainScene(projectFile) {
|
|
|
30079
30185
|
const scene = parseProjectGodot(readFileSync13(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
30080
30186
|
return typeof scene === "string" && scene !== "";
|
|
30081
30187
|
}
|
|
30188
|
+
function runtimeAddonAnnouncesEditors(projectPath) {
|
|
30189
|
+
if (projectPath === null) {
|
|
30190
|
+
return false;
|
|
30191
|
+
}
|
|
30192
|
+
try {
|
|
30193
|
+
return readFileSync13(join13(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
|
|
30194
|
+
} catch {
|
|
30195
|
+
return false;
|
|
30196
|
+
}
|
|
30197
|
+
}
|
|
30082
30198
|
function editorPlaysHeadless(projectFile) {
|
|
30083
30199
|
const runArgs = parseProjectGodot(readFileSync13(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
30084
30200
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
@@ -30633,7 +30749,7 @@ class GodotServer {
|
|
|
30633
30749
|
switch (op) {
|
|
30634
30750
|
case "tree":
|
|
30635
30751
|
return await this.handleRuntimeCommand("get_tree", {
|
|
30636
|
-
|
|
30752
|
+
...whichGame(args),
|
|
30637
30753
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
30638
30754
|
depth: readPositiveNumber(args, "depth") ?? 3,
|
|
30639
30755
|
include_properties: readBoolean(args, "includeProperties") ?? false
|
|
@@ -30642,36 +30758,36 @@ class GodotServer {
|
|
|
30642
30758
|
return await this.handleFindRuntimeNodes(args);
|
|
30643
30759
|
case "text":
|
|
30644
30760
|
return await this.handleRuntimeCommand("read_text", {
|
|
30645
|
-
|
|
30761
|
+
...whichGame(args),
|
|
30646
30762
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
30647
30763
|
include_hidden: readBoolean(args, "includeHidden") ?? false,
|
|
30648
30764
|
limit: readPositiveNumber(args, "limit") ?? 500
|
|
30649
30765
|
});
|
|
30650
30766
|
case "rect":
|
|
30651
30767
|
return await this.handleRuntimeCommand("get_rect", {
|
|
30652
|
-
|
|
30768
|
+
...whichGame(args),
|
|
30653
30769
|
path: readNonEmptyString(args, "nodePath") ?? ""
|
|
30654
30770
|
});
|
|
30655
30771
|
case "property":
|
|
30656
30772
|
return await this.handleRuntimeCommand("get_property", {
|
|
30657
|
-
|
|
30773
|
+
...whichGame(args),
|
|
30658
30774
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
30659
30775
|
property: readNonEmptyString(args, "property") ?? ""
|
|
30660
30776
|
});
|
|
30661
30777
|
default:
|
|
30662
30778
|
return await this.handleRuntimeCommand("get_metrics", {
|
|
30663
|
-
|
|
30779
|
+
...whichGame(args),
|
|
30664
30780
|
metrics: readArray(args, "metrics") ?? []
|
|
30665
30781
|
});
|
|
30666
30782
|
}
|
|
30667
30783
|
case "runtime_invoke":
|
|
30668
30784
|
return op === "set" ? await this.handleRuntimeCommand("set_property", {
|
|
30669
|
-
|
|
30785
|
+
...whichGame(args),
|
|
30670
30786
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
30671
30787
|
property: readString(args, "property") ?? "",
|
|
30672
30788
|
value: args["value"]
|
|
30673
30789
|
}) : await this.handleRuntimeCommand("call_method", {
|
|
30674
|
-
|
|
30790
|
+
...whichGame(args),
|
|
30675
30791
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
30676
30792
|
method: readString(args, "method") ?? "",
|
|
30677
30793
|
args: readArray(args, "args") ?? []
|
|
@@ -30681,7 +30797,7 @@ class GodotServer {
|
|
|
30681
30797
|
case "runtime_input":
|
|
30682
30798
|
if (op === "click") {
|
|
30683
30799
|
return await this.handleRuntimeCommand("click", {
|
|
30684
|
-
|
|
30800
|
+
...whichGame(args),
|
|
30685
30801
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
30686
30802
|
button: readString(args, "button") ?? "left",
|
|
30687
30803
|
double: readBoolean(args, "doubleClick") ?? false
|
|
@@ -30689,7 +30805,7 @@ class GodotServer {
|
|
|
30689
30805
|
}
|
|
30690
30806
|
if (op === "choose") {
|
|
30691
30807
|
return await this.handleRuntimeCommand("choose", {
|
|
30692
|
-
|
|
30808
|
+
...whichGame(args),
|
|
30693
30809
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
30694
30810
|
text: readString(args, "text") ?? "",
|
|
30695
30811
|
...args["index"] === undefined ? {} : { index: args["index"] }
|
|
@@ -30720,7 +30836,9 @@ class GodotServer {
|
|
|
30720
30836
|
return this.jsonTextResponse({
|
|
30721
30837
|
...asParams(JSON.parse(answered.content[0]?.text ?? "{}")),
|
|
30722
30838
|
held: this.breakpointsAsProjectSpells(project.value.path, held),
|
|
30723
|
-
|
|
30839
|
+
setInEditor: this.breakpointsAsProjectSpells(project.value.path, session.breakpointsInEditor()),
|
|
30840
|
+
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.",
|
|
30841
|
+
...this.breakpointsAtRisk()
|
|
30724
30842
|
});
|
|
30725
30843
|
}
|
|
30726
30844
|
case "debug_control":
|
|
@@ -30794,6 +30912,16 @@ class GodotServer {
|
|
|
30794
30912
|
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"])
|
|
30795
30913
|
};
|
|
30796
30914
|
}
|
|
30915
|
+
const going = await this.runStillGoing(game);
|
|
30916
|
+
if (!going) {
|
|
30917
|
+
return {
|
|
30918
|
+
ok: false,
|
|
30919
|
+
response: this.createErrorResponse("The last run has already ended, so there is no debug session to answer for.", [
|
|
30920
|
+
"editor_output still reads what it printed, until the next run starts",
|
|
30921
|
+
"editor_run start plays another, which the debugger can hold"
|
|
30922
|
+
])
|
|
30923
|
+
};
|
|
30924
|
+
}
|
|
30797
30925
|
if (!game.throughEditor) {
|
|
30798
30926
|
return {
|
|
30799
30927
|
ok: false,
|
|
@@ -30815,7 +30943,7 @@ class GodotServer {
|
|
|
30815
30943
|
};
|
|
30816
30944
|
}
|
|
30817
30945
|
if (!this.dap().isStopped()) {
|
|
30818
|
-
const hold = await this.holdOf(game);
|
|
30946
|
+
const hold = await this.holdOf(game, going);
|
|
30819
30947
|
if (hold.heldAt !== null) {
|
|
30820
30948
|
if (intent === "move") {
|
|
30821
30949
|
return { ok: true, value: game };
|
|
@@ -31381,6 +31509,12 @@ class GodotServer {
|
|
|
31381
31509
|
this.dapClient.setOutputSink((line) => {
|
|
31382
31510
|
this.writeToTranscript(line);
|
|
31383
31511
|
});
|
|
31512
|
+
this.dapClient.setBreakpointsSink((held) => {
|
|
31513
|
+
const project = this.godotBridge.getStatus().projectPath ?? this.ownProject;
|
|
31514
|
+
if (project !== null) {
|
|
31515
|
+
writeBreakpointNote(project, held);
|
|
31516
|
+
}
|
|
31517
|
+
});
|
|
31384
31518
|
}
|
|
31385
31519
|
return this.dapClient;
|
|
31386
31520
|
}
|
|
@@ -31392,6 +31526,15 @@ class GodotServer {
|
|
|
31392
31526
|
}
|
|
31393
31527
|
return client;
|
|
31394
31528
|
}
|
|
31529
|
+
breakpointsAtRisk() {
|
|
31530
|
+
const status = this.godotBridge.getStatus();
|
|
31531
|
+
if (!status.connected || status.syncsBreakpoints === true) {
|
|
31532
|
+
return {};
|
|
31533
|
+
}
|
|
31534
|
+
return {
|
|
31535
|
+
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."
|
|
31536
|
+
};
|
|
31537
|
+
}
|
|
31395
31538
|
breakpointsAsProjectSpells(projectPath, held) {
|
|
31396
31539
|
return held.map((one) => ({
|
|
31397
31540
|
scriptPath: projectSpelling(projectPath, one.scriptPath),
|
|
@@ -31454,6 +31597,7 @@ class GodotServer {
|
|
|
31454
31597
|
...status.connected ? this.whatTheLaunchedEditorDropped() : {},
|
|
31455
31598
|
startupError: this.bridgeStartupError,
|
|
31456
31599
|
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
|
|
31600
|
+
...this.breakpointsAtRisk(),
|
|
31457
31601
|
retryingBridge: this.bridgeRetry === null ? undefined : true,
|
|
31458
31602
|
announcedAt: this.announcedAt ?? undefined,
|
|
31459
31603
|
projectIs: this.projectHasMovedOn() ?? undefined,
|
|
@@ -31488,7 +31632,7 @@ class GodotServer {
|
|
|
31488
31632
|
const located = await this.locator.find();
|
|
31489
31633
|
const announced = runtimesAnnounced();
|
|
31490
31634
|
const pinged = Promise.all(announced.running.map(async (endpoint) => {
|
|
31491
|
-
const held = this.knownHoldOn(endpoint);
|
|
31635
|
+
const held = this.knownHoldOn(endpoint, announced.running);
|
|
31492
31636
|
const reply = held === null ? await runtimeRequest(endpoint, "ping", {}, HOLD_PING_MS) : {
|
|
31493
31637
|
ok: false,
|
|
31494
31638
|
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.`
|
|
@@ -31497,6 +31641,7 @@ class GodotServer {
|
|
|
31497
31641
|
pid: endpoint.pid,
|
|
31498
31642
|
port: endpoint.port,
|
|
31499
31643
|
project: endpoint.project,
|
|
31644
|
+
editorPid: endpoint.editorPid,
|
|
31500
31645
|
reachable: reply.ok,
|
|
31501
31646
|
problem: reply.ok ? null : reply.message
|
|
31502
31647
|
};
|
|
@@ -31505,9 +31650,14 @@ class GodotServer {
|
|
|
31505
31650
|
const playing = await this.editorPlayingState();
|
|
31506
31651
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
31507
31652
|
const current = this.currentRun();
|
|
31508
|
-
|
|
31653
|
+
const active = current === null ? false : await this.runStillGoing(current, playing?.playing ?? null);
|
|
31654
|
+
return {
|
|
31655
|
+
playing,
|
|
31656
|
+
active,
|
|
31657
|
+
hold: current === null ? {} : await this.holdOf(current, active)
|
|
31658
|
+
};
|
|
31509
31659
|
})();
|
|
31510
|
-
const [games, { playing, hold }] = await Promise.all([pinged, asked]);
|
|
31660
|
+
const [games, { playing, active, hold }] = await Promise.all([pinged, asked]);
|
|
31511
31661
|
return this.jsonTextResponse({
|
|
31512
31662
|
editor: {
|
|
31513
31663
|
...this.getEditorStatusPayload(),
|
|
@@ -31519,7 +31669,7 @@ class GodotServer {
|
|
|
31519
31669
|
problem: located.ok ? undefined : GodotLocator.refusal(located).message
|
|
31520
31670
|
},
|
|
31521
31671
|
game: {
|
|
31522
|
-
processActive:
|
|
31672
|
+
processActive: active,
|
|
31523
31673
|
playingInEditor: playing,
|
|
31524
31674
|
...hold,
|
|
31525
31675
|
runtimeConnected: games.some((game) => game.reachable),
|
|
@@ -31732,10 +31882,12 @@ class GodotServer {
|
|
|
31732
31882
|
if (op === "check") {
|
|
31733
31883
|
return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value);
|
|
31734
31884
|
}
|
|
31885
|
+
await this.pickUpWhatTheEditorIsPlaying();
|
|
31735
31886
|
let ended = null;
|
|
31736
|
-
|
|
31887
|
+
const before = this.currentRun();
|
|
31888
|
+
if (before !== null && await this.runStillGoing(before)) {
|
|
31737
31889
|
this.logDebug("Ending the running game before starting another");
|
|
31738
|
-
ended = this.
|
|
31890
|
+
ended = { pid: before.pid ?? this.announcedPidOf(before) ?? null };
|
|
31739
31891
|
await this.endActiveGame("editor_run start, which ends the run that was going before it starts another");
|
|
31740
31892
|
}
|
|
31741
31893
|
const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
|
|
@@ -31759,8 +31911,8 @@ class GodotServer {
|
|
|
31759
31911
|
const started = this.spawnKeptGame(engine.value, cmdArgs, project.value.path);
|
|
31760
31912
|
this.activeProcess = started;
|
|
31761
31913
|
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." : "";
|
|
31762
|
-
const endedForThis = ended
|
|
31763
|
-
const stranded = wereHereFirst.filter((one) => one.pid !== ended && alive(one.pid));
|
|
31914
|
+
const endedForThis = endedToStartThis(ended);
|
|
31915
|
+
const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
|
|
31764
31916
|
const named = stranded.map((one) => one.protocol === null ? `pid ${one.pid}` : `pid ${one.pid} on protocol ${one.protocol}`).join(", ");
|
|
31765
31917
|
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.`;
|
|
31766
31918
|
return this.jsonTextResponse({
|
|
@@ -31771,23 +31923,39 @@ class GodotServer {
|
|
|
31771
31923
|
arguments: cmdArgs,
|
|
31772
31924
|
transcript: started.transcript,
|
|
31773
31925
|
refreshedClasses: refreshed.value,
|
|
31774
|
-
endedPreviousRun: ended
|
|
31926
|
+
endedPreviousRun: endedPreviousRun(ended),
|
|
31775
31927
|
runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
|
|
31776
31928
|
message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}${alsoRunning}`
|
|
31777
31929
|
});
|
|
31778
31930
|
}
|
|
31779
|
-
async gameIsUp() {
|
|
31780
|
-
const run = this.currentRun();
|
|
31781
|
-
const editorSays = run?.throughEditor === true ? (await this.editorPlayingState())?.playing ?? null : null;
|
|
31782
|
-
return runIsUp(run, editorSays);
|
|
31783
|
-
}
|
|
31784
31931
|
async runtimeUp(projectPath, before, budgetMs, withArgs) {
|
|
31785
31932
|
if (!existsSync9(join13(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
31786
31933
|
return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
|
|
31787
31934
|
}
|
|
31935
|
+
let asking = null;
|
|
31936
|
+
let editorAskedAt = 0;
|
|
31937
|
+
let editorSaysGoing = true;
|
|
31788
31938
|
const endpoint = await announcedSince(projectPath, before, {
|
|
31789
31939
|
budgetMs,
|
|
31790
|
-
|
|
31940
|
+
accept: (one) => this.isTheGameOf(this.currentRun(), one, projectPath),
|
|
31941
|
+
giveUp: () => {
|
|
31942
|
+
if (this.dapClient?.isStopped() === true) {
|
|
31943
|
+
return true;
|
|
31944
|
+
}
|
|
31945
|
+
const run = this.currentRun();
|
|
31946
|
+
if (run === null || !run.throughEditor) {
|
|
31947
|
+
return !stillRunning(run);
|
|
31948
|
+
}
|
|
31949
|
+
if (asking === null && Date.now() - editorAskedAt >= RUN_POLL_MS) {
|
|
31950
|
+
editorAskedAt = Date.now();
|
|
31951
|
+
asking = this.runStillGoing(run).then((going) => {
|
|
31952
|
+
editorSaysGoing = going;
|
|
31953
|
+
}).finally(() => {
|
|
31954
|
+
asking = null;
|
|
31955
|
+
});
|
|
31956
|
+
}
|
|
31957
|
+
return !editorSaysGoing;
|
|
31958
|
+
}
|
|
31791
31959
|
});
|
|
31792
31960
|
const going = this.currentRun();
|
|
31793
31961
|
if (endpoint !== null && going?.throughEditor === true && going.announcedPid === undefined) {
|
|
@@ -31797,7 +31965,7 @@ class GodotServer {
|
|
|
31797
31965
|
addon: true,
|
|
31798
31966
|
budgetMs,
|
|
31799
31967
|
heldAt: this.dapClient?.whereItStopped() ?? null,
|
|
31800
|
-
running: await this.
|
|
31968
|
+
running: endpoint !== null || going !== null && await this.runStillGoing(going),
|
|
31801
31969
|
withArgs
|
|
31802
31970
|
});
|
|
31803
31971
|
}
|
|
@@ -31838,7 +32006,8 @@ class GodotServer {
|
|
|
31838
32006
|
startedAt,
|
|
31839
32007
|
exitCode: null,
|
|
31840
32008
|
throughEditor: true,
|
|
31841
|
-
brokeOn: null
|
|
32009
|
+
brokeOn: null,
|
|
32010
|
+
announcedBefore: alreadyPlaying
|
|
31842
32011
|
};
|
|
31843
32012
|
this.activeProcess = played;
|
|
31844
32013
|
writeEditorRunNote({ projectPath, transcript: transcript.path, startedAt });
|
|
@@ -31849,14 +32018,14 @@ class GodotServer {
|
|
|
31849
32018
|
scene: scene === null ? "the main scene" : `res://${scene}`,
|
|
31850
32019
|
refreshedClasses,
|
|
31851
32020
|
debugPort: readNumber(asParams(JSON.parse(answer.content[0]?.text ?? "{}")), "debugPort"),
|
|
31852
|
-
endedPreviousRun: ended
|
|
32021
|
+
endedPreviousRun: endedPreviousRun(ended),
|
|
31853
32022
|
breakpoints: breakpoints.applied.length === 0 ? undefined : this.breakpointsAsProjectSpells(projectPath, breakpoints.applied),
|
|
31854
32023
|
breakpointsRefused: breakpoints.refused.length === 0 ? undefined : breakpoints.refused.map((one) => ({
|
|
31855
32024
|
scriptPath: projectSpelling(projectPath, one.scriptPath),
|
|
31856
32025
|
reason: one.reason
|
|
31857
32026
|
})),
|
|
31858
32027
|
runtime: await this.runtimeUp(projectPath, alreadyPlaying, runtimeWaitMs, false),
|
|
31859
|
-
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
|
|
32028
|
+
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)
|
|
31860
32029
|
});
|
|
31861
32030
|
}
|
|
31862
32031
|
drainEditorOutput(game) {
|
|
@@ -32072,15 +32241,40 @@ class GodotServer {
|
|
|
32072
32241
|
brokeOn: null,
|
|
32073
32242
|
pickedUpPlaying: true
|
|
32074
32243
|
};
|
|
32075
|
-
const announced = this.theOneAnnouncedForOurProject();
|
|
32076
|
-
if (announced !== undefined) {
|
|
32077
|
-
picked.announcedPid = announced.pid;
|
|
32078
|
-
}
|
|
32079
32244
|
this.activeProcess = picked;
|
|
32080
32245
|
}
|
|
32081
|
-
|
|
32082
|
-
|
|
32083
|
-
|
|
32246
|
+
announcedPidOf(run, announced) {
|
|
32247
|
+
if (!run.throughEditor || run.announcedPid !== undefined) {
|
|
32248
|
+
return run.announcedPid;
|
|
32249
|
+
}
|
|
32250
|
+
const before = run.announcedBefore ?? new Set;
|
|
32251
|
+
const fresh = this.allAnnouncedForOurProject(announced ?? runtimesAnnounced().running).filter((one) => !before.has(one.pid));
|
|
32252
|
+
const candidates = fresh.filter((one) => this.playedByOurEditor(one, run.projectPath));
|
|
32253
|
+
const theOne = candidates.length === 1 ? candidates[0] : undefined;
|
|
32254
|
+
if (theOne !== undefined) {
|
|
32255
|
+
run.announcedPid = theOne.pid;
|
|
32256
|
+
}
|
|
32257
|
+
return run.announcedPid;
|
|
32258
|
+
}
|
|
32259
|
+
playedByOurEditor(endpoint, projectPath) {
|
|
32260
|
+
const editor = this.godotBridge.getStatus().editorPid;
|
|
32261
|
+
if (editor === undefined) {
|
|
32262
|
+
return true;
|
|
32263
|
+
}
|
|
32264
|
+
if (endpoint.editorPid !== undefined) {
|
|
32265
|
+
return endpoint.editorPid === editor;
|
|
32266
|
+
}
|
|
32267
|
+
return !runtimeAddonAnnouncesEditors(projectPath ?? this.ownProject);
|
|
32268
|
+
}
|
|
32269
|
+
isTheGameOf(run, endpoint, projectPath) {
|
|
32270
|
+
if (run === null) {
|
|
32271
|
+
return false;
|
|
32272
|
+
}
|
|
32273
|
+
if (run.throughEditor) {
|
|
32274
|
+
return this.playedByOurEditor(endpoint, projectPath);
|
|
32275
|
+
}
|
|
32276
|
+
const editor = this.godotBridge.getStatus().editorPid;
|
|
32277
|
+
return endpoint.editorPid === undefined || endpoint.editorPid !== editor;
|
|
32084
32278
|
}
|
|
32085
32279
|
adoptRecordedRun() {
|
|
32086
32280
|
const record = readRunRecord();
|
|
@@ -32210,12 +32404,25 @@ class GodotServer {
|
|
|
32210
32404
|
}
|
|
32211
32405
|
const budgetMs = readPositiveNumber(args, "timeoutMs") ?? WAIT_FOR_RUN_MS;
|
|
32212
32406
|
const until = Date.now() + budgetMs;
|
|
32213
|
-
while (
|
|
32407
|
+
while (await this.runStillGoing(run) && Date.now() < until) {
|
|
32214
32408
|
this.drainEditorOutput(run);
|
|
32215
32409
|
this.drainTranscript(run);
|
|
32216
32410
|
await delay3(RUN_POLL_MS);
|
|
32217
32411
|
}
|
|
32218
|
-
return await this.handleGetDebugOutput(args,
|
|
32412
|
+
return await this.handleGetDebugOutput(args, await this.runStillGoing(run) ? budgetMs : undefined);
|
|
32413
|
+
}
|
|
32414
|
+
async runStillGoing(run, editorSays) {
|
|
32415
|
+
if (!run.throughEditor) {
|
|
32416
|
+
return stillRunning(run);
|
|
32417
|
+
}
|
|
32418
|
+
const announced = this.announcedPidOf(run);
|
|
32419
|
+
if (announced !== undefined && !alive(announced)) {
|
|
32420
|
+
return false;
|
|
32421
|
+
}
|
|
32422
|
+
if (announced !== undefined && editorSays === undefined) {
|
|
32423
|
+
return stillRunning(run);
|
|
32424
|
+
}
|
|
32425
|
+
return runIsUp(run, editorSays === undefined ? (await this.editorPlayingState())?.playing ?? null : editorSays);
|
|
32219
32426
|
}
|
|
32220
32427
|
async handleGetDebugOutput(args, waitedMs) {
|
|
32221
32428
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
@@ -32225,7 +32432,8 @@ class GodotServer {
|
|
|
32225
32432
|
}
|
|
32226
32433
|
this.drainEditorOutput(run);
|
|
32227
32434
|
this.drainTranscript(run);
|
|
32228
|
-
|
|
32435
|
+
const going = await this.runStillGoing(run);
|
|
32436
|
+
if (run.endedUnwatched !== true && !going) {
|
|
32229
32437
|
run.log.finish();
|
|
32230
32438
|
run.endedUnwatched = run.exitCode === null;
|
|
32231
32439
|
}
|
|
@@ -32236,12 +32444,13 @@ class GodotServer {
|
|
|
32236
32444
|
contains: readNonEmptyString(args, "contains"),
|
|
32237
32445
|
limit: readPositiveNumber(args, "limit") ?? 200
|
|
32238
32446
|
});
|
|
32239
|
-
const hold = await this.holdOf(run);
|
|
32447
|
+
const hold = await this.holdOf(run, going);
|
|
32240
32448
|
const wanted = readBoolean(args, "cpu") ?? false;
|
|
32241
|
-
const
|
|
32449
|
+
const processToAsk = run.pid ?? this.announcedPidOf(run) ?? null;
|
|
32450
|
+
const cpuSeconds = wanted && processToAsk !== null && stillRunning(run) ? await cpuSecondsOf(processToAsk) : undefined;
|
|
32242
32451
|
const notes = [];
|
|
32243
32452
|
if (wanted && cpuSeconds === undefined && stillRunning(run)) {
|
|
32244
|
-
notes.push(
|
|
32453
|
+
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.");
|
|
32245
32454
|
}
|
|
32246
32455
|
if (waitedMs !== undefined) {
|
|
32247
32456
|
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.`);
|
|
@@ -32265,10 +32474,10 @@ class GodotServer {
|
|
|
32265
32474
|
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.`);
|
|
32266
32475
|
}
|
|
32267
32476
|
return this.jsonTextResponse({
|
|
32268
|
-
running:
|
|
32477
|
+
running: going,
|
|
32269
32478
|
exitCode: run.exitCode,
|
|
32270
32479
|
through: run.throughEditor ? "editor" : "gdharness",
|
|
32271
|
-
pid: run.pid,
|
|
32480
|
+
pid: run.pid ?? this.announcedPidOf(run) ?? null,
|
|
32272
32481
|
errors: run.log.count("error"),
|
|
32273
32482
|
warnings: run.log.count("warning"),
|
|
32274
32483
|
clean: run.consoleLost === true ? undefined : run.log.count("error") === 0,
|
|
@@ -32294,18 +32503,19 @@ class GodotServer {
|
|
|
32294
32503
|
}
|
|
32295
32504
|
this.drainEditorOutput(stopped);
|
|
32296
32505
|
this.drainTranscript(stopped);
|
|
32506
|
+
const wasRunning = await this.runStillGoing(stopped);
|
|
32297
32507
|
this.logDebug("Stopping the running game");
|
|
32298
32508
|
await this.endActiveGame("editor_run stop");
|
|
32299
32509
|
return this.jsonTextResponse({
|
|
32300
32510
|
stopped: true,
|
|
32301
32511
|
through: stopped.throughEditor ? "editor" : "gdharness",
|
|
32302
|
-
endedPid: stopped.pid,
|
|
32303
|
-
exitedBeforeStop: stopped.exitCode !== null,
|
|
32512
|
+
endedPid: stopped.pid ?? this.announcedPidOf(stopped) ?? null,
|
|
32513
|
+
exitedBeforeStop: stopped.exitCode !== null || !wasRunning,
|
|
32304
32514
|
exitCode: stopped.exitCode,
|
|
32305
32515
|
errors: stopped.log.count("error"),
|
|
32306
32516
|
warnings: stopped.log.count("warning"),
|
|
32307
32517
|
clean: stopped.log.count("error") === 0,
|
|
32308
|
-
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.",
|
|
32518
|
+
note: !wasRunning ? "This run was over before the stop, so nothing was ended here: editor_output has what it printed and how it ended. Anything that game started for itself, with OS.create_process or otherwise, is a separate process and may still be running." : stopped.throughEditor ? "The editor was asked to stop the scene it is playing. 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.",
|
|
32309
32519
|
entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
32310
32520
|
});
|
|
32311
32521
|
}
|
|
@@ -32456,27 +32666,27 @@ class GodotServer {
|
|
|
32456
32666
|
const override = Number.parseInt(envValue("GDHARNESS_RUNTIME_TIMEOUT_MS") ?? "", 10);
|
|
32457
32667
|
return Number.isInteger(override) && override > 0 ? override : 1e4;
|
|
32458
32668
|
}
|
|
32459
|
-
knownHoldOn(endpoint) {
|
|
32669
|
+
knownHoldOn(endpoint, announced) {
|
|
32460
32670
|
const run = this.activeProcess;
|
|
32461
32671
|
const session = this.dapClient;
|
|
32462
32672
|
if (run === null || !run.throughEditor || !stillRunning(run) || session === null) {
|
|
32463
32673
|
return null;
|
|
32464
32674
|
}
|
|
32465
|
-
|
|
32466
|
-
if (its !== endpoint.pid || !session.holdIsKnown()) {
|
|
32675
|
+
if (this.announcedPidOf(run, announced) !== endpoint.pid || !session.holdIsKnown()) {
|
|
32467
32676
|
return null;
|
|
32468
32677
|
}
|
|
32469
32678
|
return session.whereItStopped();
|
|
32470
32679
|
}
|
|
32471
|
-
async holdOf(run) {
|
|
32472
|
-
if (!run.throughEditor || !
|
|
32680
|
+
async holdOf(run, going) {
|
|
32681
|
+
if (!run.throughEditor || !(going ?? await this.runStillGoing(run))) {
|
|
32473
32682
|
return { heldAt: null };
|
|
32474
32683
|
}
|
|
32475
32684
|
const session = this.dapClient;
|
|
32476
32685
|
if (session?.holdIsKnown()) {
|
|
32477
32686
|
return { heldAt: session.whereItStopped() };
|
|
32478
32687
|
}
|
|
32479
|
-
const
|
|
32688
|
+
const its = this.announcedPidOf(run);
|
|
32689
|
+
const endpoint = its === undefined ? undefined : runtimesAnnounced().running.find((one) => one.pid === its);
|
|
32480
32690
|
if (endpoint === undefined) {
|
|
32481
32691
|
return {
|
|
32482
32692
|
heldAt: undefined,
|
|
@@ -32501,13 +32711,13 @@ class GodotServer {
|
|
|
32501
32711
|
return { heldAt: undefined, heldUnknown: true, heldNote: reply.message };
|
|
32502
32712
|
}
|
|
32503
32713
|
async handleRuntimeCommand(command, args, timeoutMs = this.runtimeTimeoutMs()) {
|
|
32504
|
-
const { op: _op, projectPath, ...params } = asParams(args);
|
|
32714
|
+
const { op: _op, projectPath, pid, ...params } = asParams(args);
|
|
32505
32715
|
const announced = runtimesAnnounced();
|
|
32506
|
-
const choice = chooseRuntime(announced.running, typeof projectPath === "string" ? projectPath : this.ownProject ?? undefined, announced.unspoken);
|
|
32716
|
+
const choice = chooseRuntime(announced.running, typeof projectPath === "string" ? projectPath : this.ownProject ?? undefined, announced.unspoken, typeof pid === "number" ? pid : undefined);
|
|
32507
32717
|
if ("problem" in choice) {
|
|
32508
32718
|
return this.createErrorResponse(choice.problem);
|
|
32509
32719
|
}
|
|
32510
|
-
const held = this.knownHoldOn(choice.endpoint);
|
|
32720
|
+
const held = this.knownHoldOn(choice.endpoint, announced.running);
|
|
32511
32721
|
if (held !== null) {
|
|
32512
32722
|
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.`, [
|
|
32513
32723
|
"debug_control continue lets it go, after which this call answers",
|
|
@@ -32569,7 +32779,7 @@ class GodotServer {
|
|
|
32569
32779
|
const includeHidden = readBoolean(args, "includeHidden");
|
|
32570
32780
|
return await this.handleRuntimeCommand("find_nodes", {
|
|
32571
32781
|
...filters,
|
|
32572
|
-
|
|
32782
|
+
...whichGame(args),
|
|
32573
32783
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
32574
32784
|
limit: readPositiveNumber(args, "limit") ?? 100,
|
|
32575
32785
|
...property === undefined ? {} : { property },
|
|
@@ -32580,18 +32790,18 @@ class GodotServer {
|
|
|
32580
32790
|
if (op === "frames") {
|
|
32581
32791
|
const frames = readPositiveNumber(args, "frames") ?? 1;
|
|
32582
32792
|
const waited = patienceForFrames(frames, this.runtimeTimeoutMs());
|
|
32583
|
-
return await this.handleRuntimeCommand("wait_frames", {
|
|
32793
|
+
return await this.handleRuntimeCommand("wait_frames", { ...whichGame(args), frames }, waited);
|
|
32584
32794
|
}
|
|
32585
32795
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 5000;
|
|
32586
32796
|
const nodePath = readNonEmptyString(args, "nodePath") ?? "";
|
|
32587
32797
|
const patience = Math.max(this.runtimeTimeoutMs(), timeoutMs + 5000);
|
|
32588
32798
|
return op === "signal" ? await this.handleRuntimeCommand("wait_signal", {
|
|
32589
|
-
|
|
32799
|
+
...whichGame(args),
|
|
32590
32800
|
path: nodePath,
|
|
32591
32801
|
signal: readString(args, "signal") ?? "",
|
|
32592
32802
|
timeout_ms: timeoutMs
|
|
32593
32803
|
}, patience) : await this.handleRuntimeCommand("wait_until", {
|
|
32594
|
-
|
|
32804
|
+
...whichGame(args),
|
|
32595
32805
|
path: nodePath,
|
|
32596
32806
|
property: readString(args, "property") ?? "",
|
|
32597
32807
|
value: args["value"],
|