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/index.js
CHANGED
|
@@ -10175,15 +10175,15 @@ import {
|
|
|
10175
10175
|
closeSync,
|
|
10176
10176
|
existsSync as existsSync9,
|
|
10177
10177
|
fstatSync,
|
|
10178
|
-
mkdirSync as
|
|
10178
|
+
mkdirSync as mkdirSync7,
|
|
10179
10179
|
mkdtempSync as mkdtempSync2,
|
|
10180
10180
|
openSync as openSync2,
|
|
10181
10181
|
readdirSync as readdirSync6,
|
|
10182
|
-
readFileSync as
|
|
10182
|
+
readFileSync as readFileSync13,
|
|
10183
10183
|
readSync
|
|
10184
10184
|
} from "node:fs";
|
|
10185
10185
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
10186
|
-
import { basename as basename2, dirname as
|
|
10186
|
+
import { basename as basename2, dirname as dirname6, join as join13, normalize as normalize2, relative as relative3 } from "node:path";
|
|
10187
10187
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
10188
10188
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
10189
10189
|
import { promisify as promisify4 } from "node:util";
|
|
@@ -24751,16 +24751,135 @@ class StdioServerTransport {
|
|
|
24751
24751
|
}
|
|
24752
24752
|
}
|
|
24753
24753
|
|
|
24754
|
-
// src/
|
|
24754
|
+
// src/breakpoint-note.ts
|
|
24755
24755
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
24756
|
-
import { dirname, join as join2 } from "node:path";
|
|
24756
|
+
import { dirname, isAbsolute as isAbsolute2, join as join2, relative as relative2 } from "node:path";
|
|
24757
|
+
|
|
24758
|
+
// src/paths.ts
|
|
24759
|
+
import { realpathSync } from "node:fs";
|
|
24760
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
24761
|
+
var RESOURCE_SCHEME = "res://";
|
|
24762
|
+
var OTHER_SCHEME = /^[a-zA-Z][a-zA-Z\d+.-]+:\/\//;
|
|
24763
|
+
function realPathOr(path) {
|
|
24764
|
+
try {
|
|
24765
|
+
return realpathSync.native(path);
|
|
24766
|
+
} catch {
|
|
24767
|
+
return path;
|
|
24768
|
+
}
|
|
24769
|
+
}
|
|
24770
|
+
function isSameDirectory(onePath, otherPath) {
|
|
24771
|
+
return relative(realPathOr(resolve(onePath)), realPathOr(resolve(otherPath))) === "";
|
|
24772
|
+
}
|
|
24773
|
+
function isWithinRoot(rootPath, candidatePath) {
|
|
24774
|
+
const step = relative(resolve(rootPath), resolve(candidatePath));
|
|
24775
|
+
return step !== ".." && !step.startsWith(`..${sep}`) && !isAbsolute(step);
|
|
24776
|
+
}
|
|
24777
|
+
function resolveWithinProject(projectPath, candidatePath) {
|
|
24778
|
+
const trimmed = candidatePath.trim();
|
|
24779
|
+
if (trimmed === "") {
|
|
24780
|
+
return { ok: false, reason: "Path is empty." };
|
|
24781
|
+
}
|
|
24782
|
+
if (trimmed.includes("\x00")) {
|
|
24783
|
+
return { ok: false, reason: "Path contains a null byte." };
|
|
24784
|
+
}
|
|
24785
|
+
const withoutScheme = trimmed.startsWith(RESOURCE_SCHEME) ? trimmed.slice(RESOURCE_SCHEME.length) : trimmed;
|
|
24786
|
+
const scheme = OTHER_SCHEME.exec(withoutScheme);
|
|
24787
|
+
if (scheme) {
|
|
24788
|
+
return {
|
|
24789
|
+
ok: false,
|
|
24790
|
+
reason: `Path '${trimmed}' uses the '${scheme[0]}' scheme, which names somewhere other than this project.`
|
|
24791
|
+
};
|
|
24792
|
+
}
|
|
24793
|
+
if (withoutScheme.startsWith("/") || withoutScheme.startsWith("\\") || isAbsolute(withoutScheme)) {
|
|
24794
|
+
return {
|
|
24795
|
+
ok: false,
|
|
24796
|
+
reason: `Path '${trimmed}' is absolute; give a path inside the project, such as 'scenes/main.tscn'.`
|
|
24797
|
+
};
|
|
24798
|
+
}
|
|
24799
|
+
if (withoutScheme.includes(":")) {
|
|
24800
|
+
return {
|
|
24801
|
+
ok: false,
|
|
24802
|
+
reason: `Path '${trimmed}' contains ':', which no file inside a project is named with.`
|
|
24803
|
+
};
|
|
24804
|
+
}
|
|
24805
|
+
const root = resolve(projectPath);
|
|
24806
|
+
const absolutePath = resolve(root, withoutScheme);
|
|
24807
|
+
if (!isWithinRoot(root, absolutePath)) {
|
|
24808
|
+
return { ok: false, reason: `Path '${trimmed}' resolves outside the project directory.` };
|
|
24809
|
+
}
|
|
24810
|
+
const relativePath = relative(root, absolutePath).split(sep).join("/");
|
|
24811
|
+
if (relativePath === "") {
|
|
24812
|
+
return { ok: false, reason: `Path '${trimmed}' is the project directory itself, not a file in it.` };
|
|
24813
|
+
}
|
|
24814
|
+
return { ok: true, absolutePath, relativePath };
|
|
24815
|
+
}
|
|
24816
|
+
|
|
24817
|
+
// src/breakpoint-note.ts
|
|
24818
|
+
function breakpointNotePath(projectPath) {
|
|
24819
|
+
return join2(projectPath, ".godot", "gdharness-breakpoints.json");
|
|
24820
|
+
}
|
|
24821
|
+
function writeBreakpointNote(projectPath, held) {
|
|
24822
|
+
const path = breakpointNotePath(projectPath);
|
|
24823
|
+
const root = realPathOr(projectPath);
|
|
24824
|
+
const inside = held.map((one) => ({ scriptPath: realPathOr(one.scriptPath), lines: one.lines })).filter((one) => one.lines.length > 0 && withinProject(root, one.scriptPath));
|
|
24825
|
+
try {
|
|
24826
|
+
if (inside.length === 0) {
|
|
24827
|
+
rmSync(path, { force: true });
|
|
24828
|
+
return;
|
|
24829
|
+
}
|
|
24830
|
+
const note = {
|
|
24831
|
+
breakpoints: inside.map((one) => ({
|
|
24832
|
+
script: relative2(root, one.scriptPath).replaceAll("\\", "/"),
|
|
24833
|
+
lines: [...one.lines]
|
|
24834
|
+
}))
|
|
24835
|
+
};
|
|
24836
|
+
mkdirSync2(dirname(path), { recursive: true });
|
|
24837
|
+
writeFileSync2(path, `${JSON.stringify(note, null, 2)}
|
|
24838
|
+
`, "utf8");
|
|
24839
|
+
} catch (error) {
|
|
24840
|
+
console.error(`[SERVER] Could not record the breakpoints at ${path}: ${errorMessage(error)}`);
|
|
24841
|
+
}
|
|
24842
|
+
}
|
|
24843
|
+
function readBreakpointNote(projectPath) {
|
|
24844
|
+
try {
|
|
24845
|
+
const parsed = JSON.parse(readFileSync3(breakpointNotePath(projectPath), "utf8"));
|
|
24846
|
+
if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.breakpoints)) {
|
|
24847
|
+
return [];
|
|
24848
|
+
}
|
|
24849
|
+
const held = [];
|
|
24850
|
+
for (const entry of parsed.breakpoints) {
|
|
24851
|
+
if (typeof entry !== "object" || entry === null) {
|
|
24852
|
+
continue;
|
|
24853
|
+
}
|
|
24854
|
+
const { script, lines: listed } = entry;
|
|
24855
|
+
if (typeof script !== "string" || isAbsolute2(script) || script.split("/").includes("..")) {
|
|
24856
|
+
continue;
|
|
24857
|
+
}
|
|
24858
|
+
const lines = Array.isArray(listed) ? listed.filter((line) => Number.isInteger(line) && line > 0) : [];
|
|
24859
|
+
if (lines.length > 0) {
|
|
24860
|
+
held.push({ scriptPath: realPathOr(join2(projectPath, script)), lines });
|
|
24861
|
+
}
|
|
24862
|
+
}
|
|
24863
|
+
return held;
|
|
24864
|
+
} catch {
|
|
24865
|
+
return [];
|
|
24866
|
+
}
|
|
24867
|
+
}
|
|
24868
|
+
function withinProject(projectPath, scriptPath) {
|
|
24869
|
+
const rel = relative2(projectPath, scriptPath);
|
|
24870
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute2(rel);
|
|
24871
|
+
}
|
|
24872
|
+
|
|
24873
|
+
// src/bridge-announce.ts
|
|
24874
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
24875
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
24757
24876
|
function announcementPath(projectPath) {
|
|
24758
|
-
return
|
|
24877
|
+
return join3(projectPath, ".godot", "gdharness-bridge.json");
|
|
24759
24878
|
}
|
|
24760
24879
|
var BRIDGE_ANNOUNCE_PROTOCOL = 1;
|
|
24761
24880
|
function readAnnouncement(path) {
|
|
24762
24881
|
try {
|
|
24763
|
-
const parsed = JSON.parse(
|
|
24882
|
+
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
24764
24883
|
if (typeof parsed !== "object" || parsed === null) {
|
|
24765
24884
|
return null;
|
|
24766
24885
|
}
|
|
@@ -24779,7 +24898,7 @@ function announcementIsOurs(path) {
|
|
|
24779
24898
|
function announceBridge(projectPath, bridge) {
|
|
24780
24899
|
const path = announcementPath(projectPath);
|
|
24781
24900
|
try {
|
|
24782
|
-
|
|
24901
|
+
mkdirSync3(dirname2(path), { recursive: true });
|
|
24783
24902
|
const announcement = {
|
|
24784
24903
|
protocol: BRIDGE_ANNOUNCE_PROTOCOL,
|
|
24785
24904
|
host: bridge.host,
|
|
@@ -24788,7 +24907,7 @@ function announceBridge(projectPath, bridge) {
|
|
|
24788
24907
|
version: bridge.version,
|
|
24789
24908
|
startedAt: new Date().toISOString()
|
|
24790
24909
|
};
|
|
24791
|
-
|
|
24910
|
+
writeFileSync3(path, `${JSON.stringify(announcement, null, 2)}
|
|
24792
24911
|
`, "utf8");
|
|
24793
24912
|
return path;
|
|
24794
24913
|
} catch (error) {
|
|
@@ -24801,31 +24920,31 @@ function withdrawBridge(path) {
|
|
|
24801
24920
|
return;
|
|
24802
24921
|
}
|
|
24803
24922
|
try {
|
|
24804
|
-
|
|
24923
|
+
rmSync2(path, { force: true });
|
|
24805
24924
|
} catch (error) {
|
|
24806
24925
|
console.error(`[SERVER] Could not withdraw the editor bridge announcement: ${errorMessage(error)}`);
|
|
24807
24926
|
}
|
|
24808
24927
|
}
|
|
24809
24928
|
|
|
24810
24929
|
// src/class-cache.ts
|
|
24811
|
-
import { existsSync as existsSync2, readdirSync, readFileSync as
|
|
24812
|
-
import { join as
|
|
24930
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync5, statSync } from "node:fs";
|
|
24931
|
+
import { join as join4 } from "node:path";
|
|
24813
24932
|
var DECLARATION = /^(?:@[a-z_]+(?:\([^)\n]*\))?\s+)*class_name\s+([A-Za-z_][A-Za-z0-9_]*)/m;
|
|
24814
24933
|
function declaredClasses(projectPath) {
|
|
24815
24934
|
const declared = new Map;
|
|
24816
24935
|
const visit = (directory, prefix) => {
|
|
24817
|
-
if (existsSync2(
|
|
24936
|
+
if (existsSync2(join4(directory, ".gdignore"))) {
|
|
24818
24937
|
return;
|
|
24819
24938
|
}
|
|
24820
24939
|
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
24821
24940
|
if (entry.name.startsWith(".")) {
|
|
24822
24941
|
continue;
|
|
24823
24942
|
}
|
|
24824
|
-
const path =
|
|
24943
|
+
const path = join4(directory, entry.name);
|
|
24825
24944
|
if (entry.isDirectory()) {
|
|
24826
24945
|
visit(path, `${prefix}${entry.name}/`);
|
|
24827
24946
|
} else if (entry.isFile() && entry.name.endsWith(".gd")) {
|
|
24828
|
-
const found = DECLARATION.exec(
|
|
24947
|
+
const found = DECLARATION.exec(readFileSync5(path, "utf8"));
|
|
24829
24948
|
if (found?.[1]) {
|
|
24830
24949
|
declared.set(found[1], `res://${prefix}${entry.name}`);
|
|
24831
24950
|
}
|
|
@@ -24836,12 +24955,12 @@ function declaredClasses(projectPath) {
|
|
|
24836
24955
|
return declared;
|
|
24837
24956
|
}
|
|
24838
24957
|
function cachedClasses(projectPath) {
|
|
24839
|
-
const cache =
|
|
24958
|
+
const cache = join4(projectPath, ".godot", "global_script_class_cache.cfg");
|
|
24840
24959
|
if (!existsSync2(cache)) {
|
|
24841
24960
|
return null;
|
|
24842
24961
|
}
|
|
24843
24962
|
const listed = new Map;
|
|
24844
|
-
const text =
|
|
24963
|
+
const text = readFileSync5(cache, "utf8");
|
|
24845
24964
|
for (const entry of text.matchAll(/"class":\s*&"([^"]+)"[\s\S]*?"path":\s*"([^"]+)"/g)) {
|
|
24846
24965
|
listed.set(entry[1] ?? "", entry[2] ?? "");
|
|
24847
24966
|
}
|
|
@@ -24926,7 +25045,7 @@ function uncachedClassNote(types) {
|
|
|
24926
25045
|
return `${types.join(", ")} ${one ? "is declared" : "are declared"} in this project and missing from ` + `.godot/global_script_class_cache.cfg, so a game launched now would not resolve ${one ? "it" : "them"} ` + "either. Try editor_rescan first: it asks the editor already running and starts nothing, and it " + "cleared this in 314ms in a second project with a 31-worker fan-out still importing. It is not " + "always enough, because a file another engine has already imported reads as settled and the " + "walk does not look inside it. project_import refresh_classes rewrites the cache from the " + "declarations on disk and does reach that case, at the cost of a short headless engine, which " + "is worth knowing when something else is already running against this project.";
|
|
24927
25046
|
}
|
|
24928
25047
|
function cacheWrittenAt(projectPath) {
|
|
24929
|
-
const cache =
|
|
25048
|
+
const cache = join4(projectPath, ".godot", "global_script_class_cache.cfg");
|
|
24930
25049
|
try {
|
|
24931
25050
|
return statSync(cache).mtimeMs;
|
|
24932
25051
|
} catch {
|
|
@@ -24950,17 +25069,17 @@ function unseenByEditor(projectPath, editorHolds) {
|
|
|
24950
25069
|
}
|
|
24951
25070
|
|
|
24952
25071
|
// src/config-pin.ts
|
|
24953
|
-
import { existsSync as existsSync3, readFileSync as
|
|
24954
|
-
import { join as
|
|
25072
|
+
import { existsSync as existsSync3, readFileSync as readFileSync6 } from "node:fs";
|
|
25073
|
+
import { join as join5 } from "node:path";
|
|
24955
25074
|
var PINNED = /\bgdharness@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\b/g;
|
|
24956
25075
|
function versionsNamedInConfig(projectPath) {
|
|
24957
|
-
const config =
|
|
25076
|
+
const config = join5(projectPath, ".mcp.json");
|
|
24958
25077
|
if (!existsSync3(config)) {
|
|
24959
25078
|
return [];
|
|
24960
25079
|
}
|
|
24961
25080
|
let text;
|
|
24962
25081
|
try {
|
|
24963
|
-
text =
|
|
25082
|
+
text = readFileSync6(config, "utf8");
|
|
24964
25083
|
JSON.parse(text);
|
|
24965
25084
|
} catch {
|
|
24966
25085
|
return [];
|
|
@@ -25127,7 +25246,10 @@ class GodotDAPClient {
|
|
|
25127
25246
|
attached = false;
|
|
25128
25247
|
lastThreadId = 1;
|
|
25129
25248
|
halt = null;
|
|
25249
|
+
holdKnown = false;
|
|
25130
25250
|
breakpoints = new Map;
|
|
25251
|
+
inEditor = new Map;
|
|
25252
|
+
onBreakpointsChanged = null;
|
|
25131
25253
|
constructor(port = portFromEnv("GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT), host = "127.0.0.1") {
|
|
25132
25254
|
this.port = port;
|
|
25133
25255
|
this.host = host;
|
|
@@ -25172,11 +25294,7 @@ class GodotDAPClient {
|
|
|
25172
25294
|
this.failPendingRequests(error);
|
|
25173
25295
|
});
|
|
25174
25296
|
socket.on("close", () => {
|
|
25175
|
-
this.
|
|
25176
|
-
this.initialized = false;
|
|
25177
|
-
this.attached = false;
|
|
25178
|
-
this.halt = null;
|
|
25179
|
-
this.socket = null;
|
|
25297
|
+
this.forgetConnection();
|
|
25180
25298
|
this.failPendingRequests(new Error("DAP connection closed"));
|
|
25181
25299
|
});
|
|
25182
25300
|
resolve();
|
|
@@ -25192,10 +25310,7 @@ class GodotDAPClient {
|
|
|
25192
25310
|
}
|
|
25193
25311
|
async abandon() {
|
|
25194
25312
|
if (!this.socket) {
|
|
25195
|
-
this.
|
|
25196
|
-
this.initialized = false;
|
|
25197
|
-
this.attached = false;
|
|
25198
|
-
this.halt = null;
|
|
25313
|
+
this.forgetConnection();
|
|
25199
25314
|
return;
|
|
25200
25315
|
}
|
|
25201
25316
|
await new Promise((resolve) => {
|
|
@@ -25215,11 +25330,16 @@ class GodotDAPClient {
|
|
|
25215
25330
|
resolve();
|
|
25216
25331
|
}, 500);
|
|
25217
25332
|
});
|
|
25333
|
+
this.forgetConnection();
|
|
25334
|
+
}
|
|
25335
|
+
forgetConnection() {
|
|
25218
25336
|
this.socket = null;
|
|
25219
25337
|
this.connected = false;
|
|
25220
25338
|
this.initialized = false;
|
|
25221
25339
|
this.attached = false;
|
|
25222
25340
|
this.halt = null;
|
|
25341
|
+
this.holdKnown = false;
|
|
25342
|
+
this.inEditor.clear();
|
|
25223
25343
|
}
|
|
25224
25344
|
async ensureConnected() {
|
|
25225
25345
|
if (!this.connected) {
|
|
@@ -25308,6 +25428,37 @@ class GodotDAPClient {
|
|
|
25308
25428
|
description: said(body, "description"),
|
|
25309
25429
|
text: said(body, "text")
|
|
25310
25430
|
};
|
|
25431
|
+
this.holdKnown = true;
|
|
25432
|
+
return;
|
|
25433
|
+
}
|
|
25434
|
+
if (eventName === "continued") {
|
|
25435
|
+
this.halt = null;
|
|
25436
|
+
this.holdKnown = true;
|
|
25437
|
+
return;
|
|
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);
|
|
25311
25462
|
return;
|
|
25312
25463
|
}
|
|
25313
25464
|
if (eventName === "terminated" || eventName === "exited") {
|
|
@@ -25346,6 +25497,43 @@ class GodotDAPClient {
|
|
|
25346
25497
|
await this.sendRequest("attach", {});
|
|
25347
25498
|
await this.sendRequest("configurationDone", {});
|
|
25348
25499
|
this.attached = true;
|
|
25500
|
+
if (!this.holdKnown) {
|
|
25501
|
+
await this.learnWhetherHeld();
|
|
25502
|
+
}
|
|
25503
|
+
}
|
|
25504
|
+
async learnWhetherHeld() {
|
|
25505
|
+
try {
|
|
25506
|
+
const threads = await this.sendRequest("threads");
|
|
25507
|
+
const listed = threads["threads"];
|
|
25508
|
+
const first = Array.isArray(listed) ? listed[0]?.["id"] : undefined;
|
|
25509
|
+
if (typeof first !== "number") {
|
|
25510
|
+
return;
|
|
25511
|
+
}
|
|
25512
|
+
this.lastThreadId = first;
|
|
25513
|
+
const response = await this.sendRequest("stackTrace", { threadId: first, startFrame: 0, levels: 1 });
|
|
25514
|
+
if (this.holdKnown) {
|
|
25515
|
+
return;
|
|
25516
|
+
}
|
|
25517
|
+
const frames = response["stackFrames"];
|
|
25518
|
+
if (Array.isArray(frames) && frames.length > 0) {
|
|
25519
|
+
this.halt = {
|
|
25520
|
+
reason: "attached",
|
|
25521
|
+
description: "held when this session attached; the stop that held it was reported to an earlier one",
|
|
25522
|
+
text: ""
|
|
25523
|
+
};
|
|
25524
|
+
this.holdKnown = true;
|
|
25525
|
+
}
|
|
25526
|
+
} catch {}
|
|
25527
|
+
}
|
|
25528
|
+
holdIsKnown() {
|
|
25529
|
+
return this.holdKnown;
|
|
25530
|
+
}
|
|
25531
|
+
learnedRunning() {
|
|
25532
|
+
if (this.holdKnown) {
|
|
25533
|
+
return;
|
|
25534
|
+
}
|
|
25535
|
+
this.halt = null;
|
|
25536
|
+
this.holdKnown = true;
|
|
25349
25537
|
}
|
|
25350
25538
|
setOutputSink(sink) {
|
|
25351
25539
|
this.onOutput = sink;
|
|
@@ -25358,38 +25546,96 @@ class GodotDAPClient {
|
|
|
25358
25546
|
return lines;
|
|
25359
25547
|
}
|
|
25360
25548
|
async setBreakpoint(filePath, line) {
|
|
25361
|
-
|
|
25362
|
-
|
|
25363
|
-
|
|
25364
|
-
fileBreakpoints.add(line);
|
|
25365
|
-
this.breakpoints.set(filePath, fileBreakpoints);
|
|
25366
|
-
const lines = Array.from(fileBreakpoints).sort((a, b) => a - b);
|
|
25367
|
-
return await this.sendRequest("setBreakpoints", {
|
|
25368
|
-
source: { path: filePath },
|
|
25369
|
-
breakpoints: lines.map((breakpointLine) => ({ line: breakpointLine }))
|
|
25370
|
-
});
|
|
25549
|
+
const wanted = new Set(this.breakpoints.get(filePath) ?? []);
|
|
25550
|
+
wanted.add(line);
|
|
25551
|
+
return await this.sendBreakpoints(filePath, wanted);
|
|
25371
25552
|
}
|
|
25372
25553
|
async removeBreakpoint(filePath, line) {
|
|
25554
|
+
const wanted = new Set(this.breakpoints.get(filePath) ?? []);
|
|
25555
|
+
wanted.delete(line);
|
|
25556
|
+
return await this.sendBreakpoints(filePath, wanted, line);
|
|
25557
|
+
}
|
|
25558
|
+
async sendBreakpoints(filePath, mine, clearing) {
|
|
25373
25559
|
await this.ensureConnected();
|
|
25374
25560
|
await this.initialize();
|
|
25375
|
-
const
|
|
25376
|
-
|
|
25377
|
-
|
|
25378
|
-
this.breakpoints.delete(filePath);
|
|
25379
|
-
} else {
|
|
25380
|
-
this.breakpoints.set(filePath, fileBreakpoints);
|
|
25561
|
+
const lines = new Set([...mine, ...this.inEditor.get(filePath) ?? []]);
|
|
25562
|
+
if (clearing !== undefined) {
|
|
25563
|
+
lines.delete(clearing);
|
|
25381
25564
|
}
|
|
25382
|
-
const
|
|
25383
|
-
|
|
25565
|
+
const sorted = Array.from(lines).sort((a, b) => a - b);
|
|
25566
|
+
const answer = await this.sendRequest("setBreakpoints", {
|
|
25384
25567
|
source: { path: filePath },
|
|
25385
|
-
breakpoints:
|
|
25568
|
+
breakpoints: sorted.map((breakpointLine) => ({ line: breakpointLine }))
|
|
25386
25569
|
});
|
|
25570
|
+
if (mine.size === 0) {
|
|
25571
|
+
this.breakpoints.delete(filePath);
|
|
25572
|
+
} else {
|
|
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);
|
|
25579
|
+
}
|
|
25580
|
+
return answer;
|
|
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
|
+
}
|
|
25596
|
+
async reapplyBreakpoints() {
|
|
25597
|
+
const applied = [];
|
|
25598
|
+
const refused = [];
|
|
25599
|
+
for (const [filePath, lines] of Array.from(this.breakpoints)) {
|
|
25600
|
+
try {
|
|
25601
|
+
await this.sendBreakpoints(filePath, lines);
|
|
25602
|
+
applied.push({ scriptPath: filePath, lines: Array.from(lines).sort((a, b) => a - b) });
|
|
25603
|
+
} catch (error) {
|
|
25604
|
+
this.breakpoints.delete(filePath);
|
|
25605
|
+
refused.push({
|
|
25606
|
+
scriptPath: filePath,
|
|
25607
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
25608
|
+
});
|
|
25609
|
+
}
|
|
25610
|
+
}
|
|
25611
|
+
if (refused.length > 0) {
|
|
25612
|
+
this.onBreakpointsChanged?.(this.breakpointsHeld());
|
|
25613
|
+
}
|
|
25614
|
+
return { applied, refused };
|
|
25615
|
+
}
|
|
25616
|
+
breakpointsHeld() {
|
|
25617
|
+
return Array.from(this.breakpoints, ([scriptPath, lines]) => ({
|
|
25618
|
+
scriptPath,
|
|
25619
|
+
lines: Array.from(lines).sort((a, b) => a - b)
|
|
25620
|
+
}));
|
|
25621
|
+
}
|
|
25622
|
+
holdBreakpoints(held) {
|
|
25623
|
+
for (const { scriptPath, lines } of held) {
|
|
25624
|
+
const fileBreakpoints = this.breakpoints.get(scriptPath) ?? new Set;
|
|
25625
|
+
for (const line of lines) {
|
|
25626
|
+
fileBreakpoints.add(line);
|
|
25627
|
+
}
|
|
25628
|
+
if (fileBreakpoints.size > 0) {
|
|
25629
|
+
this.breakpoints.set(scriptPath, fileBreakpoints);
|
|
25630
|
+
}
|
|
25631
|
+
}
|
|
25387
25632
|
}
|
|
25388
25633
|
async continue(threadId) {
|
|
25389
25634
|
await this.attach();
|
|
25390
25635
|
const resolvedThreadId = await this.resolveThreadId(threadId);
|
|
25391
25636
|
await this.sendRequest("continue", { threadId: resolvedThreadId });
|
|
25392
25637
|
this.halt = null;
|
|
25638
|
+
this.holdKnown = true;
|
|
25393
25639
|
}
|
|
25394
25640
|
async stepOver(threadId) {
|
|
25395
25641
|
await this.attach();
|
|
@@ -25488,11 +25734,7 @@ class GodotDAPClient {
|
|
|
25488
25734
|
}
|
|
25489
25735
|
failOversizedStream(detail) {
|
|
25490
25736
|
const socket = this.socket;
|
|
25491
|
-
this.
|
|
25492
|
-
this.connected = false;
|
|
25493
|
-
this.initialized = false;
|
|
25494
|
-
this.attached = false;
|
|
25495
|
-
this.halt = null;
|
|
25737
|
+
this.forgetConnection();
|
|
25496
25738
|
this.reader = new FrameReader;
|
|
25497
25739
|
socket?.destroy();
|
|
25498
25740
|
this.failPendingRequests(new Error(`Godot DAP ${detail}. The connection was dropped.`));
|
|
@@ -25571,14 +25813,10 @@ async function handleDAPTool(client, toolName, args) {
|
|
|
25571
25813
|
}
|
|
25572
25814
|
} catch (error) {
|
|
25573
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.";
|
|
25574
25817
|
return {
|
|
25575
25818
|
isError: true,
|
|
25576
|
-
content: [
|
|
25577
|
-
{
|
|
25578
|
-
type: "text",
|
|
25579
|
-
text: `DAP tool '${toolName}' failed: ${message}. ` + "The debug tools answer for a game the editor is playing: start one with editor_run."
|
|
25580
|
-
}
|
|
25581
|
-
]
|
|
25819
|
+
content: [{ type: "text", text: `DAP tool '${toolName}' failed: ${message}. ${cure}` }]
|
|
25582
25820
|
};
|
|
25583
25821
|
}
|
|
25584
25822
|
}
|
|
@@ -25715,65 +25953,6 @@ var import_subprotocol = __toESM(require_subprotocol(), 1);
|
|
|
25715
25953
|
var import_websocket = __toESM(require_websocket(), 1);
|
|
25716
25954
|
var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
25717
25955
|
|
|
25718
|
-
// src/paths.ts
|
|
25719
|
-
import { realpathSync } from "node:fs";
|
|
25720
|
-
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
25721
|
-
var RESOURCE_SCHEME = "res://";
|
|
25722
|
-
var OTHER_SCHEME = /^[a-zA-Z][a-zA-Z\d+.-]+:\/\//;
|
|
25723
|
-
function realPathOr(path) {
|
|
25724
|
-
try {
|
|
25725
|
-
return realpathSync.native(path);
|
|
25726
|
-
} catch {
|
|
25727
|
-
return path;
|
|
25728
|
-
}
|
|
25729
|
-
}
|
|
25730
|
-
function isSameDirectory(onePath, otherPath) {
|
|
25731
|
-
return relative(realPathOr(resolve(onePath)), realPathOr(resolve(otherPath))) === "";
|
|
25732
|
-
}
|
|
25733
|
-
function isWithinRoot(rootPath, candidatePath) {
|
|
25734
|
-
const step = relative(resolve(rootPath), resolve(candidatePath));
|
|
25735
|
-
return step !== ".." && !step.startsWith(`..${sep}`) && !isAbsolute(step);
|
|
25736
|
-
}
|
|
25737
|
-
function resolveWithinProject(projectPath, candidatePath) {
|
|
25738
|
-
const trimmed = candidatePath.trim();
|
|
25739
|
-
if (trimmed === "") {
|
|
25740
|
-
return { ok: false, reason: "Path is empty." };
|
|
25741
|
-
}
|
|
25742
|
-
if (trimmed.includes("\x00")) {
|
|
25743
|
-
return { ok: false, reason: "Path contains a null byte." };
|
|
25744
|
-
}
|
|
25745
|
-
const withoutScheme = trimmed.startsWith(RESOURCE_SCHEME) ? trimmed.slice(RESOURCE_SCHEME.length) : trimmed;
|
|
25746
|
-
const scheme = OTHER_SCHEME.exec(withoutScheme);
|
|
25747
|
-
if (scheme) {
|
|
25748
|
-
return {
|
|
25749
|
-
ok: false,
|
|
25750
|
-
reason: `Path '${trimmed}' uses the '${scheme[0]}' scheme, which names somewhere other than this project.`
|
|
25751
|
-
};
|
|
25752
|
-
}
|
|
25753
|
-
if (withoutScheme.startsWith("/") || withoutScheme.startsWith("\\") || isAbsolute(withoutScheme)) {
|
|
25754
|
-
return {
|
|
25755
|
-
ok: false,
|
|
25756
|
-
reason: `Path '${trimmed}' is absolute; give a path inside the project, such as 'scenes/main.tscn'.`
|
|
25757
|
-
};
|
|
25758
|
-
}
|
|
25759
|
-
if (withoutScheme.includes(":")) {
|
|
25760
|
-
return {
|
|
25761
|
-
ok: false,
|
|
25762
|
-
reason: `Path '${trimmed}' contains ':', which no file inside a project is named with.`
|
|
25763
|
-
};
|
|
25764
|
-
}
|
|
25765
|
-
const root = resolve(projectPath);
|
|
25766
|
-
const absolutePath = resolve(root, withoutScheme);
|
|
25767
|
-
if (!isWithinRoot(root, absolutePath)) {
|
|
25768
|
-
return { ok: false, reason: `Path '${trimmed}' resolves outside the project directory.` };
|
|
25769
|
-
}
|
|
25770
|
-
const relativePath = relative(root, absolutePath).split(sep).join("/");
|
|
25771
|
-
if (relativePath === "") {
|
|
25772
|
-
return { ok: false, reason: `Path '${trimmed}' is the project directory itself, not a file in it.` };
|
|
25773
|
-
}
|
|
25774
|
-
return { ok: true, absolutePath, relativePath };
|
|
25775
|
-
}
|
|
25776
|
-
|
|
25777
25956
|
// src/godot-bridge.ts
|
|
25778
25957
|
var DEFAULT_PORT = 6505;
|
|
25779
25958
|
var DEFAULT_HOST = "127.0.0.1";
|
|
@@ -25942,6 +26121,7 @@ class GodotBridge extends EventEmitter {
|
|
|
25942
26121
|
dapPort: this.connectionInfo?.dapPort,
|
|
25943
26122
|
debugPort: this.connectionInfo?.debugPort,
|
|
25944
26123
|
openedByAServer: this.connectionInfo?.openedByAServer,
|
|
26124
|
+
syncsBreakpoints: this.connectionInfo?.syncsBreakpoints,
|
|
25945
26125
|
pendingRequests: this.pendingRequests.size,
|
|
25946
26126
|
queuedResources: this.resourceQueues.size,
|
|
25947
26127
|
listeningSince: this.listeningSince ?? undefined
|
|
@@ -26084,6 +26264,7 @@ class GodotBridge extends EventEmitter {
|
|
|
26084
26264
|
this.connectionInfo.dapPort = servedPort(message.dap_port);
|
|
26085
26265
|
this.connectionInfo.debugPort = servedPort(message.debug_port);
|
|
26086
26266
|
this.connectionInfo.openedByAServer = message.opened_by_a_server === true;
|
|
26267
|
+
this.connectionInfo.syncsBreakpoints = typeof message.syncs_breakpoints === "boolean" ? message.syncs_breakpoints : undefined;
|
|
26087
26268
|
this.log("info", `Godot ready: ${message.project_path}`);
|
|
26088
26269
|
this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
|
|
26089
26270
|
}
|
|
@@ -26265,7 +26446,7 @@ import { promisify } from "node:util";
|
|
|
26265
26446
|
// src/detection.ts
|
|
26266
26447
|
import { existsSync as existsSync4, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
|
|
26267
26448
|
import { homedir as homedir2 } from "node:os";
|
|
26268
|
-
import { join as
|
|
26449
|
+
import { join as join6 } from "node:path";
|
|
26269
26450
|
function resolveHomeDirectory() {
|
|
26270
26451
|
try {
|
|
26271
26452
|
return homedir2();
|
|
@@ -26289,7 +26470,7 @@ function scanDirectoryForGodotBinaries(directory, platform) {
|
|
|
26289
26470
|
if (!pattern.test(name)) {
|
|
26290
26471
|
continue;
|
|
26291
26472
|
}
|
|
26292
|
-
const fullPath =
|
|
26473
|
+
const fullPath = join6(directory, name);
|
|
26293
26474
|
try {
|
|
26294
26475
|
const stat = statSync2(fullPath);
|
|
26295
26476
|
if (stat.isFile()) {
|
|
@@ -26298,7 +26479,7 @@ function scanDirectoryForGodotBinaries(directory, platform) {
|
|
|
26298
26479
|
} catch {}
|
|
26299
26480
|
}
|
|
26300
26481
|
matches.sort((a, b) => b.mtime - a.mtime);
|
|
26301
|
-
return matches.map((m) =>
|
|
26482
|
+
return matches.map((m) => join6(directory, m.name));
|
|
26302
26483
|
}
|
|
26303
26484
|
function conventionalPaths(platform, home) {
|
|
26304
26485
|
const paths = ["godot"];
|
|
@@ -26467,13 +26648,13 @@ class GodotLocator {
|
|
|
26467
26648
|
|
|
26468
26649
|
// src/headless.ts
|
|
26469
26650
|
import { execFile as execFile2 } from "node:child_process";
|
|
26470
|
-
import { mkdtempSync, writeFileSync as
|
|
26651
|
+
import { mkdtempSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
26471
26652
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
26472
|
-
import { join as
|
|
26653
|
+
import { join as join7 } from "node:path";
|
|
26473
26654
|
import { promisify as promisify2 } from "node:util";
|
|
26474
26655
|
|
|
26475
26656
|
// src/scratch.ts
|
|
26476
|
-
import { rmSync as
|
|
26657
|
+
import { rmSync as rmSync3 } from "node:fs";
|
|
26477
26658
|
var SWEPT = { recursive: true, force: true, maxRetries: 20, retryDelay: 250 };
|
|
26478
26659
|
function discardWith(remove, paths) {
|
|
26479
26660
|
const left = [];
|
|
@@ -26491,7 +26672,7 @@ function discardWith(remove, paths) {
|
|
|
26491
26672
|
}
|
|
26492
26673
|
function discard(...paths) {
|
|
26493
26674
|
return discardWith((path) => {
|
|
26494
|
-
|
|
26675
|
+
rmSync3(path, SWEPT);
|
|
26495
26676
|
}, paths);
|
|
26496
26677
|
}
|
|
26497
26678
|
|
|
@@ -26535,12 +26716,12 @@ function reason(stdout, stderr) {
|
|
|
26535
26716
|
return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
|
|
26536
26717
|
}
|
|
26537
26718
|
async function runImport(godotPath, projectPath) {
|
|
26538
|
-
const logDir = mkdtempSync(
|
|
26719
|
+
const logDir = mkdtempSync(join7(tmpdir2(), "gdharness-import-"));
|
|
26539
26720
|
try {
|
|
26540
26721
|
const { stderr } = await run2(godotPath, [
|
|
26541
26722
|
"--headless",
|
|
26542
26723
|
"--log-file",
|
|
26543
|
-
|
|
26724
|
+
join7(logDir, "engine.log"),
|
|
26544
26725
|
"--path",
|
|
26545
26726
|
projectPath,
|
|
26546
26727
|
"--import"
|
|
@@ -26565,13 +26746,13 @@ async function runImport(godotPath, projectPath) {
|
|
|
26565
26746
|
}
|
|
26566
26747
|
}
|
|
26567
26748
|
async function runOperation(engine, operation, params, projectPath) {
|
|
26568
|
-
const paramsDir = mkdtempSync(
|
|
26569
|
-
const paramsFile =
|
|
26570
|
-
|
|
26749
|
+
const paramsDir = mkdtempSync(join7(tmpdir2(), "gdharness-params-"));
|
|
26750
|
+
const paramsFile = join7(paramsDir, `${operation}.json`);
|
|
26751
|
+
writeFileSync4(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
|
|
26571
26752
|
const args = [
|
|
26572
26753
|
"--headless",
|
|
26573
26754
|
"--log-file",
|
|
26574
|
-
|
|
26755
|
+
join7(paramsDir, "engine.log"),
|
|
26575
26756
|
"--path",
|
|
26576
26757
|
projectPath,
|
|
26577
26758
|
"--script",
|
|
@@ -27000,7 +27181,7 @@ function orphansPrinted(printed) {
|
|
|
27000
27181
|
import { realpathSync as realpathSync2 } from "node:fs";
|
|
27001
27182
|
import { readFile, realpath } from "node:fs/promises";
|
|
27002
27183
|
import { createConnection as createConnection2 } from "node:net";
|
|
27003
|
-
import { dirname as
|
|
27184
|
+
import { dirname as dirname3, resolve as resolve2 } from "node:path";
|
|
27004
27185
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
27005
27186
|
var DEFAULT_LSP_PORT = 6005;
|
|
27006
27187
|
var DIAGNOSTICS_TIMEOUT_MS = 5000;
|
|
@@ -27130,7 +27311,7 @@ class GodotLSPClient {
|
|
|
27130
27311
|
if (this.initialized) {
|
|
27131
27312
|
return;
|
|
27132
27313
|
}
|
|
27133
|
-
const rootPath = this.rootPath ??
|
|
27314
|
+
const rootPath = this.rootPath ?? dirname3(resolve2(filePath));
|
|
27134
27315
|
await this.initialize(rootPath);
|
|
27135
27316
|
}
|
|
27136
27317
|
async sendRequest(method, params) {
|
|
@@ -27566,14 +27747,14 @@ function secondsFromClock(printed) {
|
|
|
27566
27747
|
}
|
|
27567
27748
|
|
|
27568
27749
|
// src/project-scan.ts
|
|
27569
|
-
import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as
|
|
27570
|
-
import { join as
|
|
27750
|
+
import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync7 } from "node:fs";
|
|
27751
|
+
import { join as join8 } from "node:path";
|
|
27571
27752
|
var SKIPPED = new Set(["node_modules"]);
|
|
27572
27753
|
function skipped(name) {
|
|
27573
27754
|
return name.startsWith(".") || SKIPPED.has(name);
|
|
27574
27755
|
}
|
|
27575
27756
|
function steppedOver(directory) {
|
|
27576
|
-
return existsSync6(
|
|
27757
|
+
return existsSync6(join8(directory, ".gdignore"));
|
|
27577
27758
|
}
|
|
27578
27759
|
var ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
|
|
27579
27760
|
function projectStructure(projectPath) {
|
|
@@ -27587,7 +27768,7 @@ function projectStructure(projectPath) {
|
|
|
27587
27768
|
continue;
|
|
27588
27769
|
}
|
|
27589
27770
|
if (entry.isDirectory()) {
|
|
27590
|
-
visit(
|
|
27771
|
+
visit(join8(directory, entry.name));
|
|
27591
27772
|
} else if (entry.isFile()) {
|
|
27592
27773
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
27593
27774
|
if (extension === "tscn") {
|
|
@@ -27618,14 +27799,14 @@ function scriptsWithoutUid(projectPath) {
|
|
|
27618
27799
|
}
|
|
27619
27800
|
const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
27620
27801
|
if (entry.isDirectory()) {
|
|
27621
|
-
visit(
|
|
27802
|
+
visit(join8(directory, entry.name), spelled);
|
|
27622
27803
|
continue;
|
|
27623
27804
|
}
|
|
27624
27805
|
if (!entry.isFile()) {
|
|
27625
27806
|
continue;
|
|
27626
27807
|
}
|
|
27627
27808
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
27628
|
-
if (CARRIES_A_UID.has(extension) && !existsSync6(
|
|
27809
|
+
if (CARRIES_A_UID.has(extension) && !existsSync6(join8(directory, `${entry.name}.uid`))) {
|
|
27629
27810
|
found.push(spelled);
|
|
27630
27811
|
}
|
|
27631
27812
|
}
|
|
@@ -27660,7 +27841,7 @@ function searchProject(projectPath, options) {
|
|
|
27660
27841
|
if (skipped(entry.name)) {
|
|
27661
27842
|
continue;
|
|
27662
27843
|
}
|
|
27663
|
-
const entryPath =
|
|
27844
|
+
const entryPath = join8(directory, entry.name);
|
|
27664
27845
|
if (entry.isDirectory()) {
|
|
27665
27846
|
visit(entryPath);
|
|
27666
27847
|
continue;
|
|
@@ -27671,7 +27852,7 @@ function searchProject(projectPath, options) {
|
|
|
27671
27852
|
}
|
|
27672
27853
|
result.summary.files_searched += 1;
|
|
27673
27854
|
const matches = [];
|
|
27674
|
-
for (const [index, line] of
|
|
27855
|
+
for (const [index, line] of readFileSync7(entryPath, "utf8").split(`
|
|
27675
27856
|
`).entries()) {
|
|
27676
27857
|
if (full()) {
|
|
27677
27858
|
break;
|
|
@@ -27694,7 +27875,7 @@ function searchProject(projectPath, options) {
|
|
|
27694
27875
|
}
|
|
27695
27876
|
|
|
27696
27877
|
// src/resources.ts
|
|
27697
|
-
import { readFileSync as
|
|
27878
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
27698
27879
|
import { extname, resolve as resolve3 } from "node:path";
|
|
27699
27880
|
var STATIC_RESOURCES = [
|
|
27700
27881
|
{
|
|
@@ -27889,7 +28070,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
27889
28070
|
const parsedUri = parseGodotUri(uri);
|
|
27890
28071
|
if (parsedUri.kind === "project-info") {
|
|
27891
28072
|
const projectFilePath = resolveProjectFile(projectPath, "project.godot");
|
|
27892
|
-
const rawProject =
|
|
28073
|
+
const rawProject = readFileSync8(projectFilePath, "utf-8");
|
|
27893
28074
|
const parsedProject = parseProjectGodot(rawProject);
|
|
27894
28075
|
return {
|
|
27895
28076
|
mimeType: "application/json",
|
|
@@ -27898,7 +28079,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
27898
28079
|
}
|
|
27899
28080
|
const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
|
|
27900
28081
|
ensureAllowedExtension(parsedUri.kind, filePath);
|
|
27901
|
-
const text =
|
|
28082
|
+
const text = readFileSync8(filePath, "utf-8");
|
|
27902
28083
|
return {
|
|
27903
28084
|
mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
|
|
27904
28085
|
text
|
|
@@ -27935,16 +28116,16 @@ function setupResourceHandlers(mcp, getProjectPath) {
|
|
|
27935
28116
|
}
|
|
27936
28117
|
|
|
27937
28118
|
// src/restart-note.ts
|
|
27938
|
-
import { mkdirSync as
|
|
27939
|
-
import { dirname as
|
|
28119
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync9, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
28120
|
+
import { dirname as dirname4, join as join9 } from "node:path";
|
|
27940
28121
|
function restartNotePath(projectPath) {
|
|
27941
|
-
return
|
|
28122
|
+
return join9(projectPath, ".godot", "gdharness-restart.json");
|
|
27942
28123
|
}
|
|
27943
28124
|
function noteRestartBegun(note) {
|
|
27944
28125
|
const path = restartNotePath(note.projectPath);
|
|
27945
28126
|
try {
|
|
27946
|
-
|
|
27947
|
-
|
|
28127
|
+
mkdirSync4(dirname4(path), { recursive: true });
|
|
28128
|
+
writeFileSync5(path, `${JSON.stringify(note, null, 2)}
|
|
27948
28129
|
`, "utf8");
|
|
27949
28130
|
} catch (error) {
|
|
27950
28131
|
console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
|
|
@@ -27952,7 +28133,7 @@ function noteRestartBegun(note) {
|
|
|
27952
28133
|
}
|
|
27953
28134
|
function restartOwed(projectPath) {
|
|
27954
28135
|
try {
|
|
27955
|
-
const parsed = JSON.parse(
|
|
28136
|
+
const parsed = JSON.parse(readFileSync9(restartNotePath(projectPath), "utf8"));
|
|
27956
28137
|
if (typeof parsed !== "object" || parsed === null) {
|
|
27957
28138
|
return null;
|
|
27958
28139
|
}
|
|
@@ -27967,7 +28148,7 @@ function restartOwed(projectPath) {
|
|
|
27967
28148
|
}
|
|
27968
28149
|
function restartSettled(projectPath) {
|
|
27969
28150
|
try {
|
|
27970
|
-
|
|
28151
|
+
rmSync4(restartNotePath(projectPath), { force: true });
|
|
27971
28152
|
} catch (error) {
|
|
27972
28153
|
console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
|
|
27973
28154
|
}
|
|
@@ -27975,14 +28156,14 @@ function restartSettled(projectPath) {
|
|
|
27975
28156
|
|
|
27976
28157
|
// src/run-record.ts
|
|
27977
28158
|
import { execFileSync } from "node:child_process";
|
|
27978
|
-
import { mkdirSync as
|
|
27979
|
-
import { basename, join as
|
|
28159
|
+
import { mkdirSync as mkdirSync5, openSync, readdirSync as readdirSync5, readFileSync as readFileSync11, rmSync as rmSync5, statSync as statSync3, writeFileSync as writeFileSync6 } from "node:fs";
|
|
28160
|
+
import { basename, join as join11 } from "node:path";
|
|
27980
28161
|
|
|
27981
28162
|
// src/runtime-client.ts
|
|
27982
|
-
import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as
|
|
28163
|
+
import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync10, unlinkSync } from "node:fs";
|
|
27983
28164
|
import { createConnection as createConnection3 } from "node:net";
|
|
27984
28165
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
27985
|
-
import { join as
|
|
28166
|
+
import { join as join10, resolve as resolve4 } from "node:path";
|
|
27986
28167
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
27987
28168
|
|
|
27988
28169
|
// src/tool-args.ts
|
|
@@ -28050,13 +28231,13 @@ function runtimeDirectory(variables = process.env) {
|
|
|
28050
28231
|
return explicit;
|
|
28051
28232
|
}
|
|
28052
28233
|
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
28053
|
-
return
|
|
28234
|
+
return join10(perUser ?? tmpdir3(), "gdharness");
|
|
28054
28235
|
}
|
|
28055
28236
|
function runtimeDirectories(variables = process.env) {
|
|
28056
28237
|
const candidates = [runtimeDirectory(variables)];
|
|
28057
28238
|
const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
|
|
28058
28239
|
for (const base of fallbacks) {
|
|
28059
|
-
candidates.push(
|
|
28240
|
+
candidates.push(join10(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
|
|
28060
28241
|
}
|
|
28061
28242
|
return [...new Set(candidates.map((path) => resolve4(path)))];
|
|
28062
28243
|
}
|
|
@@ -28071,7 +28252,7 @@ function processAlive(pid) {
|
|
|
28071
28252
|
function parseAnnouncement(file, pid) {
|
|
28072
28253
|
let fields;
|
|
28073
28254
|
try {
|
|
28074
|
-
fields = asParams(JSON.parse(
|
|
28255
|
+
fields = asParams(JSON.parse(readFileSync10(file, "utf8")));
|
|
28075
28256
|
} catch {
|
|
28076
28257
|
return { kind: "rubbish" };
|
|
28077
28258
|
}
|
|
@@ -28115,7 +28296,7 @@ var gone = [];
|
|
|
28115
28296
|
function wentAway(file, pid) {
|
|
28116
28297
|
let project = "";
|
|
28117
28298
|
try {
|
|
28118
|
-
const fields = asParams(JSON.parse(
|
|
28299
|
+
const fields = asParams(JSON.parse(readFileSync10(file, "utf8")));
|
|
28119
28300
|
project = readString(readParams(fields, "project") ?? {}, "path") ?? "";
|
|
28120
28301
|
} catch {
|
|
28121
28302
|
return;
|
|
@@ -28144,7 +28325,7 @@ function announcedIn(directory) {
|
|
|
28144
28325
|
if (!match) {
|
|
28145
28326
|
continue;
|
|
28146
28327
|
}
|
|
28147
|
-
const file =
|
|
28328
|
+
const file = join10(directory, entry);
|
|
28148
28329
|
const pid = Number.parseInt(match[1] ?? "", 10);
|
|
28149
28330
|
const alive = processAlive(pid);
|
|
28150
28331
|
const announced = alive ? parseAnnouncement(file, pid) : { kind: "rubbish" };
|
|
@@ -28187,7 +28368,25 @@ function tooNew(unspoken) {
|
|
|
28187
28368
|
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";
|
|
28188
28369
|
return `A game is running, in a protocol this server does not speak ${named}. This server speaks ${RUNTIME_PROTOCOL}, so ${half}.`;
|
|
28189
28370
|
}
|
|
28190
|
-
function chooseRuntime(endpoints, projectPath, unspoken = []) {
|
|
28371
|
+
function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
|
|
28372
|
+
if (pid !== undefined) {
|
|
28373
|
+
const named = endpoints.find((endpoint) => endpoint.pid === pid);
|
|
28374
|
+
if (named !== undefined) {
|
|
28375
|
+
if (projectPath !== undefined && resolve4(named.project.path) !== resolve4(projectPath)) {
|
|
28376
|
+
return {
|
|
28377
|
+
problem: `The game with pid ${pid} is running ${named.project.path}, not ${resolve4(projectPath)}. Running: ${endpoints.map(describe2).join("; ")}.`
|
|
28378
|
+
};
|
|
28379
|
+
}
|
|
28380
|
+
return { endpoint: named };
|
|
28381
|
+
}
|
|
28382
|
+
const tooNewToo = unspoken.find((game) => game.pid === pid);
|
|
28383
|
+
if (tooNewToo !== undefined) {
|
|
28384
|
+
return { problem: tooNew([tooNewToo]) };
|
|
28385
|
+
}
|
|
28386
|
+
return {
|
|
28387
|
+
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.`
|
|
28388
|
+
};
|
|
28389
|
+
}
|
|
28191
28390
|
if (endpoints.length === 0) {
|
|
28192
28391
|
if (unspoken.length > 0) {
|
|
28193
28392
|
return { problem: tooNew(unspoken) };
|
|
@@ -28218,14 +28417,14 @@ function chooseRuntime(endpoints, projectPath, unspoken = []) {
|
|
|
28218
28417
|
return { endpoint: matching[0] };
|
|
28219
28418
|
}
|
|
28220
28419
|
return {
|
|
28221
|
-
problem: `Several games are running from ${wanted}: ${matching.map(describe2).join("; ")}.
|
|
28420
|
+
problem: `Several games are running from ${wanted}: ${matching.map(describe2).join("; ")}. Pass pid to choose one.`
|
|
28222
28421
|
};
|
|
28223
28422
|
}
|
|
28224
28423
|
if (endpoints.length === 1 && endpoints[0]) {
|
|
28225
28424
|
return { endpoint: endpoints[0] };
|
|
28226
28425
|
}
|
|
28227
28426
|
return {
|
|
28228
|
-
problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass projectPath to choose one.`
|
|
28427
|
+
problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass projectPath to choose one, or pid when they are from one project.`
|
|
28229
28428
|
};
|
|
28230
28429
|
}
|
|
28231
28430
|
var nextRequestId = 1;
|
|
@@ -28319,33 +28518,33 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
|
|
|
28319
28518
|
|
|
28320
28519
|
// src/run-record.ts
|
|
28321
28520
|
function runsDirectory() {
|
|
28322
|
-
return
|
|
28521
|
+
return join11(runtimeDirectory(), "runs");
|
|
28323
28522
|
}
|
|
28324
28523
|
function recordPath() {
|
|
28325
|
-
return
|
|
28524
|
+
return join11(runsDirectory(), "run.json");
|
|
28326
28525
|
}
|
|
28327
28526
|
function recordPaths() {
|
|
28328
|
-
return runtimeDirectories().map((directory) =>
|
|
28527
|
+
return runtimeDirectories().map((directory) => join11(directory, "runs", "run.json"));
|
|
28329
28528
|
}
|
|
28330
28529
|
function openTranscript(startedAt) {
|
|
28331
|
-
|
|
28332
|
-
const path =
|
|
28530
|
+
mkdirSync5(runsDirectory(), { recursive: true });
|
|
28531
|
+
const path = join11(runsDirectory(), `run-${startedAt}.log`);
|
|
28333
28532
|
return { path, fd: openSync(path, "a") };
|
|
28334
28533
|
}
|
|
28335
28534
|
function writeRunRecord(record) {
|
|
28336
|
-
|
|
28337
|
-
|
|
28535
|
+
mkdirSync5(runsDirectory(), { recursive: true });
|
|
28536
|
+
writeFileSync6(recordPath(), JSON.stringify(record, null, 2), "utf8");
|
|
28338
28537
|
}
|
|
28339
28538
|
function editorNotePath() {
|
|
28340
|
-
return
|
|
28539
|
+
return join11(runsDirectory(), "editor-run.json");
|
|
28341
28540
|
}
|
|
28342
28541
|
function writeEditorRunNote(note) {
|
|
28343
|
-
|
|
28344
|
-
|
|
28542
|
+
mkdirSync5(runsDirectory(), { recursive: true });
|
|
28543
|
+
writeFileSync6(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
|
|
28345
28544
|
}
|
|
28346
28545
|
function readEditorRunNote() {
|
|
28347
28546
|
for (const directory of runtimeDirectories()) {
|
|
28348
|
-
const note = editorNoteAt(
|
|
28547
|
+
const note = editorNoteAt(join11(directory, "runs", "editor-run.json"));
|
|
28349
28548
|
if (note !== null) {
|
|
28350
28549
|
return note;
|
|
28351
28550
|
}
|
|
@@ -28355,7 +28554,7 @@ function readEditorRunNote() {
|
|
|
28355
28554
|
function editorNoteAt(path) {
|
|
28356
28555
|
let parsed;
|
|
28357
28556
|
try {
|
|
28358
|
-
parsed = JSON.parse(
|
|
28557
|
+
parsed = JSON.parse(readFileSync11(path, "utf8"));
|
|
28359
28558
|
} catch {
|
|
28360
28559
|
return null;
|
|
28361
28560
|
}
|
|
@@ -28378,7 +28577,7 @@ function recordRunEnded(pid, exitCode) {
|
|
|
28378
28577
|
return;
|
|
28379
28578
|
}
|
|
28380
28579
|
try {
|
|
28381
|
-
|
|
28580
|
+
writeFileSync6(path, JSON.stringify({ ...record, exitCode }, null, 2), "utf8");
|
|
28382
28581
|
} catch {}
|
|
28383
28582
|
}
|
|
28384
28583
|
function readRunRecord() {
|
|
@@ -28393,7 +28592,7 @@ function readRunRecord() {
|
|
|
28393
28592
|
function recordAt(path) {
|
|
28394
28593
|
let parsed;
|
|
28395
28594
|
try {
|
|
28396
|
-
parsed = JSON.parse(
|
|
28595
|
+
parsed = JSON.parse(readFileSync11(path, "utf8"));
|
|
28397
28596
|
} catch {
|
|
28398
28597
|
return null;
|
|
28399
28598
|
}
|
|
@@ -28438,7 +28637,7 @@ function runningAs(pid) {
|
|
|
28438
28637
|
const began = startedAt(pid, null);
|
|
28439
28638
|
const when = began === null ? {} : { startedAt: began };
|
|
28440
28639
|
if (process.platform === "linux") {
|
|
28441
|
-
const raw =
|
|
28640
|
+
const raw = readFileSync11(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
28442
28641
|
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
28443
28642
|
}
|
|
28444
28643
|
const args = execFileSync("ps", ["-p", String(pid), "-o", "args="], {
|
|
@@ -28560,7 +28759,7 @@ function clearRunRecord(ours = () => true) {
|
|
|
28560
28759
|
continue;
|
|
28561
28760
|
}
|
|
28562
28761
|
try {
|
|
28563
|
-
|
|
28762
|
+
rmSync5(path);
|
|
28564
28763
|
} catch {}
|
|
28565
28764
|
}
|
|
28566
28765
|
}
|
|
@@ -28584,19 +28783,19 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
28584
28783
|
if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
|
|
28585
28784
|
continue;
|
|
28586
28785
|
}
|
|
28587
|
-
const path =
|
|
28786
|
+
const path = join11(directory, entry);
|
|
28588
28787
|
if (path === keep || ageMs(path, now) < keepMs) {
|
|
28589
28788
|
continue;
|
|
28590
28789
|
}
|
|
28591
28790
|
try {
|
|
28592
|
-
|
|
28791
|
+
rmSync5(path);
|
|
28593
28792
|
} catch {}
|
|
28594
28793
|
}
|
|
28595
28794
|
}
|
|
28596
28795
|
|
|
28597
28796
|
// src/setup.ts
|
|
28598
|
-
import { cpSync, existsSync as existsSync8, mkdirSync as
|
|
28599
|
-
import { dirname as
|
|
28797
|
+
import { cpSync, existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync12, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
|
|
28798
|
+
import { dirname as dirname5, join as join12 } from "node:path";
|
|
28600
28799
|
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
28601
28800
|
var RUNTIME_AUTOLOAD = {
|
|
28602
28801
|
name: "GdharnessRuntime",
|
|
@@ -28604,11 +28803,11 @@ var RUNTIME_AUTOLOAD = {
|
|
|
28604
28803
|
};
|
|
28605
28804
|
var VERSION_MARKER = ".gdharness-version";
|
|
28606
28805
|
function installedAddonVersion(projectPath) {
|
|
28607
|
-
const marker =
|
|
28806
|
+
const marker = join12(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
28608
28807
|
if (!existsSync8(marker)) {
|
|
28609
28808
|
return null;
|
|
28610
28809
|
}
|
|
28611
|
-
const said =
|
|
28810
|
+
const said = readFileSync12(marker, "utf8").trim();
|
|
28612
28811
|
return said === "" ? null : said;
|
|
28613
28812
|
}
|
|
28614
28813
|
|
|
@@ -28636,6 +28835,10 @@ var RUNNING_PROJECT_PATH = {
|
|
|
28636
28835
|
type: "string",
|
|
28637
28836
|
description: "Which game, when more than one is running: the project directory it was started from. Not needed with one game."
|
|
28638
28837
|
};
|
|
28838
|
+
var RUNNING_PID = {
|
|
28839
|
+
type: "number",
|
|
28840
|
+
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."
|
|
28841
|
+
};
|
|
28639
28842
|
var SCENE_PATH = {
|
|
28640
28843
|
type: "string",
|
|
28641
28844
|
description: 'Scene file inside the project, such as "scenes/main.tscn" or "res://scenes/main.tscn".'
|
|
@@ -29274,7 +29477,7 @@ var TOOL_SPECS = [
|
|
|
29274
29477
|
},
|
|
29275
29478
|
{
|
|
29276
29479
|
name: "editor_output",
|
|
29277
|
-
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.",
|
|
29480
|
+
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.",
|
|
29278
29481
|
parameters: {
|
|
29279
29482
|
severity: {
|
|
29280
29483
|
type: "string",
|
|
@@ -29302,7 +29505,7 @@ var TOOL_SPECS = [
|
|
|
29302
29505
|
},
|
|
29303
29506
|
{
|
|
29304
29507
|
name: "editor_status",
|
|
29305
|
-
description:
|
|
29508
|
+
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.",
|
|
29306
29509
|
parameters: {},
|
|
29307
29510
|
requires: []
|
|
29308
29511
|
},
|
|
@@ -29359,6 +29562,7 @@ var TOOL_SPECS = [
|
|
|
29359
29562
|
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.",
|
|
29360
29563
|
parameters: {
|
|
29361
29564
|
projectPath: RUNNING_PROJECT_PATH,
|
|
29565
|
+
pid: RUNNING_PID,
|
|
29362
29566
|
nodePath: {
|
|
29363
29567
|
type: "string",
|
|
29364
29568
|
ops: ["tree", "text", "find", "rect", "property"],
|
|
@@ -29437,6 +29641,7 @@ var TOOL_SPECS = [
|
|
|
29437
29641
|
description: "Sets a property or calls a method on a node in the running game. Needs the game running with the runtime addon.",
|
|
29438
29642
|
parameters: {
|
|
29439
29643
|
projectPath: RUNNING_PROJECT_PATH,
|
|
29644
|
+
pid: RUNNING_PID,
|
|
29440
29645
|
nodePath: { type: "string", description: 'Absolute node path, such as "/root/Main/Player".' },
|
|
29441
29646
|
property: {
|
|
29442
29647
|
type: "string",
|
|
@@ -29476,6 +29681,7 @@ var TOOL_SPECS = [
|
|
|
29476
29681
|
description: "A picture of the running game: the whole screen or one viewport, as an image. Needs the game running with a window.",
|
|
29477
29682
|
parameters: {
|
|
29478
29683
|
projectPath: RUNNING_PROJECT_PATH,
|
|
29684
|
+
pid: RUNNING_PID,
|
|
29479
29685
|
viewportPath: {
|
|
29480
29686
|
type: "string",
|
|
29481
29687
|
ops: ["viewport"],
|
|
@@ -29496,6 +29702,7 @@ var TOOL_SPECS = [
|
|
|
29496
29702
|
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.",
|
|
29497
29703
|
parameters: {
|
|
29498
29704
|
projectPath: RUNNING_PROJECT_PATH,
|
|
29705
|
+
pid: RUNNING_PID,
|
|
29499
29706
|
nodePath: {
|
|
29500
29707
|
type: "string",
|
|
29501
29708
|
ops: ["click", "choose"],
|
|
@@ -29592,6 +29799,7 @@ var TOOL_SPECS = [
|
|
|
29592
29799
|
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.",
|
|
29593
29800
|
parameters: {
|
|
29594
29801
|
projectPath: RUNNING_PROJECT_PATH,
|
|
29802
|
+
pid: RUNNING_PID,
|
|
29595
29803
|
frames: {
|
|
29596
29804
|
type: "number",
|
|
29597
29805
|
ops: ["frames"],
|
|
@@ -29627,7 +29835,7 @@ var TOOL_SPECS = [
|
|
|
29627
29835
|
},
|
|
29628
29836
|
{
|
|
29629
29837
|
name: "debug_breakpoint",
|
|
29630
|
-
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.",
|
|
29838
|
+
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.",
|
|
29631
29839
|
parameters: {
|
|
29632
29840
|
projectPath: PROJECT_PATH,
|
|
29633
29841
|
scriptPath: SCRIPT_PATH,
|
|
@@ -29652,7 +29860,7 @@ var TOOL_SPECS = [
|
|
|
29652
29860
|
},
|
|
29653
29861
|
{
|
|
29654
29862
|
name: "debug_state",
|
|
29655
|
-
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.",
|
|
29863
|
+
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.",
|
|
29656
29864
|
parameters: {
|
|
29657
29865
|
frameId: {
|
|
29658
29866
|
type: "number",
|
|
@@ -29719,12 +29927,33 @@ function buildToolDefinitions() {
|
|
|
29719
29927
|
var UPDATE_NOTICE_EVERY = 500;
|
|
29720
29928
|
var FEEDBACK_NOTICE_EVERY = 250;
|
|
29721
29929
|
var run4 = promisify4(execFile4);
|
|
29722
|
-
var __dirname2 =
|
|
29930
|
+
var __dirname2 = dirname6(fileURLToPath2(import.meta.url));
|
|
29723
29931
|
var EDITOR_RESTART_TIMEOUT_MS = 90000;
|
|
29932
|
+
var HOLD_PING_MS = 2000;
|
|
29724
29933
|
var SLOWEST_FRAME_RATE = 20;
|
|
29725
29934
|
function patienceForFrames(frames, atLeast) {
|
|
29726
29935
|
return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
|
|
29727
29936
|
}
|
|
29937
|
+
function projectSpelling(projectPath, absolutePath) {
|
|
29938
|
+
return `res://${relative3(realPathOr(projectPath), realPathOr(absolutePath)).replaceAll("\\", "/")}`;
|
|
29939
|
+
}
|
|
29940
|
+
function whichGame(args) {
|
|
29941
|
+
return { projectPath: args["projectPath"], pid: args["pid"] };
|
|
29942
|
+
}
|
|
29943
|
+
function describeHalt(halt) {
|
|
29944
|
+
switch (halt.reason) {
|
|
29945
|
+
case "breakpoint":
|
|
29946
|
+
return "at a breakpoint";
|
|
29947
|
+
case "step":
|
|
29948
|
+
return "after a step";
|
|
29949
|
+
case "exception":
|
|
29950
|
+
return halt.text === "" ? "on an error" : `on an error: ${halt.text}`;
|
|
29951
|
+
case "attached":
|
|
29952
|
+
return "held when this session attached";
|
|
29953
|
+
default:
|
|
29954
|
+
return `stopped with reason "${halt.reason}"`;
|
|
29955
|
+
}
|
|
29956
|
+
}
|
|
29728
29957
|
function stillRunning(run) {
|
|
29729
29958
|
if (run?.exitCode !== null || run.endedUnwatched === true) {
|
|
29730
29959
|
return false;
|
|
@@ -29746,6 +29975,19 @@ function runIsUp(run, editorSays) {
|
|
|
29746
29975
|
}
|
|
29747
29976
|
return editorSays ?? stillRunning(run);
|
|
29748
29977
|
}
|
|
29978
|
+
function endedPreviousRun(ended) {
|
|
29979
|
+
if (ended === null) {
|
|
29980
|
+
return;
|
|
29981
|
+
}
|
|
29982
|
+
return ended.pid ?? true;
|
|
29983
|
+
}
|
|
29984
|
+
function endedToStartThis(ended) {
|
|
29985
|
+
if (ended === null) {
|
|
29986
|
+
return "";
|
|
29987
|
+
}
|
|
29988
|
+
const which = ended.pid === null ? "The game the editor was playing" : `The run that was going, pid ${ended.pid},`;
|
|
29989
|
+
return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.`;
|
|
29990
|
+
}
|
|
29749
29991
|
function runtimeVerdict(endpoint, after) {
|
|
29750
29992
|
if (endpoint !== null) {
|
|
29751
29993
|
return { listening: true, pid: endpoint.pid, port: endpoint.port };
|
|
@@ -29915,7 +30157,7 @@ function aboveTheRunner(entry) {
|
|
|
29915
30157
|
}
|
|
29916
30158
|
function elsewhereIn(projectPath, asked) {
|
|
29917
30159
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
29918
|
-
if (wanted === "" || existsSync9(
|
|
30160
|
+
if (wanted === "" || existsSync9(join13(projectPath, wanted))) {
|
|
29919
30161
|
return "";
|
|
29920
30162
|
}
|
|
29921
30163
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
@@ -29929,11 +30171,11 @@ function elsewhereIn(projectPath, asked) {
|
|
|
29929
30171
|
return near.length === 0 ? "" : ` The project does have ${there}.`;
|
|
29930
30172
|
}
|
|
29931
30173
|
function hasMainScene(projectFile) {
|
|
29932
|
-
const scene = parseProjectGodot(
|
|
30174
|
+
const scene = parseProjectGodot(readFileSync13(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
29933
30175
|
return typeof scene === "string" && scene !== "";
|
|
29934
30176
|
}
|
|
29935
30177
|
function editorPlaysHeadless(projectFile) {
|
|
29936
|
-
const runArgs = parseProjectGodot(
|
|
30178
|
+
const runArgs = parseProjectGodot(readFileSync13(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
29937
30179
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
29938
30180
|
}
|
|
29939
30181
|
function camelCased(params) {
|
|
@@ -29949,7 +30191,7 @@ function camelCased(params) {
|
|
|
29949
30191
|
class GodotServer {
|
|
29950
30192
|
mcp;
|
|
29951
30193
|
locator = new GodotLocator;
|
|
29952
|
-
operationsScript =
|
|
30194
|
+
operationsScript = join13(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
29953
30195
|
godotBridge;
|
|
29954
30196
|
tools = buildToolDefinitions();
|
|
29955
30197
|
activeProcess = null;
|
|
@@ -29961,6 +30203,7 @@ class GodotServer {
|
|
|
29961
30203
|
lastGodotVersion = null;
|
|
29962
30204
|
lspClient = null;
|
|
29963
30205
|
dapClient = null;
|
|
30206
|
+
breakpointsSeededOn = null;
|
|
29964
30207
|
bridgeStartupError = null;
|
|
29965
30208
|
bridgeRetry = null;
|
|
29966
30209
|
successorWatch = null;
|
|
@@ -30485,7 +30728,7 @@ class GodotServer {
|
|
|
30485
30728
|
switch (op) {
|
|
30486
30729
|
case "tree":
|
|
30487
30730
|
return await this.handleRuntimeCommand("get_tree", {
|
|
30488
|
-
|
|
30731
|
+
...whichGame(args),
|
|
30489
30732
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
30490
30733
|
depth: readPositiveNumber(args, "depth") ?? 3,
|
|
30491
30734
|
include_properties: readBoolean(args, "includeProperties") ?? false
|
|
@@ -30494,36 +30737,36 @@ class GodotServer {
|
|
|
30494
30737
|
return await this.handleFindRuntimeNodes(args);
|
|
30495
30738
|
case "text":
|
|
30496
30739
|
return await this.handleRuntimeCommand("read_text", {
|
|
30497
|
-
|
|
30740
|
+
...whichGame(args),
|
|
30498
30741
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
30499
30742
|
include_hidden: readBoolean(args, "includeHidden") ?? false,
|
|
30500
30743
|
limit: readPositiveNumber(args, "limit") ?? 500
|
|
30501
30744
|
});
|
|
30502
30745
|
case "rect":
|
|
30503
30746
|
return await this.handleRuntimeCommand("get_rect", {
|
|
30504
|
-
|
|
30747
|
+
...whichGame(args),
|
|
30505
30748
|
path: readNonEmptyString(args, "nodePath") ?? ""
|
|
30506
30749
|
});
|
|
30507
30750
|
case "property":
|
|
30508
30751
|
return await this.handleRuntimeCommand("get_property", {
|
|
30509
|
-
|
|
30752
|
+
...whichGame(args),
|
|
30510
30753
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
30511
30754
|
property: readNonEmptyString(args, "property") ?? ""
|
|
30512
30755
|
});
|
|
30513
30756
|
default:
|
|
30514
30757
|
return await this.handleRuntimeCommand("get_metrics", {
|
|
30515
|
-
|
|
30758
|
+
...whichGame(args),
|
|
30516
30759
|
metrics: readArray(args, "metrics") ?? []
|
|
30517
30760
|
});
|
|
30518
30761
|
}
|
|
30519
30762
|
case "runtime_invoke":
|
|
30520
30763
|
return op === "set" ? await this.handleRuntimeCommand("set_property", {
|
|
30521
|
-
|
|
30764
|
+
...whichGame(args),
|
|
30522
30765
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
30523
30766
|
property: readString(args, "property") ?? "",
|
|
30524
30767
|
value: args["value"]
|
|
30525
30768
|
}) : await this.handleRuntimeCommand("call_method", {
|
|
30526
|
-
|
|
30769
|
+
...whichGame(args),
|
|
30527
30770
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
30528
30771
|
method: readString(args, "method") ?? "",
|
|
30529
30772
|
args: readArray(args, "args") ?? []
|
|
@@ -30533,7 +30776,7 @@ class GodotServer {
|
|
|
30533
30776
|
case "runtime_input":
|
|
30534
30777
|
if (op === "click") {
|
|
30535
30778
|
return await this.handleRuntimeCommand("click", {
|
|
30536
|
-
|
|
30779
|
+
...whichGame(args),
|
|
30537
30780
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
30538
30781
|
button: readString(args, "button") ?? "left",
|
|
30539
30782
|
double: readBoolean(args, "doubleClick") ?? false
|
|
@@ -30541,7 +30784,7 @@ class GodotServer {
|
|
|
30541
30784
|
}
|
|
30542
30785
|
if (op === "choose") {
|
|
30543
30786
|
return await this.handleRuntimeCommand("choose", {
|
|
30544
|
-
|
|
30787
|
+
...whichGame(args),
|
|
30545
30788
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
30546
30789
|
text: readString(args, "text") ?? "",
|
|
30547
30790
|
...args["index"] === undefined ? {} : { index: args["index"] }
|
|
@@ -30559,17 +30802,30 @@ class GodotServer {
|
|
|
30559
30802
|
if (!located.ok) {
|
|
30560
30803
|
return this.createErrorResponse(located.reason, PATH_SOLUTIONS);
|
|
30561
30804
|
}
|
|
30562
|
-
|
|
30805
|
+
const session = this.dapHoldingBreakpointsOf(project.value.path);
|
|
30806
|
+
const answered = await this.handleDAP(op === "set" ? "dap_set_breakpoint" : "dap_remove_breakpoint", {
|
|
30563
30807
|
...args,
|
|
30564
30808
|
scriptPath: realPathOr(located.absolutePath)
|
|
30565
30809
|
});
|
|
30810
|
+
if (answered.isError === true) {
|
|
30811
|
+
return answered;
|
|
30812
|
+
}
|
|
30813
|
+
const held = session.breakpointsHeld();
|
|
30814
|
+
writeBreakpointNote(project.value.path, held);
|
|
30815
|
+
return this.jsonTextResponse({
|
|
30816
|
+
...asParams(JSON.parse(answered.content[0]?.text ?? "{}")),
|
|
30817
|
+
held: this.breakpointsAsProjectSpells(project.value.path, held),
|
|
30818
|
+
setInEditor: this.breakpointsAsProjectSpells(project.value.path, session.breakpointsInEditor()),
|
|
30819
|
+
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.",
|
|
30820
|
+
...this.breakpointsAtRisk()
|
|
30821
|
+
});
|
|
30566
30822
|
}
|
|
30567
30823
|
case "debug_control":
|
|
30568
30824
|
case "debug_state": {
|
|
30569
30825
|
if (tool === "debug_state" && op === "output") {
|
|
30570
30826
|
return await this.handleDAP("dap_get_output", args);
|
|
30571
30827
|
}
|
|
30572
|
-
const held = await this.debuggedGame();
|
|
30828
|
+
const held = await this.debuggedGame(tool === "debug_control" ? "move" : "read");
|
|
30573
30829
|
if (!held.ok) {
|
|
30574
30830
|
return held.response;
|
|
30575
30831
|
}
|
|
@@ -30592,7 +30848,7 @@ class GodotServer {
|
|
|
30592
30848
|
if (path === undefined) {
|
|
30593
30849
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
30594
30850
|
}
|
|
30595
|
-
const file =
|
|
30851
|
+
const file = join13(path, "project.godot");
|
|
30596
30852
|
if (!existsSync9(file)) {
|
|
30597
30853
|
return {
|
|
30598
30854
|
ok: false,
|
|
@@ -30618,7 +30874,7 @@ class GodotServer {
|
|
|
30618
30874
|
}
|
|
30619
30875
|
return { ok: true, value: given };
|
|
30620
30876
|
}
|
|
30621
|
-
async debuggedGame() {
|
|
30877
|
+
async debuggedGame(intent) {
|
|
30622
30878
|
const game = this.activeProcess;
|
|
30623
30879
|
if (game !== null && game.exitCode !== null) {
|
|
30624
30880
|
return {
|
|
@@ -30635,6 +30891,16 @@ class GodotServer {
|
|
|
30635
30891
|
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"])
|
|
30636
30892
|
};
|
|
30637
30893
|
}
|
|
30894
|
+
const going = await this.runStillGoing(game);
|
|
30895
|
+
if (!going) {
|
|
30896
|
+
return {
|
|
30897
|
+
ok: false,
|
|
30898
|
+
response: this.createErrorResponse("The last run has already ended, so there is no debug session to answer for.", [
|
|
30899
|
+
"editor_output still reads what it printed, until the next run starts",
|
|
30900
|
+
"editor_run start plays another, which the debugger can hold"
|
|
30901
|
+
])
|
|
30902
|
+
};
|
|
30903
|
+
}
|
|
30638
30904
|
if (!game.throughEditor) {
|
|
30639
30905
|
return {
|
|
30640
30906
|
ok: false,
|
|
@@ -30656,6 +30922,19 @@ class GodotServer {
|
|
|
30656
30922
|
};
|
|
30657
30923
|
}
|
|
30658
30924
|
if (!this.dap().isStopped()) {
|
|
30925
|
+
const hold = await this.holdOf(game, going);
|
|
30926
|
+
if (hold.heldAt !== null) {
|
|
30927
|
+
if (intent === "move") {
|
|
30928
|
+
return { ok: true, value: game };
|
|
30929
|
+
}
|
|
30930
|
+
return {
|
|
30931
|
+
ok: false,
|
|
30932
|
+
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}.`, [
|
|
30933
|
+
"debug_control continue lets a held game go, and the next stop is one this session is told about",
|
|
30934
|
+
"editor_output says what it printed on the way to where it is"
|
|
30935
|
+
])
|
|
30936
|
+
};
|
|
30937
|
+
}
|
|
30659
30938
|
return {
|
|
30660
30939
|
ok: false,
|
|
30661
30940
|
response: this.createErrorResponse("The game is running, not stopped, so it has no stack and no scope to read.", [
|
|
@@ -30793,7 +31072,7 @@ class GodotServer {
|
|
|
30793
31072
|
if (!engine.ok) {
|
|
30794
31073
|
return engine.response;
|
|
30795
31074
|
}
|
|
30796
|
-
const application = parseProjectGodot(
|
|
31075
|
+
const application = parseProjectGodot(readFileSync13(project.value.file, "utf8"))["application"] ?? {};
|
|
30797
31076
|
const name = application["config/name"];
|
|
30798
31077
|
const mainScene = application["run/main_scene"];
|
|
30799
31078
|
const info = {
|
|
@@ -30854,18 +31133,18 @@ class GodotServer {
|
|
|
30854
31133
|
if (!engine.ok) {
|
|
30855
31134
|
return engine.response;
|
|
30856
31135
|
}
|
|
30857
|
-
const folder =
|
|
31136
|
+
const folder = dirname6(output.absolutePath);
|
|
30858
31137
|
try {
|
|
30859
|
-
|
|
31138
|
+
mkdirSync7(folder, { recursive: true });
|
|
30860
31139
|
} catch (error) {
|
|
30861
|
-
return this.createErrorResponse(`Cannot create ${
|
|
31140
|
+
return this.createErrorResponse(`Cannot create ${dirname6(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
30862
31141
|
}
|
|
30863
31142
|
const debug = readBoolean(args, "debug") ?? false;
|
|
30864
|
-
const exportLogs = mkdtempSync2(
|
|
31143
|
+
const exportLogs = mkdtempSync2(join13(tmpdir4(), "gdharness-export-"));
|
|
30865
31144
|
const exportArgs = [
|
|
30866
31145
|
"--headless",
|
|
30867
31146
|
"--log-file",
|
|
30868
|
-
|
|
31147
|
+
join13(exportLogs, "engine.log"),
|
|
30869
31148
|
"--path",
|
|
30870
31149
|
project.value.path,
|
|
30871
31150
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -30923,7 +31202,7 @@ class GodotServer {
|
|
|
30923
31202
|
return contained.response;
|
|
30924
31203
|
}
|
|
30925
31204
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
30926
|
-
if (!existsSync9(
|
|
31205
|
+
if (!existsSync9(join13(project.value.path, runner))) {
|
|
30927
31206
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
30928
31207
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
30929
31208
|
]);
|
|
@@ -30958,7 +31237,7 @@ class GodotServer {
|
|
|
30958
31237
|
];
|
|
30959
31238
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
30960
31239
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
30961
|
-
const userData = mkdtempSync2(
|
|
31240
|
+
const userData = mkdtempSync2(join13(tmpdir4(), "gdharness-tests-"));
|
|
30962
31241
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
30963
31242
|
const hung = await new Promise((resolve) => {
|
|
30964
31243
|
const timer = setTimeout(() => {
|
|
@@ -30974,14 +31253,14 @@ class GodotServer {
|
|
|
30974
31253
|
resolve(false);
|
|
30975
31254
|
});
|
|
30976
31255
|
});
|
|
30977
|
-
const reportsDir =
|
|
31256
|
+
const reportsDir = join13(project.value.path, ".godot", ...ours.split("/"));
|
|
30978
31257
|
let report = null;
|
|
30979
31258
|
let reportProblem = null;
|
|
30980
31259
|
try {
|
|
30981
31260
|
const written = existsSync9(reportsDir) ? readdirSync6(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
30982
31261
|
const newest = written.at(-1);
|
|
30983
31262
|
if (newest !== undefined) {
|
|
30984
|
-
report = parseJUnit(
|
|
31263
|
+
report = parseJUnit(readFileSync13(join13(reportsDir, newest, "results.xml"), "utf8"));
|
|
30985
31264
|
}
|
|
30986
31265
|
} catch (error) {
|
|
30987
31266
|
reportProblem = errorMessage(error);
|
|
@@ -31121,7 +31400,7 @@ class GodotServer {
|
|
|
31121
31400
|
return null;
|
|
31122
31401
|
}
|
|
31123
31402
|
try {
|
|
31124
|
-
return
|
|
31403
|
+
return readFileSync13(contained.absolutePath, "utf8");
|
|
31125
31404
|
} catch {
|
|
31126
31405
|
return null;
|
|
31127
31406
|
}
|
|
@@ -31163,7 +31442,7 @@ class GodotServer {
|
|
|
31163
31442
|
return null;
|
|
31164
31443
|
}
|
|
31165
31444
|
try {
|
|
31166
|
-
return
|
|
31445
|
+
return readFileSync13(contained.absolutePath, "utf8");
|
|
31167
31446
|
} catch {
|
|
31168
31447
|
return null;
|
|
31169
31448
|
}
|
|
@@ -31209,9 +31488,38 @@ class GodotServer {
|
|
|
31209
31488
|
this.dapClient.setOutputSink((line) => {
|
|
31210
31489
|
this.writeToTranscript(line);
|
|
31211
31490
|
});
|
|
31491
|
+
this.dapClient.setBreakpointsSink((held) => {
|
|
31492
|
+
const project = this.godotBridge.getStatus().projectPath ?? this.ownProject;
|
|
31493
|
+
if (project !== null) {
|
|
31494
|
+
writeBreakpointNote(project, held);
|
|
31495
|
+
}
|
|
31496
|
+
});
|
|
31212
31497
|
}
|
|
31213
31498
|
return this.dapClient;
|
|
31214
31499
|
}
|
|
31500
|
+
dapHoldingBreakpointsOf(projectPath) {
|
|
31501
|
+
const client = this.dap();
|
|
31502
|
+
if (this.breakpointsSeededOn !== client) {
|
|
31503
|
+
client.holdBreakpoints(readBreakpointNote(projectPath));
|
|
31504
|
+
this.breakpointsSeededOn = client;
|
|
31505
|
+
}
|
|
31506
|
+
return client;
|
|
31507
|
+
}
|
|
31508
|
+
breakpointsAtRisk() {
|
|
31509
|
+
const status = this.godotBridge.getStatus();
|
|
31510
|
+
if (!status.connected || status.syncsBreakpoints === true) {
|
|
31511
|
+
return {};
|
|
31512
|
+
}
|
|
31513
|
+
return {
|
|
31514
|
+
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."
|
|
31515
|
+
};
|
|
31516
|
+
}
|
|
31517
|
+
breakpointsAsProjectSpells(projectPath, held) {
|
|
31518
|
+
return held.map((one) => ({
|
|
31519
|
+
scriptPath: projectSpelling(projectPath, one.scriptPath),
|
|
31520
|
+
lines: one.lines
|
|
31521
|
+
}));
|
|
31522
|
+
}
|
|
31215
31523
|
writeToTranscript(line) {
|
|
31216
31524
|
const run = this.activeProcess;
|
|
31217
31525
|
if (run?.throughEditor !== true || run.transcript === null) {
|
|
@@ -31268,6 +31576,7 @@ class GodotServer {
|
|
|
31268
31576
|
...status.connected ? this.whatTheLaunchedEditorDropped() : {},
|
|
31269
31577
|
startupError: this.bridgeStartupError,
|
|
31270
31578
|
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
|
|
31579
|
+
...this.breakpointsAtRisk(),
|
|
31271
31580
|
retryingBridge: this.bridgeRetry === null ? undefined : true,
|
|
31272
31581
|
announcedAt: this.announcedAt ?? undefined,
|
|
31273
31582
|
projectIs: this.projectHasMovedOn() ?? undefined,
|
|
@@ -31301,8 +31610,12 @@ class GodotServer {
|
|
|
31301
31610
|
async handleEditorStatus() {
|
|
31302
31611
|
const located = await this.locator.find();
|
|
31303
31612
|
const announced = runtimesAnnounced();
|
|
31304
|
-
const
|
|
31305
|
-
const
|
|
31613
|
+
const pinged = Promise.all(announced.running.map(async (endpoint) => {
|
|
31614
|
+
const held = this.knownHoldOn(endpoint);
|
|
31615
|
+
const reply = held === null ? await runtimeRequest(endpoint, "ping", {}, HOLD_PING_MS) : {
|
|
31616
|
+
ok: false,
|
|
31617
|
+
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.`
|
|
31618
|
+
};
|
|
31306
31619
|
return {
|
|
31307
31620
|
pid: endpoint.pid,
|
|
31308
31621
|
port: endpoint.port,
|
|
@@ -31311,8 +31624,18 @@ class GodotServer {
|
|
|
31311
31624
|
problem: reply.ok ? null : reply.message
|
|
31312
31625
|
};
|
|
31313
31626
|
}));
|
|
31314
|
-
const
|
|
31315
|
-
|
|
31627
|
+
const asked = (async () => {
|
|
31628
|
+
const playing = await this.editorPlayingState();
|
|
31629
|
+
await this.pickUpWhatTheEditorIsPlaying();
|
|
31630
|
+
const current = this.currentRun();
|
|
31631
|
+
const active = current === null ? false : await this.runStillGoing(current, playing?.playing ?? null);
|
|
31632
|
+
return {
|
|
31633
|
+
playing,
|
|
31634
|
+
active,
|
|
31635
|
+
hold: current === null ? {} : await this.holdOf(current, active)
|
|
31636
|
+
};
|
|
31637
|
+
})();
|
|
31638
|
+
const [games, { playing, active, hold }] = await Promise.all([pinged, asked]);
|
|
31316
31639
|
return this.jsonTextResponse({
|
|
31317
31640
|
editor: {
|
|
31318
31641
|
...this.getEditorStatusPayload(),
|
|
@@ -31324,8 +31647,9 @@ class GodotServer {
|
|
|
31324
31647
|
problem: located.ok ? undefined : GodotLocator.refusal(located).message
|
|
31325
31648
|
},
|
|
31326
31649
|
game: {
|
|
31327
|
-
processActive:
|
|
31650
|
+
processActive: active,
|
|
31328
31651
|
playingInEditor: playing,
|
|
31652
|
+
...hold,
|
|
31329
31653
|
runtimeConnected: games.some((game) => game.reachable),
|
|
31330
31654
|
runtimes: games,
|
|
31331
31655
|
unreadable: announced.unspoken.map((game) => ({
|
|
@@ -31343,7 +31667,7 @@ class GodotServer {
|
|
|
31343
31667
|
return null;
|
|
31344
31668
|
}
|
|
31345
31669
|
try {
|
|
31346
|
-
return settingKeys(
|
|
31670
|
+
return settingKeys(readFileSync13(join13(projectPath, "project.godot"), "utf8"));
|
|
31347
31671
|
} catch {
|
|
31348
31672
|
return null;
|
|
31349
31673
|
}
|
|
@@ -31536,10 +31860,12 @@ class GodotServer {
|
|
|
31536
31860
|
if (op === "check") {
|
|
31537
31861
|
return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value);
|
|
31538
31862
|
}
|
|
31863
|
+
await this.pickUpWhatTheEditorIsPlaying();
|
|
31539
31864
|
let ended = null;
|
|
31540
|
-
|
|
31865
|
+
const before = this.currentRun();
|
|
31866
|
+
if (before !== null && await this.runStillGoing(before)) {
|
|
31541
31867
|
this.logDebug("Ending the running game before starting another");
|
|
31542
|
-
ended = this.
|
|
31868
|
+
ended = { pid: before.pid ?? this.announcedPidOf(before) ?? null };
|
|
31543
31869
|
await this.endActiveGame("editor_run start, which ends the run that was going before it starts another");
|
|
31544
31870
|
}
|
|
31545
31871
|
const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
|
|
@@ -31563,8 +31889,8 @@ class GodotServer {
|
|
|
31563
31889
|
const started = this.spawnKeptGame(engine.value, cmdArgs, project.value.path);
|
|
31564
31890
|
this.activeProcess = started;
|
|
31565
31891
|
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." : "";
|
|
31566
|
-
const endedForThis = ended
|
|
31567
|
-
const stranded = wereHereFirst.filter((one) => one.pid !== ended && alive(one.pid));
|
|
31892
|
+
const endedForThis = endedToStartThis(ended);
|
|
31893
|
+
const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
|
|
31568
31894
|
const named = stranded.map((one) => one.protocol === null ? `pid ${one.pid}` : `pid ${one.pid} on protocol ${one.protocol}`).join(", ");
|
|
31569
31895
|
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.`;
|
|
31570
31896
|
return this.jsonTextResponse({
|
|
@@ -31575,23 +31901,38 @@ class GodotServer {
|
|
|
31575
31901
|
arguments: cmdArgs,
|
|
31576
31902
|
transcript: started.transcript,
|
|
31577
31903
|
refreshedClasses: refreshed.value,
|
|
31578
|
-
endedPreviousRun: ended
|
|
31904
|
+
endedPreviousRun: endedPreviousRun(ended),
|
|
31579
31905
|
runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
|
|
31580
31906
|
message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}${alsoRunning}`
|
|
31581
31907
|
});
|
|
31582
31908
|
}
|
|
31583
|
-
async gameIsUp() {
|
|
31584
|
-
const run = this.currentRun();
|
|
31585
|
-
const editorSays = run?.throughEditor === true ? (await this.editorPlayingState())?.playing ?? null : null;
|
|
31586
|
-
return runIsUp(run, editorSays);
|
|
31587
|
-
}
|
|
31588
31909
|
async runtimeUp(projectPath, before, budgetMs, withArgs) {
|
|
31589
|
-
if (!existsSync9(
|
|
31910
|
+
if (!existsSync9(join13(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
31590
31911
|
return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
|
|
31591
31912
|
}
|
|
31913
|
+
let asking = null;
|
|
31914
|
+
let editorAskedAt = 0;
|
|
31915
|
+
let editorSaysGoing = true;
|
|
31592
31916
|
const endpoint = await announcedSince(projectPath, before, {
|
|
31593
31917
|
budgetMs,
|
|
31594
|
-
giveUp: () =>
|
|
31918
|
+
giveUp: () => {
|
|
31919
|
+
if (this.dapClient?.isStopped() === true) {
|
|
31920
|
+
return true;
|
|
31921
|
+
}
|
|
31922
|
+
const run = this.currentRun();
|
|
31923
|
+
if (run === null || !run.throughEditor) {
|
|
31924
|
+
return !stillRunning(run);
|
|
31925
|
+
}
|
|
31926
|
+
if (asking === null && Date.now() - editorAskedAt >= RUN_POLL_MS) {
|
|
31927
|
+
editorAskedAt = Date.now();
|
|
31928
|
+
asking = this.runStillGoing(run).then((going) => {
|
|
31929
|
+
editorSaysGoing = going;
|
|
31930
|
+
}).finally(() => {
|
|
31931
|
+
asking = null;
|
|
31932
|
+
});
|
|
31933
|
+
}
|
|
31934
|
+
return !editorSaysGoing;
|
|
31935
|
+
}
|
|
31595
31936
|
});
|
|
31596
31937
|
const going = this.currentRun();
|
|
31597
31938
|
if (endpoint !== null && going?.throughEditor === true && going.announcedPid === undefined) {
|
|
@@ -31601,7 +31942,7 @@ class GodotServer {
|
|
|
31601
31942
|
addon: true,
|
|
31602
31943
|
budgetMs,
|
|
31603
31944
|
heldAt: this.dapClient?.whereItStopped() ?? null,
|
|
31604
|
-
running: await this.
|
|
31945
|
+
running: endpoint !== null || going !== null && await this.runStillGoing(going),
|
|
31605
31946
|
withArgs
|
|
31606
31947
|
});
|
|
31607
31948
|
}
|
|
@@ -31624,6 +31965,7 @@ class GodotServer {
|
|
|
31624
31965
|
"Closing the other editor, or moving its debug adapter port in its editor settings, frees this one"
|
|
31625
31966
|
]);
|
|
31626
31967
|
}
|
|
31968
|
+
const breakpoints = await this.dapHoldingBreakpointsOf(projectPath).reapplyBreakpoints();
|
|
31627
31969
|
const answer = await this.handleViaBridge("play_scene", scene === null ? {} : { scenePath: `res://${scene}` });
|
|
31628
31970
|
if (answer.isError === true) {
|
|
31629
31971
|
return answer;
|
|
@@ -31641,7 +31983,8 @@ class GodotServer {
|
|
|
31641
31983
|
startedAt,
|
|
31642
31984
|
exitCode: null,
|
|
31643
31985
|
throughEditor: true,
|
|
31644
|
-
brokeOn: null
|
|
31986
|
+
brokeOn: null,
|
|
31987
|
+
announcedBefore: alreadyPlaying
|
|
31645
31988
|
};
|
|
31646
31989
|
this.activeProcess = played;
|
|
31647
31990
|
writeEditorRunNote({ projectPath, transcript: transcript.path, startedAt });
|
|
@@ -31652,9 +31995,14 @@ class GodotServer {
|
|
|
31652
31995
|
scene: scene === null ? "the main scene" : `res://${scene}`,
|
|
31653
31996
|
refreshedClasses,
|
|
31654
31997
|
debugPort: readNumber(asParams(JSON.parse(answer.content[0]?.text ?? "{}")), "debugPort"),
|
|
31655
|
-
endedPreviousRun: ended
|
|
31998
|
+
endedPreviousRun: endedPreviousRun(ended),
|
|
31999
|
+
breakpoints: breakpoints.applied.length === 0 ? undefined : this.breakpointsAsProjectSpells(projectPath, breakpoints.applied),
|
|
32000
|
+
breakpointsRefused: breakpoints.refused.length === 0 ? undefined : breakpoints.refused.map((one) => ({
|
|
32001
|
+
scriptPath: projectSpelling(projectPath, one.scriptPath),
|
|
32002
|
+
reason: one.reason
|
|
32003
|
+
})),
|
|
31656
32004
|
runtime: await this.runtimeUp(projectPath, alreadyPlaying, runtimeWaitMs, false),
|
|
31657
|
-
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
|
|
32005
|
+
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)
|
|
31658
32006
|
});
|
|
31659
32007
|
}
|
|
31660
32008
|
drainEditorOutput(game) {
|
|
@@ -31850,7 +32198,7 @@ class GodotServer {
|
|
|
31850
32198
|
return;
|
|
31851
32199
|
}
|
|
31852
32200
|
try {
|
|
31853
|
-
await this.dap().
|
|
32201
|
+
await this.dap().attach();
|
|
31854
32202
|
} catch (error) {
|
|
31855
32203
|
this.logDebug(`Picked up an editor-played run without its adapter: ${errorMessage(error)}`);
|
|
31856
32204
|
}
|
|
@@ -31872,6 +32220,18 @@ class GodotServer {
|
|
|
31872
32220
|
};
|
|
31873
32221
|
this.activeProcess = picked;
|
|
31874
32222
|
}
|
|
32223
|
+
announcedPidOf(run) {
|
|
32224
|
+
if (!run.throughEditor || run.announcedPid !== undefined) {
|
|
32225
|
+
return run.announcedPid;
|
|
32226
|
+
}
|
|
32227
|
+
const before = run.announcedBefore ?? new Set;
|
|
32228
|
+
const fresh = this.allAnnouncedForOurProject(runtimesAnnounced().running).filter((one) => !before.has(one.pid));
|
|
32229
|
+
const theOne = fresh.length === 1 ? fresh[0] : undefined;
|
|
32230
|
+
if (theOne !== undefined) {
|
|
32231
|
+
run.announcedPid = theOne.pid;
|
|
32232
|
+
}
|
|
32233
|
+
return run.announcedPid;
|
|
32234
|
+
}
|
|
31875
32235
|
adoptRecordedRun() {
|
|
31876
32236
|
const record = readRunRecord();
|
|
31877
32237
|
if (record === null || !this.couldBeOurs(record.projectPath)) {
|
|
@@ -32000,12 +32360,22 @@ class GodotServer {
|
|
|
32000
32360
|
}
|
|
32001
32361
|
const budgetMs = readPositiveNumber(args, "timeoutMs") ?? WAIT_FOR_RUN_MS;
|
|
32002
32362
|
const until = Date.now() + budgetMs;
|
|
32003
|
-
while (
|
|
32363
|
+
while (await this.runStillGoing(run) && Date.now() < until) {
|
|
32004
32364
|
this.drainEditorOutput(run);
|
|
32005
32365
|
this.drainTranscript(run);
|
|
32006
32366
|
await delay3(RUN_POLL_MS);
|
|
32007
32367
|
}
|
|
32008
|
-
return await this.handleGetDebugOutput(args,
|
|
32368
|
+
return await this.handleGetDebugOutput(args, await this.runStillGoing(run) ? budgetMs : undefined);
|
|
32369
|
+
}
|
|
32370
|
+
async runStillGoing(run, editorSays) {
|
|
32371
|
+
if (!run.throughEditor) {
|
|
32372
|
+
return stillRunning(run);
|
|
32373
|
+
}
|
|
32374
|
+
const announced = this.announcedPidOf(run);
|
|
32375
|
+
if (announced !== undefined && !alive(announced)) {
|
|
32376
|
+
return false;
|
|
32377
|
+
}
|
|
32378
|
+
return runIsUp(run, editorSays === undefined ? (await this.editorPlayingState())?.playing ?? null : editorSays);
|
|
32009
32379
|
}
|
|
32010
32380
|
async handleGetDebugOutput(args, waitedMs) {
|
|
32011
32381
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
@@ -32015,7 +32385,8 @@ class GodotServer {
|
|
|
32015
32385
|
}
|
|
32016
32386
|
this.drainEditorOutput(run);
|
|
32017
32387
|
this.drainTranscript(run);
|
|
32018
|
-
|
|
32388
|
+
const going = await this.runStillGoing(run);
|
|
32389
|
+
if (run.endedUnwatched !== true && !going) {
|
|
32019
32390
|
run.log.finish();
|
|
32020
32391
|
run.endedUnwatched = run.exitCode === null;
|
|
32021
32392
|
}
|
|
@@ -32026,12 +32397,13 @@ class GodotServer {
|
|
|
32026
32397
|
contains: readNonEmptyString(args, "contains"),
|
|
32027
32398
|
limit: readPositiveNumber(args, "limit") ?? 200
|
|
32028
32399
|
});
|
|
32029
|
-
const
|
|
32400
|
+
const hold = await this.holdOf(run, going);
|
|
32030
32401
|
const wanted = readBoolean(args, "cpu") ?? false;
|
|
32031
|
-
const
|
|
32402
|
+
const processToAsk = run.pid ?? this.announcedPidOf(run) ?? null;
|
|
32403
|
+
const cpuSeconds = wanted && processToAsk !== null && stillRunning(run) ? await cpuSecondsOf(processToAsk) : undefined;
|
|
32032
32404
|
const notes = [];
|
|
32033
32405
|
if (wanted && cpuSeconds === undefined && stillRunning(run)) {
|
|
32034
|
-
notes.push(
|
|
32406
|
+
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.");
|
|
32035
32407
|
}
|
|
32036
32408
|
if (waitedMs !== undefined) {
|
|
32037
32409
|
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.`);
|
|
@@ -32055,15 +32427,15 @@ class GodotServer {
|
|
|
32055
32427
|
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.`);
|
|
32056
32428
|
}
|
|
32057
32429
|
return this.jsonTextResponse({
|
|
32058
|
-
running:
|
|
32430
|
+
running: going,
|
|
32059
32431
|
exitCode: run.exitCode,
|
|
32060
32432
|
through: run.throughEditor ? "editor" : "gdharness",
|
|
32061
|
-
pid: run.pid,
|
|
32433
|
+
pid: run.pid ?? this.announcedPidOf(run) ?? null,
|
|
32062
32434
|
errors: run.log.count("error"),
|
|
32063
32435
|
warnings: run.log.count("warning"),
|
|
32064
32436
|
clean: run.consoleLost === true ? undefined : run.log.count("error") === 0,
|
|
32065
32437
|
consoleLost: run.consoleLost === true ? true : undefined,
|
|
32066
|
-
|
|
32438
|
+
...hold,
|
|
32067
32439
|
endedUnwatched: run.endedUnwatched === true ? true : undefined,
|
|
32068
32440
|
endedBy: stillRunning(run) ? undefined : run.endedHere ?? null,
|
|
32069
32441
|
project: !run.throughEditor && run.process === null ? run.projectPath ?? undefined : undefined,
|
|
@@ -32084,18 +32456,19 @@ class GodotServer {
|
|
|
32084
32456
|
}
|
|
32085
32457
|
this.drainEditorOutput(stopped);
|
|
32086
32458
|
this.drainTranscript(stopped);
|
|
32459
|
+
const wasRunning = await this.runStillGoing(stopped);
|
|
32087
32460
|
this.logDebug("Stopping the running game");
|
|
32088
32461
|
await this.endActiveGame("editor_run stop");
|
|
32089
32462
|
return this.jsonTextResponse({
|
|
32090
32463
|
stopped: true,
|
|
32091
32464
|
through: stopped.throughEditor ? "editor" : "gdharness",
|
|
32092
|
-
endedPid: stopped.pid,
|
|
32093
|
-
exitedBeforeStop: stopped.exitCode !== null,
|
|
32465
|
+
endedPid: stopped.pid ?? this.announcedPidOf(stopped) ?? null,
|
|
32466
|
+
exitedBeforeStop: stopped.exitCode !== null || !wasRunning,
|
|
32094
32467
|
exitCode: stopped.exitCode,
|
|
32095
32468
|
errors: stopped.log.count("error"),
|
|
32096
32469
|
warnings: stopped.log.count("warning"),
|
|
32097
32470
|
clean: stopped.log.count("error") === 0,
|
|
32098
|
-
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.",
|
|
32471
|
+
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.",
|
|
32099
32472
|
entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
32100
32473
|
});
|
|
32101
32474
|
}
|
|
@@ -32246,16 +32619,68 @@ class GodotServer {
|
|
|
32246
32619
|
const override = Number.parseInt(envValue("GDHARNESS_RUNTIME_TIMEOUT_MS") ?? "", 10);
|
|
32247
32620
|
return Number.isInteger(override) && override > 0 ? override : 1e4;
|
|
32248
32621
|
}
|
|
32622
|
+
knownHoldOn(endpoint) {
|
|
32623
|
+
const run = this.activeProcess;
|
|
32624
|
+
const session = this.dapClient;
|
|
32625
|
+
if (run === null || !run.throughEditor || !stillRunning(run) || session === null) {
|
|
32626
|
+
return null;
|
|
32627
|
+
}
|
|
32628
|
+
if (this.announcedPidOf(run) !== endpoint.pid || !session.holdIsKnown()) {
|
|
32629
|
+
return null;
|
|
32630
|
+
}
|
|
32631
|
+
return session.whereItStopped();
|
|
32632
|
+
}
|
|
32633
|
+
async holdOf(run, going) {
|
|
32634
|
+
if (!run.throughEditor || !(going ?? await this.runStillGoing(run))) {
|
|
32635
|
+
return { heldAt: null };
|
|
32636
|
+
}
|
|
32637
|
+
const session = this.dapClient;
|
|
32638
|
+
if (session?.holdIsKnown()) {
|
|
32639
|
+
return { heldAt: session.whereItStopped() };
|
|
32640
|
+
}
|
|
32641
|
+
const its = this.announcedPidOf(run);
|
|
32642
|
+
const endpoint = its === undefined ? undefined : runtimesAnnounced().running.find((one) => one.pid === its);
|
|
32643
|
+
if (endpoint === undefined) {
|
|
32644
|
+
return {
|
|
32645
|
+
heldAt: undefined,
|
|
32646
|
+
heldUnknown: true,
|
|
32647
|
+
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."
|
|
32648
|
+
};
|
|
32649
|
+
}
|
|
32650
|
+
const reply = await runtimeRequest(endpoint, "ping", {}, HOLD_PING_MS);
|
|
32651
|
+
if (reply.ok) {
|
|
32652
|
+
session?.learnedRunning();
|
|
32653
|
+
return { heldAt: session?.whereItStopped() ?? null };
|
|
32654
|
+
}
|
|
32655
|
+
if (reply.reason === "busy") {
|
|
32656
|
+
return {
|
|
32657
|
+
heldAt: {
|
|
32658
|
+
reason: "unanswered",
|
|
32659
|
+
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`,
|
|
32660
|
+
text: ""
|
|
32661
|
+
}
|
|
32662
|
+
};
|
|
32663
|
+
}
|
|
32664
|
+
return { heldAt: undefined, heldUnknown: true, heldNote: reply.message };
|
|
32665
|
+
}
|
|
32249
32666
|
async handleRuntimeCommand(command, args, timeoutMs = this.runtimeTimeoutMs()) {
|
|
32250
|
-
const { op: _op, projectPath, ...params } = asParams(args);
|
|
32667
|
+
const { op: _op, projectPath, pid, ...params } = asParams(args);
|
|
32251
32668
|
const announced = runtimesAnnounced();
|
|
32252
|
-
const choice = chooseRuntime(announced.running, typeof projectPath === "string" ? projectPath : this.ownProject ?? undefined, announced.unspoken);
|
|
32669
|
+
const choice = chooseRuntime(announced.running, typeof projectPath === "string" ? projectPath : this.ownProject ?? undefined, announced.unspoken, typeof pid === "number" ? pid : undefined);
|
|
32253
32670
|
if ("problem" in choice) {
|
|
32254
32671
|
return this.createErrorResponse(choice.problem);
|
|
32255
32672
|
}
|
|
32673
|
+
const held = this.knownHoldOn(choice.endpoint);
|
|
32674
|
+
if (held !== null) {
|
|
32675
|
+
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.`, [
|
|
32676
|
+
"debug_control continue lets it go, after which this call answers",
|
|
32677
|
+
"debug_state stack reads where it is held, and debug_state scopes what it is holding there",
|
|
32678
|
+
"editor_output says heldAt for the run, and editor_run stop ends it"
|
|
32679
|
+
]);
|
|
32680
|
+
}
|
|
32256
32681
|
const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
|
|
32257
|
-
const screenshotDir = expectsScreenshot ? mkdtempSync2(
|
|
32258
|
-
const screenshotPath = screenshotDir ?
|
|
32682
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync2(join13(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
|
|
32683
|
+
const screenshotPath = screenshotDir ? join13(screenshotDir, "capture.png") : null;
|
|
32259
32684
|
try {
|
|
32260
32685
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
|
|
32261
32686
|
if (!reply.ok) {
|
|
@@ -32276,7 +32701,7 @@ class GodotServer {
|
|
|
32276
32701
|
return {
|
|
32277
32702
|
content: [
|
|
32278
32703
|
{ type: "text", text: `Screenshot captured: ${dimensions}` },
|
|
32279
|
-
{ type: "image", data:
|
|
32704
|
+
{ type: "image", data: readFileSync13(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
32280
32705
|
]
|
|
32281
32706
|
};
|
|
32282
32707
|
} finally {
|
|
@@ -32307,7 +32732,7 @@ class GodotServer {
|
|
|
32307
32732
|
const includeHidden = readBoolean(args, "includeHidden");
|
|
32308
32733
|
return await this.handleRuntimeCommand("find_nodes", {
|
|
32309
32734
|
...filters,
|
|
32310
|
-
|
|
32735
|
+
...whichGame(args),
|
|
32311
32736
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
32312
32737
|
limit: readPositiveNumber(args, "limit") ?? 100,
|
|
32313
32738
|
...property === undefined ? {} : { property },
|
|
@@ -32318,18 +32743,18 @@ class GodotServer {
|
|
|
32318
32743
|
if (op === "frames") {
|
|
32319
32744
|
const frames = readPositiveNumber(args, "frames") ?? 1;
|
|
32320
32745
|
const waited = patienceForFrames(frames, this.runtimeTimeoutMs());
|
|
32321
|
-
return await this.handleRuntimeCommand("wait_frames", {
|
|
32746
|
+
return await this.handleRuntimeCommand("wait_frames", { ...whichGame(args), frames }, waited);
|
|
32322
32747
|
}
|
|
32323
32748
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 5000;
|
|
32324
32749
|
const nodePath = readNonEmptyString(args, "nodePath") ?? "";
|
|
32325
32750
|
const patience = Math.max(this.runtimeTimeoutMs(), timeoutMs + 5000);
|
|
32326
32751
|
return op === "signal" ? await this.handleRuntimeCommand("wait_signal", {
|
|
32327
|
-
|
|
32752
|
+
...whichGame(args),
|
|
32328
32753
|
path: nodePath,
|
|
32329
32754
|
signal: readString(args, "signal") ?? "",
|
|
32330
32755
|
timeout_ms: timeoutMs
|
|
32331
32756
|
}, patience) : await this.handleRuntimeCommand("wait_until", {
|
|
32332
|
-
|
|
32757
|
+
...whichGame(args),
|
|
32333
32758
|
path: nodePath,
|
|
32334
32759
|
property: readString(args, "property") ?? "",
|
|
32335
32760
|
value: args["value"],
|