gdharness 0.13.38 → 0.13.39
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 +396 -132
- package/build/index.js +477 -215
- 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,6 +25246,7 @@ class GodotDAPClient {
|
|
|
25127
25246
|
attached = false;
|
|
25128
25247
|
lastThreadId = 1;
|
|
25129
25248
|
halt = null;
|
|
25249
|
+
holdKnown = false;
|
|
25130
25250
|
breakpoints = new Map;
|
|
25131
25251
|
constructor(port = portFromEnv("GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT), host = "127.0.0.1") {
|
|
25132
25252
|
this.port = port;
|
|
@@ -25172,11 +25292,7 @@ class GodotDAPClient {
|
|
|
25172
25292
|
this.failPendingRequests(error);
|
|
25173
25293
|
});
|
|
25174
25294
|
socket.on("close", () => {
|
|
25175
|
-
this.
|
|
25176
|
-
this.initialized = false;
|
|
25177
|
-
this.attached = false;
|
|
25178
|
-
this.halt = null;
|
|
25179
|
-
this.socket = null;
|
|
25295
|
+
this.forgetConnection();
|
|
25180
25296
|
this.failPendingRequests(new Error("DAP connection closed"));
|
|
25181
25297
|
});
|
|
25182
25298
|
resolve();
|
|
@@ -25192,10 +25308,7 @@ class GodotDAPClient {
|
|
|
25192
25308
|
}
|
|
25193
25309
|
async abandon() {
|
|
25194
25310
|
if (!this.socket) {
|
|
25195
|
-
this.
|
|
25196
|
-
this.initialized = false;
|
|
25197
|
-
this.attached = false;
|
|
25198
|
-
this.halt = null;
|
|
25311
|
+
this.forgetConnection();
|
|
25199
25312
|
return;
|
|
25200
25313
|
}
|
|
25201
25314
|
await new Promise((resolve) => {
|
|
@@ -25215,11 +25328,15 @@ class GodotDAPClient {
|
|
|
25215
25328
|
resolve();
|
|
25216
25329
|
}, 500);
|
|
25217
25330
|
});
|
|
25331
|
+
this.forgetConnection();
|
|
25332
|
+
}
|
|
25333
|
+
forgetConnection() {
|
|
25218
25334
|
this.socket = null;
|
|
25219
25335
|
this.connected = false;
|
|
25220
25336
|
this.initialized = false;
|
|
25221
25337
|
this.attached = false;
|
|
25222
25338
|
this.halt = null;
|
|
25339
|
+
this.holdKnown = false;
|
|
25223
25340
|
}
|
|
25224
25341
|
async ensureConnected() {
|
|
25225
25342
|
if (!this.connected) {
|
|
@@ -25308,6 +25425,12 @@ class GodotDAPClient {
|
|
|
25308
25425
|
description: said(body, "description"),
|
|
25309
25426
|
text: said(body, "text")
|
|
25310
25427
|
};
|
|
25428
|
+
this.holdKnown = true;
|
|
25429
|
+
return;
|
|
25430
|
+
}
|
|
25431
|
+
if (eventName === "continued") {
|
|
25432
|
+
this.halt = null;
|
|
25433
|
+
this.holdKnown = true;
|
|
25311
25434
|
return;
|
|
25312
25435
|
}
|
|
25313
25436
|
if (eventName === "terminated" || eventName === "exited") {
|
|
@@ -25346,6 +25469,43 @@ class GodotDAPClient {
|
|
|
25346
25469
|
await this.sendRequest("attach", {});
|
|
25347
25470
|
await this.sendRequest("configurationDone", {});
|
|
25348
25471
|
this.attached = true;
|
|
25472
|
+
if (!this.holdKnown) {
|
|
25473
|
+
await this.learnWhetherHeld();
|
|
25474
|
+
}
|
|
25475
|
+
}
|
|
25476
|
+
async learnWhetherHeld() {
|
|
25477
|
+
try {
|
|
25478
|
+
const threads = await this.sendRequest("threads");
|
|
25479
|
+
const listed = threads["threads"];
|
|
25480
|
+
const first = Array.isArray(listed) ? listed[0]?.["id"] : undefined;
|
|
25481
|
+
if (typeof first !== "number") {
|
|
25482
|
+
return;
|
|
25483
|
+
}
|
|
25484
|
+
this.lastThreadId = first;
|
|
25485
|
+
const response = await this.sendRequest("stackTrace", { threadId: first, startFrame: 0, levels: 1 });
|
|
25486
|
+
if (this.holdKnown) {
|
|
25487
|
+
return;
|
|
25488
|
+
}
|
|
25489
|
+
const frames = response["stackFrames"];
|
|
25490
|
+
if (Array.isArray(frames) && frames.length > 0) {
|
|
25491
|
+
this.halt = {
|
|
25492
|
+
reason: "attached",
|
|
25493
|
+
description: "held when this session attached; the stop that held it was reported to an earlier one",
|
|
25494
|
+
text: ""
|
|
25495
|
+
};
|
|
25496
|
+
this.holdKnown = true;
|
|
25497
|
+
}
|
|
25498
|
+
} catch {}
|
|
25499
|
+
}
|
|
25500
|
+
holdIsKnown() {
|
|
25501
|
+
return this.holdKnown;
|
|
25502
|
+
}
|
|
25503
|
+
learnedRunning() {
|
|
25504
|
+
if (this.holdKnown) {
|
|
25505
|
+
return;
|
|
25506
|
+
}
|
|
25507
|
+
this.halt = null;
|
|
25508
|
+
this.holdKnown = true;
|
|
25349
25509
|
}
|
|
25350
25510
|
setOutputSink(sink) {
|
|
25351
25511
|
this.onOutput = sink;
|
|
@@ -25358,38 +25518,70 @@ class GodotDAPClient {
|
|
|
25358
25518
|
return lines;
|
|
25359
25519
|
}
|
|
25360
25520
|
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
|
-
});
|
|
25521
|
+
const wanted = new Set(this.breakpoints.get(filePath) ?? []);
|
|
25522
|
+
wanted.add(line);
|
|
25523
|
+
return await this.sendBreakpoints(filePath, wanted);
|
|
25371
25524
|
}
|
|
25372
25525
|
async removeBreakpoint(filePath, line) {
|
|
25526
|
+
const wanted = new Set(this.breakpoints.get(filePath) ?? []);
|
|
25527
|
+
wanted.delete(line);
|
|
25528
|
+
return await this.sendBreakpoints(filePath, wanted);
|
|
25529
|
+
}
|
|
25530
|
+
async sendBreakpoints(filePath, lines) {
|
|
25373
25531
|
await this.ensureConnected();
|
|
25374
25532
|
await this.initialize();
|
|
25375
|
-
const
|
|
25376
|
-
|
|
25377
|
-
|
|
25533
|
+
const sorted = Array.from(lines).sort((a, b) => a - b);
|
|
25534
|
+
const answer = await this.sendRequest("setBreakpoints", {
|
|
25535
|
+
source: { path: filePath },
|
|
25536
|
+
breakpoints: sorted.map((breakpointLine) => ({ line: breakpointLine }))
|
|
25537
|
+
});
|
|
25538
|
+
if (lines.size === 0) {
|
|
25378
25539
|
this.breakpoints.delete(filePath);
|
|
25379
25540
|
} else {
|
|
25380
|
-
this.breakpoints.set(filePath,
|
|
25541
|
+
this.breakpoints.set(filePath, new Set(lines));
|
|
25542
|
+
}
|
|
25543
|
+
return answer;
|
|
25544
|
+
}
|
|
25545
|
+
async reapplyBreakpoints() {
|
|
25546
|
+
const applied = [];
|
|
25547
|
+
const refused = [];
|
|
25548
|
+
for (const [filePath, lines] of Array.from(this.breakpoints)) {
|
|
25549
|
+
try {
|
|
25550
|
+
await this.sendBreakpoints(filePath, lines);
|
|
25551
|
+
applied.push({ scriptPath: filePath, lines: Array.from(lines).sort((a, b) => a - b) });
|
|
25552
|
+
} catch (error) {
|
|
25553
|
+
this.breakpoints.delete(filePath);
|
|
25554
|
+
refused.push({
|
|
25555
|
+
scriptPath: filePath,
|
|
25556
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
25557
|
+
});
|
|
25558
|
+
}
|
|
25559
|
+
}
|
|
25560
|
+
return { applied, refused };
|
|
25561
|
+
}
|
|
25562
|
+
breakpointsHeld() {
|
|
25563
|
+
return Array.from(this.breakpoints, ([scriptPath, lines]) => ({
|
|
25564
|
+
scriptPath,
|
|
25565
|
+
lines: Array.from(lines).sort((a, b) => a - b)
|
|
25566
|
+
}));
|
|
25567
|
+
}
|
|
25568
|
+
holdBreakpoints(held) {
|
|
25569
|
+
for (const { scriptPath, lines } of held) {
|
|
25570
|
+
const fileBreakpoints = this.breakpoints.get(scriptPath) ?? new Set;
|
|
25571
|
+
for (const line of lines) {
|
|
25572
|
+
fileBreakpoints.add(line);
|
|
25573
|
+
}
|
|
25574
|
+
if (fileBreakpoints.size > 0) {
|
|
25575
|
+
this.breakpoints.set(scriptPath, fileBreakpoints);
|
|
25576
|
+
}
|
|
25381
25577
|
}
|
|
25382
|
-
const remaining = Array.from(fileBreakpoints).sort((a, b) => a - b);
|
|
25383
|
-
return await this.sendRequest("setBreakpoints", {
|
|
25384
|
-
source: { path: filePath },
|
|
25385
|
-
breakpoints: remaining.map((breakpointLine) => ({ line: breakpointLine }))
|
|
25386
|
-
});
|
|
25387
25578
|
}
|
|
25388
25579
|
async continue(threadId) {
|
|
25389
25580
|
await this.attach();
|
|
25390
25581
|
const resolvedThreadId = await this.resolveThreadId(threadId);
|
|
25391
25582
|
await this.sendRequest("continue", { threadId: resolvedThreadId });
|
|
25392
25583
|
this.halt = null;
|
|
25584
|
+
this.holdKnown = true;
|
|
25393
25585
|
}
|
|
25394
25586
|
async stepOver(threadId) {
|
|
25395
25587
|
await this.attach();
|
|
@@ -25488,11 +25680,7 @@ class GodotDAPClient {
|
|
|
25488
25680
|
}
|
|
25489
25681
|
failOversizedStream(detail) {
|
|
25490
25682
|
const socket = this.socket;
|
|
25491
|
-
this.
|
|
25492
|
-
this.connected = false;
|
|
25493
|
-
this.initialized = false;
|
|
25494
|
-
this.attached = false;
|
|
25495
|
-
this.halt = null;
|
|
25683
|
+
this.forgetConnection();
|
|
25496
25684
|
this.reader = new FrameReader;
|
|
25497
25685
|
socket?.destroy();
|
|
25498
25686
|
this.failPendingRequests(new Error(`Godot DAP ${detail}. The connection was dropped.`));
|
|
@@ -25715,65 +25903,6 @@ var import_subprotocol = __toESM(require_subprotocol(), 1);
|
|
|
25715
25903
|
var import_websocket = __toESM(require_websocket(), 1);
|
|
25716
25904
|
var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
25717
25905
|
|
|
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
25906
|
// src/godot-bridge.ts
|
|
25778
25907
|
var DEFAULT_PORT = 6505;
|
|
25779
25908
|
var DEFAULT_HOST = "127.0.0.1";
|
|
@@ -26265,7 +26394,7 @@ import { promisify } from "node:util";
|
|
|
26265
26394
|
// src/detection.ts
|
|
26266
26395
|
import { existsSync as existsSync4, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
|
|
26267
26396
|
import { homedir as homedir2 } from "node:os";
|
|
26268
|
-
import { join as
|
|
26397
|
+
import { join as join6 } from "node:path";
|
|
26269
26398
|
function resolveHomeDirectory() {
|
|
26270
26399
|
try {
|
|
26271
26400
|
return homedir2();
|
|
@@ -26289,7 +26418,7 @@ function scanDirectoryForGodotBinaries(directory, platform) {
|
|
|
26289
26418
|
if (!pattern.test(name)) {
|
|
26290
26419
|
continue;
|
|
26291
26420
|
}
|
|
26292
|
-
const fullPath =
|
|
26421
|
+
const fullPath = join6(directory, name);
|
|
26293
26422
|
try {
|
|
26294
26423
|
const stat = statSync2(fullPath);
|
|
26295
26424
|
if (stat.isFile()) {
|
|
@@ -26298,7 +26427,7 @@ function scanDirectoryForGodotBinaries(directory, platform) {
|
|
|
26298
26427
|
} catch {}
|
|
26299
26428
|
}
|
|
26300
26429
|
matches.sort((a, b) => b.mtime - a.mtime);
|
|
26301
|
-
return matches.map((m) =>
|
|
26430
|
+
return matches.map((m) => join6(directory, m.name));
|
|
26302
26431
|
}
|
|
26303
26432
|
function conventionalPaths(platform, home) {
|
|
26304
26433
|
const paths = ["godot"];
|
|
@@ -26467,13 +26596,13 @@ class GodotLocator {
|
|
|
26467
26596
|
|
|
26468
26597
|
// src/headless.ts
|
|
26469
26598
|
import { execFile as execFile2 } from "node:child_process";
|
|
26470
|
-
import { mkdtempSync, writeFileSync as
|
|
26599
|
+
import { mkdtempSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
26471
26600
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
26472
|
-
import { join as
|
|
26601
|
+
import { join as join7 } from "node:path";
|
|
26473
26602
|
import { promisify as promisify2 } from "node:util";
|
|
26474
26603
|
|
|
26475
26604
|
// src/scratch.ts
|
|
26476
|
-
import { rmSync as
|
|
26605
|
+
import { rmSync as rmSync3 } from "node:fs";
|
|
26477
26606
|
var SWEPT = { recursive: true, force: true, maxRetries: 20, retryDelay: 250 };
|
|
26478
26607
|
function discardWith(remove, paths) {
|
|
26479
26608
|
const left = [];
|
|
@@ -26491,7 +26620,7 @@ function discardWith(remove, paths) {
|
|
|
26491
26620
|
}
|
|
26492
26621
|
function discard(...paths) {
|
|
26493
26622
|
return discardWith((path) => {
|
|
26494
|
-
|
|
26623
|
+
rmSync3(path, SWEPT);
|
|
26495
26624
|
}, paths);
|
|
26496
26625
|
}
|
|
26497
26626
|
|
|
@@ -26535,12 +26664,12 @@ function reason(stdout, stderr) {
|
|
|
26535
26664
|
return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
|
|
26536
26665
|
}
|
|
26537
26666
|
async function runImport(godotPath, projectPath) {
|
|
26538
|
-
const logDir = mkdtempSync(
|
|
26667
|
+
const logDir = mkdtempSync(join7(tmpdir2(), "gdharness-import-"));
|
|
26539
26668
|
try {
|
|
26540
26669
|
const { stderr } = await run2(godotPath, [
|
|
26541
26670
|
"--headless",
|
|
26542
26671
|
"--log-file",
|
|
26543
|
-
|
|
26672
|
+
join7(logDir, "engine.log"),
|
|
26544
26673
|
"--path",
|
|
26545
26674
|
projectPath,
|
|
26546
26675
|
"--import"
|
|
@@ -26565,13 +26694,13 @@ async function runImport(godotPath, projectPath) {
|
|
|
26565
26694
|
}
|
|
26566
26695
|
}
|
|
26567
26696
|
async function runOperation(engine, operation, params, projectPath) {
|
|
26568
|
-
const paramsDir = mkdtempSync(
|
|
26569
|
-
const paramsFile =
|
|
26570
|
-
|
|
26697
|
+
const paramsDir = mkdtempSync(join7(tmpdir2(), "gdharness-params-"));
|
|
26698
|
+
const paramsFile = join7(paramsDir, `${operation}.json`);
|
|
26699
|
+
writeFileSync4(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
|
|
26571
26700
|
const args = [
|
|
26572
26701
|
"--headless",
|
|
26573
26702
|
"--log-file",
|
|
26574
|
-
|
|
26703
|
+
join7(paramsDir, "engine.log"),
|
|
26575
26704
|
"--path",
|
|
26576
26705
|
projectPath,
|
|
26577
26706
|
"--script",
|
|
@@ -27000,7 +27129,7 @@ function orphansPrinted(printed) {
|
|
|
27000
27129
|
import { realpathSync as realpathSync2 } from "node:fs";
|
|
27001
27130
|
import { readFile, realpath } from "node:fs/promises";
|
|
27002
27131
|
import { createConnection as createConnection2 } from "node:net";
|
|
27003
|
-
import { dirname as
|
|
27132
|
+
import { dirname as dirname3, resolve as resolve2 } from "node:path";
|
|
27004
27133
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
27005
27134
|
var DEFAULT_LSP_PORT = 6005;
|
|
27006
27135
|
var DIAGNOSTICS_TIMEOUT_MS = 5000;
|
|
@@ -27130,7 +27259,7 @@ class GodotLSPClient {
|
|
|
27130
27259
|
if (this.initialized) {
|
|
27131
27260
|
return;
|
|
27132
27261
|
}
|
|
27133
|
-
const rootPath = this.rootPath ??
|
|
27262
|
+
const rootPath = this.rootPath ?? dirname3(resolve2(filePath));
|
|
27134
27263
|
await this.initialize(rootPath);
|
|
27135
27264
|
}
|
|
27136
27265
|
async sendRequest(method, params) {
|
|
@@ -27566,14 +27695,14 @@ function secondsFromClock(printed) {
|
|
|
27566
27695
|
}
|
|
27567
27696
|
|
|
27568
27697
|
// src/project-scan.ts
|
|
27569
|
-
import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as
|
|
27570
|
-
import { join as
|
|
27698
|
+
import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync7 } from "node:fs";
|
|
27699
|
+
import { join as join8 } from "node:path";
|
|
27571
27700
|
var SKIPPED = new Set(["node_modules"]);
|
|
27572
27701
|
function skipped(name) {
|
|
27573
27702
|
return name.startsWith(".") || SKIPPED.has(name);
|
|
27574
27703
|
}
|
|
27575
27704
|
function steppedOver(directory) {
|
|
27576
|
-
return existsSync6(
|
|
27705
|
+
return existsSync6(join8(directory, ".gdignore"));
|
|
27577
27706
|
}
|
|
27578
27707
|
var ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
|
|
27579
27708
|
function projectStructure(projectPath) {
|
|
@@ -27587,7 +27716,7 @@ function projectStructure(projectPath) {
|
|
|
27587
27716
|
continue;
|
|
27588
27717
|
}
|
|
27589
27718
|
if (entry.isDirectory()) {
|
|
27590
|
-
visit(
|
|
27719
|
+
visit(join8(directory, entry.name));
|
|
27591
27720
|
} else if (entry.isFile()) {
|
|
27592
27721
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
27593
27722
|
if (extension === "tscn") {
|
|
@@ -27618,14 +27747,14 @@ function scriptsWithoutUid(projectPath) {
|
|
|
27618
27747
|
}
|
|
27619
27748
|
const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
27620
27749
|
if (entry.isDirectory()) {
|
|
27621
|
-
visit(
|
|
27750
|
+
visit(join8(directory, entry.name), spelled);
|
|
27622
27751
|
continue;
|
|
27623
27752
|
}
|
|
27624
27753
|
if (!entry.isFile()) {
|
|
27625
27754
|
continue;
|
|
27626
27755
|
}
|
|
27627
27756
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
27628
|
-
if (CARRIES_A_UID.has(extension) && !existsSync6(
|
|
27757
|
+
if (CARRIES_A_UID.has(extension) && !existsSync6(join8(directory, `${entry.name}.uid`))) {
|
|
27629
27758
|
found.push(spelled);
|
|
27630
27759
|
}
|
|
27631
27760
|
}
|
|
@@ -27660,7 +27789,7 @@ function searchProject(projectPath, options) {
|
|
|
27660
27789
|
if (skipped(entry.name)) {
|
|
27661
27790
|
continue;
|
|
27662
27791
|
}
|
|
27663
|
-
const entryPath =
|
|
27792
|
+
const entryPath = join8(directory, entry.name);
|
|
27664
27793
|
if (entry.isDirectory()) {
|
|
27665
27794
|
visit(entryPath);
|
|
27666
27795
|
continue;
|
|
@@ -27671,7 +27800,7 @@ function searchProject(projectPath, options) {
|
|
|
27671
27800
|
}
|
|
27672
27801
|
result.summary.files_searched += 1;
|
|
27673
27802
|
const matches = [];
|
|
27674
|
-
for (const [index, line] of
|
|
27803
|
+
for (const [index, line] of readFileSync7(entryPath, "utf8").split(`
|
|
27675
27804
|
`).entries()) {
|
|
27676
27805
|
if (full()) {
|
|
27677
27806
|
break;
|
|
@@ -27694,7 +27823,7 @@ function searchProject(projectPath, options) {
|
|
|
27694
27823
|
}
|
|
27695
27824
|
|
|
27696
27825
|
// src/resources.ts
|
|
27697
|
-
import { readFileSync as
|
|
27826
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
27698
27827
|
import { extname, resolve as resolve3 } from "node:path";
|
|
27699
27828
|
var STATIC_RESOURCES = [
|
|
27700
27829
|
{
|
|
@@ -27889,7 +28018,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
27889
28018
|
const parsedUri = parseGodotUri(uri);
|
|
27890
28019
|
if (parsedUri.kind === "project-info") {
|
|
27891
28020
|
const projectFilePath = resolveProjectFile(projectPath, "project.godot");
|
|
27892
|
-
const rawProject =
|
|
28021
|
+
const rawProject = readFileSync8(projectFilePath, "utf-8");
|
|
27893
28022
|
const parsedProject = parseProjectGodot(rawProject);
|
|
27894
28023
|
return {
|
|
27895
28024
|
mimeType: "application/json",
|
|
@@ -27898,7 +28027,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
27898
28027
|
}
|
|
27899
28028
|
const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
|
|
27900
28029
|
ensureAllowedExtension(parsedUri.kind, filePath);
|
|
27901
|
-
const text =
|
|
28030
|
+
const text = readFileSync8(filePath, "utf-8");
|
|
27902
28031
|
return {
|
|
27903
28032
|
mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
|
|
27904
28033
|
text
|
|
@@ -27935,16 +28064,16 @@ function setupResourceHandlers(mcp, getProjectPath) {
|
|
|
27935
28064
|
}
|
|
27936
28065
|
|
|
27937
28066
|
// src/restart-note.ts
|
|
27938
|
-
import { mkdirSync as
|
|
27939
|
-
import { dirname as
|
|
28067
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync9, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
28068
|
+
import { dirname as dirname4, join as join9 } from "node:path";
|
|
27940
28069
|
function restartNotePath(projectPath) {
|
|
27941
|
-
return
|
|
28070
|
+
return join9(projectPath, ".godot", "gdharness-restart.json");
|
|
27942
28071
|
}
|
|
27943
28072
|
function noteRestartBegun(note) {
|
|
27944
28073
|
const path = restartNotePath(note.projectPath);
|
|
27945
28074
|
try {
|
|
27946
|
-
|
|
27947
|
-
|
|
28075
|
+
mkdirSync4(dirname4(path), { recursive: true });
|
|
28076
|
+
writeFileSync5(path, `${JSON.stringify(note, null, 2)}
|
|
27948
28077
|
`, "utf8");
|
|
27949
28078
|
} catch (error) {
|
|
27950
28079
|
console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
|
|
@@ -27952,7 +28081,7 @@ function noteRestartBegun(note) {
|
|
|
27952
28081
|
}
|
|
27953
28082
|
function restartOwed(projectPath) {
|
|
27954
28083
|
try {
|
|
27955
|
-
const parsed = JSON.parse(
|
|
28084
|
+
const parsed = JSON.parse(readFileSync9(restartNotePath(projectPath), "utf8"));
|
|
27956
28085
|
if (typeof parsed !== "object" || parsed === null) {
|
|
27957
28086
|
return null;
|
|
27958
28087
|
}
|
|
@@ -27967,7 +28096,7 @@ function restartOwed(projectPath) {
|
|
|
27967
28096
|
}
|
|
27968
28097
|
function restartSettled(projectPath) {
|
|
27969
28098
|
try {
|
|
27970
|
-
|
|
28099
|
+
rmSync4(restartNotePath(projectPath), { force: true });
|
|
27971
28100
|
} catch (error) {
|
|
27972
28101
|
console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
|
|
27973
28102
|
}
|
|
@@ -27975,14 +28104,14 @@ function restartSettled(projectPath) {
|
|
|
27975
28104
|
|
|
27976
28105
|
// src/run-record.ts
|
|
27977
28106
|
import { execFileSync } from "node:child_process";
|
|
27978
|
-
import { mkdirSync as
|
|
27979
|
-
import { basename, join as
|
|
28107
|
+
import { mkdirSync as mkdirSync5, openSync, readdirSync as readdirSync5, readFileSync as readFileSync11, rmSync as rmSync5, statSync as statSync3, writeFileSync as writeFileSync6 } from "node:fs";
|
|
28108
|
+
import { basename, join as join11 } from "node:path";
|
|
27980
28109
|
|
|
27981
28110
|
// src/runtime-client.ts
|
|
27982
|
-
import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as
|
|
28111
|
+
import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync10, unlinkSync } from "node:fs";
|
|
27983
28112
|
import { createConnection as createConnection3 } from "node:net";
|
|
27984
28113
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
27985
|
-
import { join as
|
|
28114
|
+
import { join as join10, resolve as resolve4 } from "node:path";
|
|
27986
28115
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
27987
28116
|
|
|
27988
28117
|
// src/tool-args.ts
|
|
@@ -28050,13 +28179,13 @@ function runtimeDirectory(variables = process.env) {
|
|
|
28050
28179
|
return explicit;
|
|
28051
28180
|
}
|
|
28052
28181
|
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
28053
|
-
return
|
|
28182
|
+
return join10(perUser ?? tmpdir3(), "gdharness");
|
|
28054
28183
|
}
|
|
28055
28184
|
function runtimeDirectories(variables = process.env) {
|
|
28056
28185
|
const candidates = [runtimeDirectory(variables)];
|
|
28057
28186
|
const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
|
|
28058
28187
|
for (const base of fallbacks) {
|
|
28059
|
-
candidates.push(
|
|
28188
|
+
candidates.push(join10(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
|
|
28060
28189
|
}
|
|
28061
28190
|
return [...new Set(candidates.map((path) => resolve4(path)))];
|
|
28062
28191
|
}
|
|
@@ -28071,7 +28200,7 @@ function processAlive(pid) {
|
|
|
28071
28200
|
function parseAnnouncement(file, pid) {
|
|
28072
28201
|
let fields;
|
|
28073
28202
|
try {
|
|
28074
|
-
fields = asParams(JSON.parse(
|
|
28203
|
+
fields = asParams(JSON.parse(readFileSync10(file, "utf8")));
|
|
28075
28204
|
} catch {
|
|
28076
28205
|
return { kind: "rubbish" };
|
|
28077
28206
|
}
|
|
@@ -28115,7 +28244,7 @@ var gone = [];
|
|
|
28115
28244
|
function wentAway(file, pid) {
|
|
28116
28245
|
let project = "";
|
|
28117
28246
|
try {
|
|
28118
|
-
const fields = asParams(JSON.parse(
|
|
28247
|
+
const fields = asParams(JSON.parse(readFileSync10(file, "utf8")));
|
|
28119
28248
|
project = readString(readParams(fields, "project") ?? {}, "path") ?? "";
|
|
28120
28249
|
} catch {
|
|
28121
28250
|
return;
|
|
@@ -28144,7 +28273,7 @@ function announcedIn(directory) {
|
|
|
28144
28273
|
if (!match) {
|
|
28145
28274
|
continue;
|
|
28146
28275
|
}
|
|
28147
|
-
const file =
|
|
28276
|
+
const file = join10(directory, entry);
|
|
28148
28277
|
const pid = Number.parseInt(match[1] ?? "", 10);
|
|
28149
28278
|
const alive = processAlive(pid);
|
|
28150
28279
|
const announced = alive ? parseAnnouncement(file, pid) : { kind: "rubbish" };
|
|
@@ -28319,33 +28448,33 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
|
|
|
28319
28448
|
|
|
28320
28449
|
// src/run-record.ts
|
|
28321
28450
|
function runsDirectory() {
|
|
28322
|
-
return
|
|
28451
|
+
return join11(runtimeDirectory(), "runs");
|
|
28323
28452
|
}
|
|
28324
28453
|
function recordPath() {
|
|
28325
|
-
return
|
|
28454
|
+
return join11(runsDirectory(), "run.json");
|
|
28326
28455
|
}
|
|
28327
28456
|
function recordPaths() {
|
|
28328
|
-
return runtimeDirectories().map((directory) =>
|
|
28457
|
+
return runtimeDirectories().map((directory) => join11(directory, "runs", "run.json"));
|
|
28329
28458
|
}
|
|
28330
28459
|
function openTranscript(startedAt) {
|
|
28331
|
-
|
|
28332
|
-
const path =
|
|
28460
|
+
mkdirSync5(runsDirectory(), { recursive: true });
|
|
28461
|
+
const path = join11(runsDirectory(), `run-${startedAt}.log`);
|
|
28333
28462
|
return { path, fd: openSync(path, "a") };
|
|
28334
28463
|
}
|
|
28335
28464
|
function writeRunRecord(record) {
|
|
28336
|
-
|
|
28337
|
-
|
|
28465
|
+
mkdirSync5(runsDirectory(), { recursive: true });
|
|
28466
|
+
writeFileSync6(recordPath(), JSON.stringify(record, null, 2), "utf8");
|
|
28338
28467
|
}
|
|
28339
28468
|
function editorNotePath() {
|
|
28340
|
-
return
|
|
28469
|
+
return join11(runsDirectory(), "editor-run.json");
|
|
28341
28470
|
}
|
|
28342
28471
|
function writeEditorRunNote(note) {
|
|
28343
|
-
|
|
28344
|
-
|
|
28472
|
+
mkdirSync5(runsDirectory(), { recursive: true });
|
|
28473
|
+
writeFileSync6(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
|
|
28345
28474
|
}
|
|
28346
28475
|
function readEditorRunNote() {
|
|
28347
28476
|
for (const directory of runtimeDirectories()) {
|
|
28348
|
-
const note = editorNoteAt(
|
|
28477
|
+
const note = editorNoteAt(join11(directory, "runs", "editor-run.json"));
|
|
28349
28478
|
if (note !== null) {
|
|
28350
28479
|
return note;
|
|
28351
28480
|
}
|
|
@@ -28355,7 +28484,7 @@ function readEditorRunNote() {
|
|
|
28355
28484
|
function editorNoteAt(path) {
|
|
28356
28485
|
let parsed;
|
|
28357
28486
|
try {
|
|
28358
|
-
parsed = JSON.parse(
|
|
28487
|
+
parsed = JSON.parse(readFileSync11(path, "utf8"));
|
|
28359
28488
|
} catch {
|
|
28360
28489
|
return null;
|
|
28361
28490
|
}
|
|
@@ -28378,7 +28507,7 @@ function recordRunEnded(pid, exitCode) {
|
|
|
28378
28507
|
return;
|
|
28379
28508
|
}
|
|
28380
28509
|
try {
|
|
28381
|
-
|
|
28510
|
+
writeFileSync6(path, JSON.stringify({ ...record, exitCode }, null, 2), "utf8");
|
|
28382
28511
|
} catch {}
|
|
28383
28512
|
}
|
|
28384
28513
|
function readRunRecord() {
|
|
@@ -28393,7 +28522,7 @@ function readRunRecord() {
|
|
|
28393
28522
|
function recordAt(path) {
|
|
28394
28523
|
let parsed;
|
|
28395
28524
|
try {
|
|
28396
|
-
parsed = JSON.parse(
|
|
28525
|
+
parsed = JSON.parse(readFileSync11(path, "utf8"));
|
|
28397
28526
|
} catch {
|
|
28398
28527
|
return null;
|
|
28399
28528
|
}
|
|
@@ -28438,7 +28567,7 @@ function runningAs(pid) {
|
|
|
28438
28567
|
const began = startedAt(pid, null);
|
|
28439
28568
|
const when = began === null ? {} : { startedAt: began };
|
|
28440
28569
|
if (process.platform === "linux") {
|
|
28441
|
-
const raw =
|
|
28570
|
+
const raw = readFileSync11(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
28442
28571
|
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
28443
28572
|
}
|
|
28444
28573
|
const args = execFileSync("ps", ["-p", String(pid), "-o", "args="], {
|
|
@@ -28560,7 +28689,7 @@ function clearRunRecord(ours = () => true) {
|
|
|
28560
28689
|
continue;
|
|
28561
28690
|
}
|
|
28562
28691
|
try {
|
|
28563
|
-
|
|
28692
|
+
rmSync5(path);
|
|
28564
28693
|
} catch {}
|
|
28565
28694
|
}
|
|
28566
28695
|
}
|
|
@@ -28584,19 +28713,19 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
28584
28713
|
if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
|
|
28585
28714
|
continue;
|
|
28586
28715
|
}
|
|
28587
|
-
const path =
|
|
28716
|
+
const path = join11(directory, entry);
|
|
28588
28717
|
if (path === keep || ageMs(path, now) < keepMs) {
|
|
28589
28718
|
continue;
|
|
28590
28719
|
}
|
|
28591
28720
|
try {
|
|
28592
|
-
|
|
28721
|
+
rmSync5(path);
|
|
28593
28722
|
} catch {}
|
|
28594
28723
|
}
|
|
28595
28724
|
}
|
|
28596
28725
|
|
|
28597
28726
|
// src/setup.ts
|
|
28598
|
-
import { cpSync, existsSync as existsSync8, mkdirSync as
|
|
28599
|
-
import { dirname as
|
|
28727
|
+
import { cpSync, existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync12, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
|
|
28728
|
+
import { dirname as dirname5, join as join12 } from "node:path";
|
|
28600
28729
|
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
28601
28730
|
var RUNTIME_AUTOLOAD = {
|
|
28602
28731
|
name: "GdharnessRuntime",
|
|
@@ -28604,11 +28733,11 @@ var RUNTIME_AUTOLOAD = {
|
|
|
28604
28733
|
};
|
|
28605
28734
|
var VERSION_MARKER = ".gdharness-version";
|
|
28606
28735
|
function installedAddonVersion(projectPath) {
|
|
28607
|
-
const marker =
|
|
28736
|
+
const marker = join12(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
28608
28737
|
if (!existsSync8(marker)) {
|
|
28609
28738
|
return null;
|
|
28610
28739
|
}
|
|
28611
|
-
const said =
|
|
28740
|
+
const said = readFileSync12(marker, "utf8").trim();
|
|
28612
28741
|
return said === "" ? null : said;
|
|
28613
28742
|
}
|
|
28614
28743
|
|
|
@@ -29274,7 +29403,7 @@ var TOOL_SPECS = [
|
|
|
29274
29403
|
},
|
|
29275
29404
|
{
|
|
29276
29405
|
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.",
|
|
29406
|
+
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
29407
|
parameters: {
|
|
29279
29408
|
severity: {
|
|
29280
29409
|
type: "string",
|
|
@@ -29302,7 +29431,7 @@ var TOOL_SPECS = [
|
|
|
29302
29431
|
},
|
|
29303
29432
|
{
|
|
29304
29433
|
name: "editor_status",
|
|
29305
|
-
description:
|
|
29434
|
+
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it.",
|
|
29306
29435
|
parameters: {},
|
|
29307
29436
|
requires: []
|
|
29308
29437
|
},
|
|
@@ -29627,7 +29756,7 @@ var TOOL_SPECS = [
|
|
|
29627
29756
|
},
|
|
29628
29757
|
{
|
|
29629
29758
|
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.",
|
|
29759
|
+
description: "Sets or removes a breakpoint through the editor's debug adapter. Needs the editor, not a running game: set them first, then editor_run, and the game stops where you asked. The editor keeps a breakpoint set this way for one play, so every breakpoint this server holds is sent again before each play editor_run starts, and the start answer lists them under breakpoints. The set is also kept in the project, for the server that comes after a reconnect: a breakpoint stays until it is removed here, whichever server set it. Every answer lists the whole set under held.",
|
|
29631
29760
|
parameters: {
|
|
29632
29761
|
projectPath: PROJECT_PATH,
|
|
29633
29762
|
scriptPath: SCRIPT_PATH,
|
|
@@ -29652,7 +29781,7 @@ var TOOL_SPECS = [
|
|
|
29652
29781
|
},
|
|
29653
29782
|
{
|
|
29654
29783
|
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.",
|
|
29784
|
+
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
29785
|
parameters: {
|
|
29657
29786
|
frameId: {
|
|
29658
29787
|
type: "number",
|
|
@@ -29719,12 +29848,30 @@ function buildToolDefinitions() {
|
|
|
29719
29848
|
var UPDATE_NOTICE_EVERY = 500;
|
|
29720
29849
|
var FEEDBACK_NOTICE_EVERY = 250;
|
|
29721
29850
|
var run4 = promisify4(execFile4);
|
|
29722
|
-
var __dirname2 =
|
|
29851
|
+
var __dirname2 = dirname6(fileURLToPath2(import.meta.url));
|
|
29723
29852
|
var EDITOR_RESTART_TIMEOUT_MS = 90000;
|
|
29853
|
+
var HOLD_PING_MS = 2000;
|
|
29724
29854
|
var SLOWEST_FRAME_RATE = 20;
|
|
29725
29855
|
function patienceForFrames(frames, atLeast) {
|
|
29726
29856
|
return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
|
|
29727
29857
|
}
|
|
29858
|
+
function projectSpelling(projectPath, absolutePath) {
|
|
29859
|
+
return `res://${relative3(realPathOr(projectPath), realPathOr(absolutePath)).replaceAll("\\", "/")}`;
|
|
29860
|
+
}
|
|
29861
|
+
function describeHalt(halt) {
|
|
29862
|
+
switch (halt.reason) {
|
|
29863
|
+
case "breakpoint":
|
|
29864
|
+
return "at a breakpoint";
|
|
29865
|
+
case "step":
|
|
29866
|
+
return "after a step";
|
|
29867
|
+
case "exception":
|
|
29868
|
+
return halt.text === "" ? "on an error" : `on an error: ${halt.text}`;
|
|
29869
|
+
case "attached":
|
|
29870
|
+
return "held when this session attached";
|
|
29871
|
+
default:
|
|
29872
|
+
return `stopped with reason "${halt.reason}"`;
|
|
29873
|
+
}
|
|
29874
|
+
}
|
|
29728
29875
|
function stillRunning(run) {
|
|
29729
29876
|
if (run?.exitCode !== null || run.endedUnwatched === true) {
|
|
29730
29877
|
return false;
|
|
@@ -29915,7 +30062,7 @@ function aboveTheRunner(entry) {
|
|
|
29915
30062
|
}
|
|
29916
30063
|
function elsewhereIn(projectPath, asked) {
|
|
29917
30064
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
29918
|
-
if (wanted === "" || existsSync9(
|
|
30065
|
+
if (wanted === "" || existsSync9(join13(projectPath, wanted))) {
|
|
29919
30066
|
return "";
|
|
29920
30067
|
}
|
|
29921
30068
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
@@ -29929,11 +30076,11 @@ function elsewhereIn(projectPath, asked) {
|
|
|
29929
30076
|
return near.length === 0 ? "" : ` The project does have ${there}.`;
|
|
29930
30077
|
}
|
|
29931
30078
|
function hasMainScene(projectFile) {
|
|
29932
|
-
const scene = parseProjectGodot(
|
|
30079
|
+
const scene = parseProjectGodot(readFileSync13(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
29933
30080
|
return typeof scene === "string" && scene !== "";
|
|
29934
30081
|
}
|
|
29935
30082
|
function editorPlaysHeadless(projectFile) {
|
|
29936
|
-
const runArgs = parseProjectGodot(
|
|
30083
|
+
const runArgs = parseProjectGodot(readFileSync13(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
29937
30084
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
29938
30085
|
}
|
|
29939
30086
|
function camelCased(params) {
|
|
@@ -29949,7 +30096,7 @@ function camelCased(params) {
|
|
|
29949
30096
|
class GodotServer {
|
|
29950
30097
|
mcp;
|
|
29951
30098
|
locator = new GodotLocator;
|
|
29952
|
-
operationsScript =
|
|
30099
|
+
operationsScript = join13(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
29953
30100
|
godotBridge;
|
|
29954
30101
|
tools = buildToolDefinitions();
|
|
29955
30102
|
activeProcess = null;
|
|
@@ -29961,6 +30108,7 @@ class GodotServer {
|
|
|
29961
30108
|
lastGodotVersion = null;
|
|
29962
30109
|
lspClient = null;
|
|
29963
30110
|
dapClient = null;
|
|
30111
|
+
breakpointsSeededOn = null;
|
|
29964
30112
|
bridgeStartupError = null;
|
|
29965
30113
|
bridgeRetry = null;
|
|
29966
30114
|
successorWatch = null;
|
|
@@ -30559,17 +30707,28 @@ class GodotServer {
|
|
|
30559
30707
|
if (!located.ok) {
|
|
30560
30708
|
return this.createErrorResponse(located.reason, PATH_SOLUTIONS);
|
|
30561
30709
|
}
|
|
30562
|
-
|
|
30710
|
+
const session = this.dapHoldingBreakpointsOf(project.value.path);
|
|
30711
|
+
const answered = await this.handleDAP(op === "set" ? "dap_set_breakpoint" : "dap_remove_breakpoint", {
|
|
30563
30712
|
...args,
|
|
30564
30713
|
scriptPath: realPathOr(located.absolutePath)
|
|
30565
30714
|
});
|
|
30715
|
+
if (answered.isError === true) {
|
|
30716
|
+
return answered;
|
|
30717
|
+
}
|
|
30718
|
+
const held = session.breakpointsHeld();
|
|
30719
|
+
writeBreakpointNote(project.value.path, held);
|
|
30720
|
+
return this.jsonTextResponse({
|
|
30721
|
+
...asParams(JSON.parse(answered.content[0]?.text ?? "{}")),
|
|
30722
|
+
held: this.breakpointsAsProjectSpells(project.value.path, held),
|
|
30723
|
+
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."
|
|
30724
|
+
});
|
|
30566
30725
|
}
|
|
30567
30726
|
case "debug_control":
|
|
30568
30727
|
case "debug_state": {
|
|
30569
30728
|
if (tool === "debug_state" && op === "output") {
|
|
30570
30729
|
return await this.handleDAP("dap_get_output", args);
|
|
30571
30730
|
}
|
|
30572
|
-
const held = await this.debuggedGame();
|
|
30731
|
+
const held = await this.debuggedGame(tool === "debug_control" ? "move" : "read");
|
|
30573
30732
|
if (!held.ok) {
|
|
30574
30733
|
return held.response;
|
|
30575
30734
|
}
|
|
@@ -30592,7 +30751,7 @@ class GodotServer {
|
|
|
30592
30751
|
if (path === undefined) {
|
|
30593
30752
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
30594
30753
|
}
|
|
30595
|
-
const file =
|
|
30754
|
+
const file = join13(path, "project.godot");
|
|
30596
30755
|
if (!existsSync9(file)) {
|
|
30597
30756
|
return {
|
|
30598
30757
|
ok: false,
|
|
@@ -30618,7 +30777,7 @@ class GodotServer {
|
|
|
30618
30777
|
}
|
|
30619
30778
|
return { ok: true, value: given };
|
|
30620
30779
|
}
|
|
30621
|
-
async debuggedGame() {
|
|
30780
|
+
async debuggedGame(intent) {
|
|
30622
30781
|
const game = this.activeProcess;
|
|
30623
30782
|
if (game !== null && game.exitCode !== null) {
|
|
30624
30783
|
return {
|
|
@@ -30656,6 +30815,19 @@ class GodotServer {
|
|
|
30656
30815
|
};
|
|
30657
30816
|
}
|
|
30658
30817
|
if (!this.dap().isStopped()) {
|
|
30818
|
+
const hold = await this.holdOf(game);
|
|
30819
|
+
if (hold.heldAt !== null) {
|
|
30820
|
+
if (intent === "move") {
|
|
30821
|
+
return { ok: true, value: game };
|
|
30822
|
+
}
|
|
30823
|
+
return {
|
|
30824
|
+
ok: false,
|
|
30825
|
+
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}.`, [
|
|
30826
|
+
"debug_control continue lets a held game go, and the next stop is one this session is told about",
|
|
30827
|
+
"editor_output says what it printed on the way to where it is"
|
|
30828
|
+
])
|
|
30829
|
+
};
|
|
30830
|
+
}
|
|
30659
30831
|
return {
|
|
30660
30832
|
ok: false,
|
|
30661
30833
|
response: this.createErrorResponse("The game is running, not stopped, so it has no stack and no scope to read.", [
|
|
@@ -30793,7 +30965,7 @@ class GodotServer {
|
|
|
30793
30965
|
if (!engine.ok) {
|
|
30794
30966
|
return engine.response;
|
|
30795
30967
|
}
|
|
30796
|
-
const application = parseProjectGodot(
|
|
30968
|
+
const application = parseProjectGodot(readFileSync13(project.value.file, "utf8"))["application"] ?? {};
|
|
30797
30969
|
const name = application["config/name"];
|
|
30798
30970
|
const mainScene = application["run/main_scene"];
|
|
30799
30971
|
const info = {
|
|
@@ -30854,18 +31026,18 @@ class GodotServer {
|
|
|
30854
31026
|
if (!engine.ok) {
|
|
30855
31027
|
return engine.response;
|
|
30856
31028
|
}
|
|
30857
|
-
const folder =
|
|
31029
|
+
const folder = dirname6(output.absolutePath);
|
|
30858
31030
|
try {
|
|
30859
|
-
|
|
31031
|
+
mkdirSync7(folder, { recursive: true });
|
|
30860
31032
|
} catch (error) {
|
|
30861
|
-
return this.createErrorResponse(`Cannot create ${
|
|
31033
|
+
return this.createErrorResponse(`Cannot create ${dirname6(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
30862
31034
|
}
|
|
30863
31035
|
const debug = readBoolean(args, "debug") ?? false;
|
|
30864
|
-
const exportLogs = mkdtempSync2(
|
|
31036
|
+
const exportLogs = mkdtempSync2(join13(tmpdir4(), "gdharness-export-"));
|
|
30865
31037
|
const exportArgs = [
|
|
30866
31038
|
"--headless",
|
|
30867
31039
|
"--log-file",
|
|
30868
|
-
|
|
31040
|
+
join13(exportLogs, "engine.log"),
|
|
30869
31041
|
"--path",
|
|
30870
31042
|
project.value.path,
|
|
30871
31043
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -30923,7 +31095,7 @@ class GodotServer {
|
|
|
30923
31095
|
return contained.response;
|
|
30924
31096
|
}
|
|
30925
31097
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
30926
|
-
if (!existsSync9(
|
|
31098
|
+
if (!existsSync9(join13(project.value.path, runner))) {
|
|
30927
31099
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
30928
31100
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
30929
31101
|
]);
|
|
@@ -30958,7 +31130,7 @@ class GodotServer {
|
|
|
30958
31130
|
];
|
|
30959
31131
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
30960
31132
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
30961
|
-
const userData = mkdtempSync2(
|
|
31133
|
+
const userData = mkdtempSync2(join13(tmpdir4(), "gdharness-tests-"));
|
|
30962
31134
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
30963
31135
|
const hung = await new Promise((resolve) => {
|
|
30964
31136
|
const timer = setTimeout(() => {
|
|
@@ -30974,14 +31146,14 @@ class GodotServer {
|
|
|
30974
31146
|
resolve(false);
|
|
30975
31147
|
});
|
|
30976
31148
|
});
|
|
30977
|
-
const reportsDir =
|
|
31149
|
+
const reportsDir = join13(project.value.path, ".godot", ...ours.split("/"));
|
|
30978
31150
|
let report = null;
|
|
30979
31151
|
let reportProblem = null;
|
|
30980
31152
|
try {
|
|
30981
31153
|
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
31154
|
const newest = written.at(-1);
|
|
30983
31155
|
if (newest !== undefined) {
|
|
30984
|
-
report = parseJUnit(
|
|
31156
|
+
report = parseJUnit(readFileSync13(join13(reportsDir, newest, "results.xml"), "utf8"));
|
|
30985
31157
|
}
|
|
30986
31158
|
} catch (error) {
|
|
30987
31159
|
reportProblem = errorMessage(error);
|
|
@@ -31121,7 +31293,7 @@ class GodotServer {
|
|
|
31121
31293
|
return null;
|
|
31122
31294
|
}
|
|
31123
31295
|
try {
|
|
31124
|
-
return
|
|
31296
|
+
return readFileSync13(contained.absolutePath, "utf8");
|
|
31125
31297
|
} catch {
|
|
31126
31298
|
return null;
|
|
31127
31299
|
}
|
|
@@ -31163,7 +31335,7 @@ class GodotServer {
|
|
|
31163
31335
|
return null;
|
|
31164
31336
|
}
|
|
31165
31337
|
try {
|
|
31166
|
-
return
|
|
31338
|
+
return readFileSync13(contained.absolutePath, "utf8");
|
|
31167
31339
|
} catch {
|
|
31168
31340
|
return null;
|
|
31169
31341
|
}
|
|
@@ -31212,6 +31384,20 @@ class GodotServer {
|
|
|
31212
31384
|
}
|
|
31213
31385
|
return this.dapClient;
|
|
31214
31386
|
}
|
|
31387
|
+
dapHoldingBreakpointsOf(projectPath) {
|
|
31388
|
+
const client = this.dap();
|
|
31389
|
+
if (this.breakpointsSeededOn !== client) {
|
|
31390
|
+
client.holdBreakpoints(readBreakpointNote(projectPath));
|
|
31391
|
+
this.breakpointsSeededOn = client;
|
|
31392
|
+
}
|
|
31393
|
+
return client;
|
|
31394
|
+
}
|
|
31395
|
+
breakpointsAsProjectSpells(projectPath, held) {
|
|
31396
|
+
return held.map((one) => ({
|
|
31397
|
+
scriptPath: projectSpelling(projectPath, one.scriptPath),
|
|
31398
|
+
lines: one.lines
|
|
31399
|
+
}));
|
|
31400
|
+
}
|
|
31215
31401
|
writeToTranscript(line) {
|
|
31216
31402
|
const run = this.activeProcess;
|
|
31217
31403
|
if (run?.throughEditor !== true || run.transcript === null) {
|
|
@@ -31301,8 +31487,12 @@ class GodotServer {
|
|
|
31301
31487
|
async handleEditorStatus() {
|
|
31302
31488
|
const located = await this.locator.find();
|
|
31303
31489
|
const announced = runtimesAnnounced();
|
|
31304
|
-
const
|
|
31305
|
-
const
|
|
31490
|
+
const pinged = Promise.all(announced.running.map(async (endpoint) => {
|
|
31491
|
+
const held = this.knownHoldOn(endpoint);
|
|
31492
|
+
const reply = held === null ? await runtimeRequest(endpoint, "ping", {}, HOLD_PING_MS) : {
|
|
31493
|
+
ok: false,
|
|
31494
|
+
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.`
|
|
31495
|
+
};
|
|
31306
31496
|
return {
|
|
31307
31497
|
pid: endpoint.pid,
|
|
31308
31498
|
port: endpoint.port,
|
|
@@ -31311,8 +31501,13 @@ class GodotServer {
|
|
|
31311
31501
|
problem: reply.ok ? null : reply.message
|
|
31312
31502
|
};
|
|
31313
31503
|
}));
|
|
31314
|
-
const
|
|
31315
|
-
|
|
31504
|
+
const asked = (async () => {
|
|
31505
|
+
const playing = await this.editorPlayingState();
|
|
31506
|
+
await this.pickUpWhatTheEditorIsPlaying();
|
|
31507
|
+
const current = this.currentRun();
|
|
31508
|
+
return { playing, hold: current === null ? {} : await this.holdOf(current) };
|
|
31509
|
+
})();
|
|
31510
|
+
const [games, { playing, hold }] = await Promise.all([pinged, asked]);
|
|
31316
31511
|
return this.jsonTextResponse({
|
|
31317
31512
|
editor: {
|
|
31318
31513
|
...this.getEditorStatusPayload(),
|
|
@@ -31326,6 +31521,7 @@ class GodotServer {
|
|
|
31326
31521
|
game: {
|
|
31327
31522
|
processActive: runIsUp(this.currentRun(), playing === null ? null : playing.playing),
|
|
31328
31523
|
playingInEditor: playing,
|
|
31524
|
+
...hold,
|
|
31329
31525
|
runtimeConnected: games.some((game) => game.reachable),
|
|
31330
31526
|
runtimes: games,
|
|
31331
31527
|
unreadable: announced.unspoken.map((game) => ({
|
|
@@ -31343,7 +31539,7 @@ class GodotServer {
|
|
|
31343
31539
|
return null;
|
|
31344
31540
|
}
|
|
31345
31541
|
try {
|
|
31346
|
-
return settingKeys(
|
|
31542
|
+
return settingKeys(readFileSync13(join13(projectPath, "project.godot"), "utf8"));
|
|
31347
31543
|
} catch {
|
|
31348
31544
|
return null;
|
|
31349
31545
|
}
|
|
@@ -31586,7 +31782,7 @@ class GodotServer {
|
|
|
31586
31782
|
return runIsUp(run, editorSays);
|
|
31587
31783
|
}
|
|
31588
31784
|
async runtimeUp(projectPath, before, budgetMs, withArgs) {
|
|
31589
|
-
if (!existsSync9(
|
|
31785
|
+
if (!existsSync9(join13(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
31590
31786
|
return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
|
|
31591
31787
|
}
|
|
31592
31788
|
const endpoint = await announcedSince(projectPath, before, {
|
|
@@ -31624,6 +31820,7 @@ class GodotServer {
|
|
|
31624
31820
|
"Closing the other editor, or moving its debug adapter port in its editor settings, frees this one"
|
|
31625
31821
|
]);
|
|
31626
31822
|
}
|
|
31823
|
+
const breakpoints = await this.dapHoldingBreakpointsOf(projectPath).reapplyBreakpoints();
|
|
31627
31824
|
const answer = await this.handleViaBridge("play_scene", scene === null ? {} : { scenePath: `res://${scene}` });
|
|
31628
31825
|
if (answer.isError === true) {
|
|
31629
31826
|
return answer;
|
|
@@ -31653,6 +31850,11 @@ class GodotServer {
|
|
|
31653
31850
|
refreshedClasses,
|
|
31654
31851
|
debugPort: readNumber(asParams(JSON.parse(answer.content[0]?.text ?? "{}")), "debugPort"),
|
|
31655
31852
|
endedPreviousRun: ended ?? undefined,
|
|
31853
|
+
breakpoints: breakpoints.applied.length === 0 ? undefined : this.breakpointsAsProjectSpells(projectPath, breakpoints.applied),
|
|
31854
|
+
breakpointsRefused: breakpoints.refused.length === 0 ? undefined : breakpoints.refused.map((one) => ({
|
|
31855
|
+
scriptPath: projectSpelling(projectPath, one.scriptPath),
|
|
31856
|
+
reason: one.reason
|
|
31857
|
+
})),
|
|
31656
31858
|
runtime: await this.runtimeUp(projectPath, alreadyPlaying, runtimeWaitMs, false),
|
|
31657
31859
|
message: "The editor is playing it, so its debugger holds it: the debug_* tools can reach it, " + "editor_output reads its console through the debug adapter, and editor_run stop ends it." + (ended === null ? "" : ` The run that was going, pid ${ended}, was ended to start this one; its output is no longer what editor_output answers about.`)
|
|
31658
31860
|
});
|
|
@@ -31850,7 +32052,7 @@ class GodotServer {
|
|
|
31850
32052
|
return;
|
|
31851
32053
|
}
|
|
31852
32054
|
try {
|
|
31853
|
-
await this.dap().
|
|
32055
|
+
await this.dap().attach();
|
|
31854
32056
|
} catch (error) {
|
|
31855
32057
|
this.logDebug(`Picked up an editor-played run without its adapter: ${errorMessage(error)}`);
|
|
31856
32058
|
}
|
|
@@ -31870,8 +32072,16 @@ class GodotServer {
|
|
|
31870
32072
|
brokeOn: null,
|
|
31871
32073
|
pickedUpPlaying: true
|
|
31872
32074
|
};
|
|
32075
|
+
const announced = this.theOneAnnouncedForOurProject();
|
|
32076
|
+
if (announced !== undefined) {
|
|
32077
|
+
picked.announcedPid = announced.pid;
|
|
32078
|
+
}
|
|
31873
32079
|
this.activeProcess = picked;
|
|
31874
32080
|
}
|
|
32081
|
+
theOneAnnouncedForOurProject() {
|
|
32082
|
+
const ofProject = this.allAnnouncedForOurProject(runtimesAnnounced().running);
|
|
32083
|
+
return ofProject.length === 1 ? ofProject[0] : undefined;
|
|
32084
|
+
}
|
|
31875
32085
|
adoptRecordedRun() {
|
|
31876
32086
|
const record = readRunRecord();
|
|
31877
32087
|
if (record === null || !this.couldBeOurs(record.projectPath)) {
|
|
@@ -32026,7 +32236,7 @@ class GodotServer {
|
|
|
32026
32236
|
contains: readNonEmptyString(args, "contains"),
|
|
32027
32237
|
limit: readPositiveNumber(args, "limit") ?? 200
|
|
32028
32238
|
});
|
|
32029
|
-
const
|
|
32239
|
+
const hold = await this.holdOf(run);
|
|
32030
32240
|
const wanted = readBoolean(args, "cpu") ?? false;
|
|
32031
32241
|
const cpuSeconds = wanted && run.pid !== null && stillRunning(run) ? await cpuSecondsOf(run.pid) : undefined;
|
|
32032
32242
|
const notes = [];
|
|
@@ -32063,7 +32273,7 @@ class GodotServer {
|
|
|
32063
32273
|
warnings: run.log.count("warning"),
|
|
32064
32274
|
clean: run.consoleLost === true ? undefined : run.log.count("error") === 0,
|
|
32065
32275
|
consoleLost: run.consoleLost === true ? true : undefined,
|
|
32066
|
-
|
|
32276
|
+
...hold,
|
|
32067
32277
|
endedUnwatched: run.endedUnwatched === true ? true : undefined,
|
|
32068
32278
|
endedBy: stillRunning(run) ? undefined : run.endedHere ?? null,
|
|
32069
32279
|
project: !run.throughEditor && run.process === null ? run.projectPath ?? undefined : undefined,
|
|
@@ -32246,6 +32456,50 @@ class GodotServer {
|
|
|
32246
32456
|
const override = Number.parseInt(envValue("GDHARNESS_RUNTIME_TIMEOUT_MS") ?? "", 10);
|
|
32247
32457
|
return Number.isInteger(override) && override > 0 ? override : 1e4;
|
|
32248
32458
|
}
|
|
32459
|
+
knownHoldOn(endpoint) {
|
|
32460
|
+
const run = this.activeProcess;
|
|
32461
|
+
const session = this.dapClient;
|
|
32462
|
+
if (run === null || !run.throughEditor || !stillRunning(run) || session === null) {
|
|
32463
|
+
return null;
|
|
32464
|
+
}
|
|
32465
|
+
const its = run.announcedPid ?? this.theOneAnnouncedForOurProject()?.pid;
|
|
32466
|
+
if (its !== endpoint.pid || !session.holdIsKnown()) {
|
|
32467
|
+
return null;
|
|
32468
|
+
}
|
|
32469
|
+
return session.whereItStopped();
|
|
32470
|
+
}
|
|
32471
|
+
async holdOf(run) {
|
|
32472
|
+
if (!run.throughEditor || !stillRunning(run)) {
|
|
32473
|
+
return { heldAt: null };
|
|
32474
|
+
}
|
|
32475
|
+
const session = this.dapClient;
|
|
32476
|
+
if (session?.holdIsKnown()) {
|
|
32477
|
+
return { heldAt: session.whereItStopped() };
|
|
32478
|
+
}
|
|
32479
|
+
const endpoint = run.announcedPid !== undefined ? runtimesAnnounced().running.find((one) => one.pid === run.announcedPid) : this.theOneAnnouncedForOurProject();
|
|
32480
|
+
if (endpoint === undefined) {
|
|
32481
|
+
return {
|
|
32482
|
+
heldAt: undefined,
|
|
32483
|
+
heldUnknown: true,
|
|
32484
|
+
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."
|
|
32485
|
+
};
|
|
32486
|
+
}
|
|
32487
|
+
const reply = await runtimeRequest(endpoint, "ping", {}, HOLD_PING_MS);
|
|
32488
|
+
if (reply.ok) {
|
|
32489
|
+
session?.learnedRunning();
|
|
32490
|
+
return { heldAt: session?.whereItStopped() ?? null };
|
|
32491
|
+
}
|
|
32492
|
+
if (reply.reason === "busy") {
|
|
32493
|
+
return {
|
|
32494
|
+
heldAt: {
|
|
32495
|
+
reason: "unanswered",
|
|
32496
|
+
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`,
|
|
32497
|
+
text: ""
|
|
32498
|
+
}
|
|
32499
|
+
};
|
|
32500
|
+
}
|
|
32501
|
+
return { heldAt: undefined, heldUnknown: true, heldNote: reply.message };
|
|
32502
|
+
}
|
|
32249
32503
|
async handleRuntimeCommand(command, args, timeoutMs = this.runtimeTimeoutMs()) {
|
|
32250
32504
|
const { op: _op, projectPath, ...params } = asParams(args);
|
|
32251
32505
|
const announced = runtimesAnnounced();
|
|
@@ -32253,9 +32507,17 @@ class GodotServer {
|
|
|
32253
32507
|
if ("problem" in choice) {
|
|
32254
32508
|
return this.createErrorResponse(choice.problem);
|
|
32255
32509
|
}
|
|
32510
|
+
const held = this.knownHoldOn(choice.endpoint);
|
|
32511
|
+
if (held !== null) {
|
|
32512
|
+
return this.createErrorResponse(`The game (pid ${choice.endpoint.pid}) is held by the editor's debugger, ${describeHalt(held)}, and answers no runtime call while it is.`, [
|
|
32513
|
+
"debug_control continue lets it go, after which this call answers",
|
|
32514
|
+
"debug_state stack reads where it is held, and debug_state scopes what it is holding there",
|
|
32515
|
+
"editor_output says heldAt for the run, and editor_run stop ends it"
|
|
32516
|
+
]);
|
|
32517
|
+
}
|
|
32256
32518
|
const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
|
|
32257
|
-
const screenshotDir = expectsScreenshot ? mkdtempSync2(
|
|
32258
|
-
const screenshotPath = screenshotDir ?
|
|
32519
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync2(join13(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
|
|
32520
|
+
const screenshotPath = screenshotDir ? join13(screenshotDir, "capture.png") : null;
|
|
32259
32521
|
try {
|
|
32260
32522
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
|
|
32261
32523
|
if (!reply.ok) {
|
|
@@ -32276,7 +32538,7 @@ class GodotServer {
|
|
|
32276
32538
|
return {
|
|
32277
32539
|
content: [
|
|
32278
32540
|
{ type: "text", text: `Screenshot captured: ${dimensions}` },
|
|
32279
|
-
{ type: "image", data:
|
|
32541
|
+
{ type: "image", data: readFileSync13(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
32280
32542
|
]
|
|
32281
32543
|
};
|
|
32282
32544
|
} finally {
|