gdharness 0.10.1 → 0.11.0
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 +438 -91
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +56 -6
- package/build/index.js +428 -87
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -155,6 +155,9 @@ function runArguments(options) {
|
|
|
155
155
|
if (options.scene !== null) {
|
|
156
156
|
args.push(`res://${options.scene}`);
|
|
157
157
|
}
|
|
158
|
+
if (options.userArgs !== undefined && options.userArgs.length > 0) {
|
|
159
|
+
args.push("--", ...options.userArgs);
|
|
160
|
+
}
|
|
158
161
|
return args;
|
|
159
162
|
}
|
|
160
163
|
function editorArguments(projectPath, ports) {
|
|
@@ -7187,8 +7190,12 @@ function announced(line) {
|
|
|
7187
7190
|
|
|
7188
7191
|
class GameLog {
|
|
7189
7192
|
entries = [];
|
|
7190
|
-
decoders = {
|
|
7191
|
-
|
|
7193
|
+
decoders = {
|
|
7194
|
+
stdout: new StringDecoder("utf8"),
|
|
7195
|
+
stderr: new StringDecoder("utf8"),
|
|
7196
|
+
transcript: new StringDecoder("utf8")
|
|
7197
|
+
};
|
|
7198
|
+
partial = { stdout: "", stderr: "", transcript: "" };
|
|
7192
7199
|
lastHeadline = null;
|
|
7193
7200
|
cursor = 0;
|
|
7194
7201
|
append(source, chunk) {
|
|
@@ -7201,7 +7208,7 @@ class GameLog {
|
|
|
7201
7208
|
}
|
|
7202
7209
|
}
|
|
7203
7210
|
finish() {
|
|
7204
|
-
for (const source of
|
|
7211
|
+
for (const source of STREAMS) {
|
|
7205
7212
|
const tail = this.partial[source] + this.decoders[source].end();
|
|
7206
7213
|
this.partial[source] = "";
|
|
7207
7214
|
if (tail !== "") {
|
|
@@ -7264,10 +7271,11 @@ class GameLog {
|
|
|
7264
7271
|
return { entries: matching.slice(omitted), omitted };
|
|
7265
7272
|
}
|
|
7266
7273
|
}
|
|
7267
|
-
var HEADLINE, COLOUR_CODE;
|
|
7274
|
+
var HEADLINE, COLOUR_CODE, STREAMS;
|
|
7268
7275
|
var init_game_log = __esm(() => {
|
|
7269
7276
|
HEADLINE = /^(USER )?(SCRIPT ERROR|ERROR|WARNING):\s?(.*)$/;
|
|
7270
7277
|
COLOUR_CODE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
7278
|
+
STREAMS = ["stdout", "stderr", "transcript"];
|
|
7271
7279
|
});
|
|
7272
7280
|
|
|
7273
7281
|
// src/update-check.ts
|
|
@@ -15405,7 +15413,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15405
15413
|
},
|
|
15406
15414
|
{
|
|
15407
15415
|
name: "project_test",
|
|
15408
|
-
description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines. The class list is rebuilt first, so a suite written a moment ago is found. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. Needs gdUnit4 under addons/gdUnit4.",
|
|
15416
|
+
description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines. The class list is rebuilt first, so a suite written a moment ago is found. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. Needs gdUnit4 under addons/gdUnit4.",
|
|
15409
15417
|
parameters: {
|
|
15410
15418
|
projectPath: PROJECT_PATH,
|
|
15411
15419
|
path: {
|
|
@@ -15680,10 +15688,15 @@ var init_tool_definitions = __esm(() => {
|
|
|
15680
15688
|
},
|
|
15681
15689
|
{
|
|
15682
15690
|
name: "editor_run",
|
|
15683
|
-
description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice.",
|
|
15691
|
+
description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. args hands the game its own flags, the ones it reads with OS.get_cmdline_user_args(), and a run carrying any is started by this server rather than by the editor. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice.",
|
|
15684
15692
|
parameters: {
|
|
15685
15693
|
projectPath: PROJECT_PATH,
|
|
15686
15694
|
scene: { type: "string", description: "A scene to run instead of the main scene." },
|
|
15695
|
+
args: {
|
|
15696
|
+
type: "array",
|
|
15697
|
+
items: { type: "string" },
|
|
15698
|
+
description: `The game's own arguments, what OS.get_cmdline_user_args() answers, such as ["--level=2"]. The separator is added here. A run with any is started by this server rather than by the editor, which fixes the game's command line when it opens the project, so the debug_* tools do not answer for it.`
|
|
15699
|
+
},
|
|
15687
15700
|
headless: { type: "boolean", description: "start: force a window or no window." },
|
|
15688
15701
|
frames: { type: "number", description: "check: frames to run before quitting. Default 3." },
|
|
15689
15702
|
timeoutMs: {
|
|
@@ -15812,8 +15825,8 @@ var init_tool_definitions = __esm(() => {
|
|
|
15812
15825
|
},
|
|
15813
15826
|
includeHidden: {
|
|
15814
15827
|
type: "boolean",
|
|
15815
|
-
ops: ["text"],
|
|
15816
|
-
description: "text: read hidden nodes as well, for checking that something is not showing. Default false."
|
|
15828
|
+
ops: ["text", "find"],
|
|
15829
|
+
description: "text: read hidden nodes as well, for checking that something is not showing. Default false. find: default true, since a find means the node whether or not it is drawn; false answers with what the player can actually see, which is how a panel that keeps a label for every line and hides the ones that do not is read. A node counts as hidden when anything above it is, and how many matches were left out comes back under hidden."
|
|
15817
15830
|
},
|
|
15818
15831
|
metrics: {
|
|
15819
15832
|
type: "array",
|
|
@@ -15830,7 +15843,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15830
15843
|
requires: []
|
|
15831
15844
|
},
|
|
15832
15845
|
find: {
|
|
15833
|
-
summary: "the paths of every node matching className, script, namePattern, group or says, with property read off each",
|
|
15846
|
+
summary: "the paths of every node matching className, script, namePattern, group or says, with property read off each, hidden ones included unless includeHidden says otherwise",
|
|
15834
15847
|
requires: []
|
|
15835
15848
|
},
|
|
15836
15849
|
rect: {
|
|
@@ -35160,7 +35173,37 @@ function whyNoReport(printed, asked) {
|
|
|
35160
35173
|
}
|
|
35161
35174
|
return printed.some((line) => NOTHING_RAN.test(line)) ? `no test cases found at ${asked}` : null;
|
|
35162
35175
|
}
|
|
35163
|
-
|
|
35176
|
+
function orphansPrinted(printed) {
|
|
35177
|
+
const suites = [];
|
|
35178
|
+
let total = null;
|
|
35179
|
+
let running = null;
|
|
35180
|
+
for (const line of printed) {
|
|
35181
|
+
const started = RUNNING_SUITE.exec(line);
|
|
35182
|
+
if (started?.[1] !== undefined) {
|
|
35183
|
+
running = started[1];
|
|
35184
|
+
continue;
|
|
35185
|
+
}
|
|
35186
|
+
const counted = ORPHANS.exec(line);
|
|
35187
|
+
if (counted?.[1] === undefined) {
|
|
35188
|
+
continue;
|
|
35189
|
+
}
|
|
35190
|
+
const orphans = Number(counted[1]);
|
|
35191
|
+
if (line.includes(OVERALL)) {
|
|
35192
|
+
total = orphans;
|
|
35193
|
+
running = null;
|
|
35194
|
+
continue;
|
|
35195
|
+
}
|
|
35196
|
+
if (running !== null && orphans > 0) {
|
|
35197
|
+
suites.push({ path: running, orphans });
|
|
35198
|
+
}
|
|
35199
|
+
running = null;
|
|
35200
|
+
}
|
|
35201
|
+
return {
|
|
35202
|
+
total: total ?? suites.reduce((sum, suite) => sum + suite.orphans, 0),
|
|
35203
|
+
suites
|
|
35204
|
+
};
|
|
35205
|
+
}
|
|
35206
|
+
var MalformedReportError, ENTITIES, ATTRIBUTE, NOTHING_RAN, NO_SUCH_PATH, RUNNING_SUITE, ORPHANS, OVERALL = "Overall Summary";
|
|
35164
35207
|
var init_junit = __esm(() => {
|
|
35165
35208
|
MalformedReportError = class MalformedReportError extends Error {
|
|
35166
35209
|
constructor(message) {
|
|
@@ -35172,6 +35215,8 @@ var init_junit = __esm(() => {
|
|
|
35172
35215
|
ATTRIBUTE = /\s*([^\s=/>]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/y;
|
|
35173
35216
|
NOTHING_RAN = /no test cases found/i;
|
|
35174
35217
|
NO_SUCH_PATH = /given directory or file does not exists:\s*(\S+)/i;
|
|
35218
|
+
RUNNING_SUITE = /Run Test Suite:\s*(\S+)/;
|
|
35219
|
+
ORPHANS = /(\d+)\s+orphans/;
|
|
35175
35220
|
});
|
|
35176
35221
|
|
|
35177
35222
|
// src/lsp_client.ts
|
|
@@ -35475,6 +35520,14 @@ class GodotLSPClient {
|
|
|
35475
35520
|
}
|
|
35476
35521
|
return uri;
|
|
35477
35522
|
}
|
|
35523
|
+
closeDocument(uri) {
|
|
35524
|
+
if (!this.documentVersions.delete(uri)) {
|
|
35525
|
+
return;
|
|
35526
|
+
}
|
|
35527
|
+
try {
|
|
35528
|
+
this.sendNotification("textDocument/didClose", { textDocument: { uri } });
|
|
35529
|
+
} catch {}
|
|
35530
|
+
}
|
|
35478
35531
|
async initialize(rootPath) {
|
|
35479
35532
|
await this.ensureConnected();
|
|
35480
35533
|
const resolvedRootPath = resolve3(rootPath);
|
|
@@ -35528,8 +35581,9 @@ class GodotLSPClient {
|
|
|
35528
35581
|
timer
|
|
35529
35582
|
});
|
|
35530
35583
|
});
|
|
35584
|
+
let opened = null;
|
|
35531
35585
|
try {
|
|
35532
|
-
this.syncDocument(filePath, content);
|
|
35586
|
+
opened = this.syncDocument(filePath, content);
|
|
35533
35587
|
return await diagnosticsPromise;
|
|
35534
35588
|
} catch (error) {
|
|
35535
35589
|
const waiter = this.diagnosticsWaiters.get(key);
|
|
@@ -35538,16 +35592,25 @@ class GodotLSPClient {
|
|
|
35538
35592
|
this.diagnosticsWaiters.delete(key);
|
|
35539
35593
|
}
|
|
35540
35594
|
throw error;
|
|
35595
|
+
} finally {
|
|
35596
|
+
if (opened !== null) {
|
|
35597
|
+
this.closeDocument(opened);
|
|
35598
|
+
}
|
|
35541
35599
|
}
|
|
35542
35600
|
}
|
|
35543
35601
|
async getCompletions(filePath, content, line, character) {
|
|
35544
35602
|
await this.ensureConnected();
|
|
35545
35603
|
await this.ensureInitializedForFile(filePath);
|
|
35546
35604
|
const uri = this.syncDocument(filePath, content);
|
|
35547
|
-
|
|
35548
|
-
|
|
35549
|
-
|
|
35550
|
-
|
|
35605
|
+
let result;
|
|
35606
|
+
try {
|
|
35607
|
+
result = await this.sendRequest("textDocument/completion", {
|
|
35608
|
+
textDocument: { uri },
|
|
35609
|
+
position: { line, character }
|
|
35610
|
+
});
|
|
35611
|
+
} finally {
|
|
35612
|
+
this.closeDocument(uri);
|
|
35613
|
+
}
|
|
35551
35614
|
if (Array.isArray(result)) {
|
|
35552
35615
|
return result;
|
|
35553
35616
|
}
|
|
@@ -35563,18 +35626,27 @@ class GodotLSPClient {
|
|
|
35563
35626
|
await this.ensureConnected();
|
|
35564
35627
|
await this.ensureInitializedForFile(filePath);
|
|
35565
35628
|
const uri = this.syncDocument(filePath, content);
|
|
35566
|
-
|
|
35567
|
-
textDocument
|
|
35568
|
-
|
|
35569
|
-
|
|
35629
|
+
try {
|
|
35630
|
+
return await this.sendRequest("textDocument/hover", {
|
|
35631
|
+
textDocument: { uri },
|
|
35632
|
+
position: { line, character }
|
|
35633
|
+
});
|
|
35634
|
+
} finally {
|
|
35635
|
+
this.closeDocument(uri);
|
|
35636
|
+
}
|
|
35570
35637
|
}
|
|
35571
35638
|
async getDocumentSymbols(filePath, content) {
|
|
35572
35639
|
await this.ensureConnected();
|
|
35573
35640
|
await this.ensureInitializedForFile(filePath);
|
|
35574
35641
|
const uri = this.syncDocument(filePath, content);
|
|
35575
|
-
|
|
35576
|
-
|
|
35577
|
-
|
|
35642
|
+
let result;
|
|
35643
|
+
try {
|
|
35644
|
+
result = await this.sendRequest("textDocument/documentSymbol", {
|
|
35645
|
+
textDocument: { uri }
|
|
35646
|
+
});
|
|
35647
|
+
} finally {
|
|
35648
|
+
this.closeDocument(uri);
|
|
35649
|
+
}
|
|
35578
35650
|
return Array.isArray(result) ? result : [];
|
|
35579
35651
|
}
|
|
35580
35652
|
isConnected() {
|
|
@@ -36016,15 +36088,97 @@ var init_runtime_client = __esm(() => {
|
|
|
36016
36088
|
ANNOUNCEMENT_PATTERN = /^runtime-(\d+)\.json$/;
|
|
36017
36089
|
});
|
|
36018
36090
|
|
|
36091
|
+
// src/run-record.ts
|
|
36092
|
+
import { mkdirSync as mkdirSync6, openSync, readdirSync as readdirSync6, readFileSync as readFileSync12, rmSync as rmSync7, statSync as statSync2, writeFileSync as writeFileSync8 } from "node:fs";
|
|
36093
|
+
import { join as join14 } from "node:path";
|
|
36094
|
+
function runsDirectory() {
|
|
36095
|
+
return join14(runtimeDirectory(), "runs");
|
|
36096
|
+
}
|
|
36097
|
+
function recordPath() {
|
|
36098
|
+
return join14(runsDirectory(), "run.json");
|
|
36099
|
+
}
|
|
36100
|
+
function openTranscript(startedAt) {
|
|
36101
|
+
mkdirSync6(runsDirectory(), { recursive: true });
|
|
36102
|
+
const path = join14(runsDirectory(), `run-${startedAt}.log`);
|
|
36103
|
+
return { path, fd: openSync(path, "a") };
|
|
36104
|
+
}
|
|
36105
|
+
function writeRunRecord(record) {
|
|
36106
|
+
mkdirSync6(runsDirectory(), { recursive: true });
|
|
36107
|
+
writeFileSync8(recordPath(), JSON.stringify(record, null, 2), "utf8");
|
|
36108
|
+
}
|
|
36109
|
+
function readRunRecord() {
|
|
36110
|
+
let parsed;
|
|
36111
|
+
try {
|
|
36112
|
+
parsed = JSON.parse(readFileSync12(recordPath(), "utf8"));
|
|
36113
|
+
} catch {
|
|
36114
|
+
return null;
|
|
36115
|
+
}
|
|
36116
|
+
if (!parsed || typeof parsed !== "object") {
|
|
36117
|
+
return null;
|
|
36118
|
+
}
|
|
36119
|
+
const fields = parsed;
|
|
36120
|
+
const pid = fields["pid"];
|
|
36121
|
+
const transcript = fields["transcript"];
|
|
36122
|
+
const startedAt = fields["startedAt"];
|
|
36123
|
+
if (typeof pid !== "number" || typeof transcript !== "string" || typeof startedAt !== "number") {
|
|
36124
|
+
return null;
|
|
36125
|
+
}
|
|
36126
|
+
const args = fields["arguments"];
|
|
36127
|
+
return {
|
|
36128
|
+
pid,
|
|
36129
|
+
transcript,
|
|
36130
|
+
startedAt,
|
|
36131
|
+
projectPath: typeof fields["projectPath"] === "string" ? fields["projectPath"] : "",
|
|
36132
|
+
arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : []
|
|
36133
|
+
};
|
|
36134
|
+
}
|
|
36135
|
+
function clearRunRecord() {
|
|
36136
|
+
try {
|
|
36137
|
+
rmSync7(recordPath());
|
|
36138
|
+
} catch {}
|
|
36139
|
+
}
|
|
36140
|
+
function ageMs(path, now) {
|
|
36141
|
+
try {
|
|
36142
|
+
return now - statSync2(path).mtimeMs;
|
|
36143
|
+
} catch {
|
|
36144
|
+
return 0;
|
|
36145
|
+
}
|
|
36146
|
+
}
|
|
36147
|
+
function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
36148
|
+
const directory = runsDirectory();
|
|
36149
|
+
const keep = readRunRecord()?.transcript;
|
|
36150
|
+
let entries;
|
|
36151
|
+
try {
|
|
36152
|
+
entries = readdirSync6(directory);
|
|
36153
|
+
} catch {
|
|
36154
|
+
return;
|
|
36155
|
+
}
|
|
36156
|
+
for (const entry of entries) {
|
|
36157
|
+
if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
|
|
36158
|
+
continue;
|
|
36159
|
+
}
|
|
36160
|
+
const path = join14(directory, entry);
|
|
36161
|
+
if (path === keep || ageMs(path, now) < keepMs) {
|
|
36162
|
+
continue;
|
|
36163
|
+
}
|
|
36164
|
+
try {
|
|
36165
|
+
rmSync7(path);
|
|
36166
|
+
} catch {}
|
|
36167
|
+
}
|
|
36168
|
+
}
|
|
36169
|
+
var init_run_record = __esm(() => {
|
|
36170
|
+
init_runtime_client();
|
|
36171
|
+
});
|
|
36172
|
+
|
|
36019
36173
|
// src/setup.ts
|
|
36020
|
-
import { cpSync as cpSync2, existsSync as existsSync11, mkdirSync as
|
|
36021
|
-
import { dirname as dirname8, join as
|
|
36174
|
+
import { cpSync as cpSync2, existsSync as existsSync11, mkdirSync as mkdirSync7, readFileSync as readFileSync13, rmSync as rmSync8, writeFileSync as writeFileSync9 } from "node:fs";
|
|
36175
|
+
import { dirname as dirname8, join as join15 } from "node:path";
|
|
36022
36176
|
function installedAddonVersion(projectPath) {
|
|
36023
|
-
const marker =
|
|
36177
|
+
const marker = join15(projectPath, "addons", ADDONS2[0], VERSION_MARKER2);
|
|
36024
36178
|
if (!existsSync11(marker)) {
|
|
36025
36179
|
return null;
|
|
36026
36180
|
}
|
|
36027
|
-
const said =
|
|
36181
|
+
const said = readFileSync13(marker, "utf8").trim();
|
|
36028
36182
|
return said === "" ? null : said;
|
|
36029
36183
|
}
|
|
36030
36184
|
var ADDONS2, RUNTIME_AUTOLOAD2, VERSION_MARKER2 = ".gdharness-version";
|
|
@@ -36042,24 +36196,45 @@ var init_setup = __esm(() => {
|
|
|
36042
36196
|
|
|
36043
36197
|
// src/server.ts
|
|
36044
36198
|
import { execFile as execFile5, spawn } from "node:child_process";
|
|
36045
|
-
import {
|
|
36199
|
+
import {
|
|
36200
|
+
closeSync,
|
|
36201
|
+
existsSync as existsSync12,
|
|
36202
|
+
fstatSync,
|
|
36203
|
+
mkdirSync as mkdirSync8,
|
|
36204
|
+
mkdtempSync as mkdtempSync3,
|
|
36205
|
+
openSync as openSync2,
|
|
36206
|
+
readdirSync as readdirSync7,
|
|
36207
|
+
readFileSync as readFileSync14,
|
|
36208
|
+
readSync,
|
|
36209
|
+
realpathSync as realpathSync2,
|
|
36210
|
+
rmSync as rmSync9
|
|
36211
|
+
} from "node:fs";
|
|
36046
36212
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
36047
|
-
import { basename as basename2, dirname as dirname9, join as
|
|
36213
|
+
import { basename as basename2, dirname as dirname9, join as join16, normalize as normalize3 } from "node:path";
|
|
36048
36214
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
36049
36215
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
36050
36216
|
import { promisify as promisify5 } from "node:util";
|
|
36051
36217
|
function patienceForFrames(frames, atLeast) {
|
|
36052
36218
|
return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
|
|
36053
36219
|
}
|
|
36220
|
+
function stillRunning(run) {
|
|
36221
|
+
if (run?.exitCode !== null || run.endedUnwatched === true) {
|
|
36222
|
+
return false;
|
|
36223
|
+
}
|
|
36224
|
+
if (!run.throughEditor && run.process === null && run.pid !== null) {
|
|
36225
|
+
return alive(run.pid);
|
|
36226
|
+
}
|
|
36227
|
+
return true;
|
|
36228
|
+
}
|
|
36054
36229
|
function alive(pid) {
|
|
36055
|
-
if (pid === undefined) {
|
|
36230
|
+
if (pid === undefined || pid === null) {
|
|
36056
36231
|
return false;
|
|
36057
36232
|
}
|
|
36058
36233
|
try {
|
|
36059
36234
|
process.kill(pid, 0);
|
|
36060
36235
|
return true;
|
|
36061
|
-
} catch {
|
|
36062
|
-
return
|
|
36236
|
+
} catch (error) {
|
|
36237
|
+
return error.code === "EPERM";
|
|
36063
36238
|
}
|
|
36064
36239
|
}
|
|
36065
36240
|
function isOfType(value, kind) {
|
|
@@ -36139,13 +36314,13 @@ function aboveTheRunner(entry) {
|
|
|
36139
36314
|
}
|
|
36140
36315
|
function elsewhereIn(projectPath, asked) {
|
|
36141
36316
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
36142
|
-
if (wanted === "" || existsSync12(
|
|
36317
|
+
if (wanted === "" || existsSync12(join16(projectPath, wanted))) {
|
|
36143
36318
|
return "";
|
|
36144
36319
|
}
|
|
36145
36320
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
36146
36321
|
let near = [];
|
|
36147
36322
|
try {
|
|
36148
|
-
near =
|
|
36323
|
+
near = readdirSync7(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
|
|
36149
36324
|
} catch {
|
|
36150
36325
|
return "";
|
|
36151
36326
|
}
|
|
@@ -36153,11 +36328,11 @@ function elsewhereIn(projectPath, asked) {
|
|
|
36153
36328
|
return near.length === 0 ? "" : ` The project does have ${there}.`;
|
|
36154
36329
|
}
|
|
36155
36330
|
function hasMainScene(projectFile) {
|
|
36156
|
-
const scene = parseProjectGodot(
|
|
36331
|
+
const scene = parseProjectGodot(readFileSync14(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
36157
36332
|
return typeof scene === "string" && scene !== "";
|
|
36158
36333
|
}
|
|
36159
36334
|
function editorPlaysHeadless(projectFile) {
|
|
36160
|
-
const runArgs = parseProjectGodot(
|
|
36335
|
+
const runArgs = parseProjectGodot(readFileSync14(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
36161
36336
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
36162
36337
|
}
|
|
36163
36338
|
function camelCased(params) {
|
|
@@ -36173,7 +36348,7 @@ function camelCased(params) {
|
|
|
36173
36348
|
class GodotServer {
|
|
36174
36349
|
mcp;
|
|
36175
36350
|
locator = new GodotLocator2;
|
|
36176
|
-
operationsScript =
|
|
36351
|
+
operationsScript = join16(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
36177
36352
|
godotBridge;
|
|
36178
36353
|
tools = buildToolDefinitions();
|
|
36179
36354
|
activeProcess = null;
|
|
@@ -36330,10 +36505,7 @@ class GodotServer {
|
|
|
36330
36505
|
this.stopWatchingForASuccessor();
|
|
36331
36506
|
withdrawBridge(this.announcedAt);
|
|
36332
36507
|
this.announcedAt = null;
|
|
36333
|
-
|
|
36334
|
-
this.activeProcess.process?.kill();
|
|
36335
|
-
this.activeProcess = null;
|
|
36336
|
-
}
|
|
36508
|
+
this.activeProcess = null;
|
|
36337
36509
|
if (this.lspClient) {
|
|
36338
36510
|
try {
|
|
36339
36511
|
await this.lspClient.disconnect();
|
|
@@ -36398,14 +36570,7 @@ class GodotServer {
|
|
|
36398
36570
|
return;
|
|
36399
36571
|
}
|
|
36400
36572
|
this.shutdownInitiated = true;
|
|
36401
|
-
|
|
36402
|
-
try {
|
|
36403
|
-
this.activeProcess.process?.kill();
|
|
36404
|
-
} catch (error) {
|
|
36405
|
-
console.error("[SERVER] Failed to kill the Godot process on exit:", errorMessage(error));
|
|
36406
|
-
}
|
|
36407
|
-
this.activeProcess = null;
|
|
36408
|
-
}
|
|
36573
|
+
this.activeProcess = null;
|
|
36409
36574
|
}
|
|
36410
36575
|
setupToolHandlers() {
|
|
36411
36576
|
this.mcp.server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: this.tools }));
|
|
@@ -36765,7 +36930,7 @@ class GodotServer {
|
|
|
36765
36930
|
if (path === undefined) {
|
|
36766
36931
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
36767
36932
|
}
|
|
36768
|
-
const file =
|
|
36933
|
+
const file = join16(path, "project.godot");
|
|
36769
36934
|
if (!existsSync12(file)) {
|
|
36770
36935
|
return {
|
|
36771
36936
|
ok: false,
|
|
@@ -36776,6 +36941,21 @@ class GodotServer {
|
|
|
36776
36941
|
}
|
|
36777
36942
|
return { ok: true, value: { path, file } };
|
|
36778
36943
|
}
|
|
36944
|
+
gameArguments(args) {
|
|
36945
|
+
if (args["args"] === undefined) {
|
|
36946
|
+
return { ok: true, value: [] };
|
|
36947
|
+
}
|
|
36948
|
+
const given = readStringArray(args, "args");
|
|
36949
|
+
if (given === undefined) {
|
|
36950
|
+
return {
|
|
36951
|
+
ok: false,
|
|
36952
|
+
response: this.createErrorResponse("args must be a list of strings.", [
|
|
36953
|
+
`Pass the game's own flags, such as ["--level=2"]`
|
|
36954
|
+
])
|
|
36955
|
+
};
|
|
36956
|
+
}
|
|
36957
|
+
return { ok: true, value: given };
|
|
36958
|
+
}
|
|
36779
36959
|
async debuggedGame() {
|
|
36780
36960
|
const game = this.activeProcess;
|
|
36781
36961
|
if (game !== null && game.exitCode !== null) {
|
|
@@ -36920,7 +37100,7 @@ class GodotServer {
|
|
|
36920
37100
|
if (!engine.ok) {
|
|
36921
37101
|
return engine.response;
|
|
36922
37102
|
}
|
|
36923
|
-
const application = parseProjectGodot(
|
|
37103
|
+
const application = parseProjectGodot(readFileSync14(project.value.file, "utf8"))["application"] ?? {};
|
|
36924
37104
|
const name = application["config/name"];
|
|
36925
37105
|
const mainScene = application["run/main_scene"];
|
|
36926
37106
|
const info = {
|
|
@@ -36983,7 +37163,7 @@ class GodotServer {
|
|
|
36983
37163
|
}
|
|
36984
37164
|
const folder = dirname9(output.absolutePath);
|
|
36985
37165
|
try {
|
|
36986
|
-
|
|
37166
|
+
mkdirSync8(folder, { recursive: true });
|
|
36987
37167
|
} catch (error) {
|
|
36988
37168
|
return this.createErrorResponse(`Cannot create ${dirname9(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
36989
37169
|
}
|
|
@@ -37045,7 +37225,7 @@ class GodotServer {
|
|
|
37045
37225
|
return contained.response;
|
|
37046
37226
|
}
|
|
37047
37227
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
37048
|
-
if (!existsSync12(
|
|
37228
|
+
if (!existsSync12(join16(project.value.path, runner))) {
|
|
37049
37229
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
37050
37230
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
37051
37231
|
]);
|
|
@@ -37079,7 +37259,7 @@ class GodotServer {
|
|
|
37079
37259
|
];
|
|
37080
37260
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
37081
37261
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
37082
|
-
const userData = mkdtempSync3(
|
|
37262
|
+
const userData = mkdtempSync3(join16(tmpdir5(), "gdharness-tests-"));
|
|
37083
37263
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
37084
37264
|
const hung = await new Promise((resolve) => {
|
|
37085
37265
|
const timer = setTimeout(() => {
|
|
@@ -37095,26 +37275,26 @@ class GodotServer {
|
|
|
37095
37275
|
resolve(false);
|
|
37096
37276
|
});
|
|
37097
37277
|
});
|
|
37098
|
-
const reportsDir =
|
|
37278
|
+
const reportsDir = join16(project.value.path, ".godot", "gdharness-reports");
|
|
37099
37279
|
let report = null;
|
|
37100
37280
|
let reportProblem = null;
|
|
37101
37281
|
try {
|
|
37102
|
-
const written = existsSync12(reportsDir) ?
|
|
37282
|
+
const written = existsSync12(reportsDir) ? readdirSync7(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
37103
37283
|
const newest = written.at(-1);
|
|
37104
37284
|
if (newest !== undefined) {
|
|
37105
|
-
report = parseJUnit(
|
|
37285
|
+
report = parseJUnit(readFileSync14(join16(reportsDir, newest, "results.xml"), "utf8"));
|
|
37106
37286
|
}
|
|
37107
37287
|
} catch (error) {
|
|
37108
37288
|
reportProblem = errorMessage(error);
|
|
37109
37289
|
} finally {
|
|
37110
|
-
|
|
37111
|
-
|
|
37290
|
+
rmSync9(reportsDir, { recursive: true, force: true });
|
|
37291
|
+
rmSync9(userData, { recursive: true, force: true });
|
|
37112
37292
|
}
|
|
37113
37293
|
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
|
|
37114
37294
|
const verdicts = {
|
|
37115
37295
|
0: "passed",
|
|
37116
37296
|
100: "failures",
|
|
37117
|
-
101: "warnings",
|
|
37297
|
+
101: "warnings: orphan nodes",
|
|
37118
37298
|
103: "headless not supported by this gdUnit4",
|
|
37119
37299
|
104: "Godot version not supported by this gdUnit4",
|
|
37120
37300
|
105: "script errors"
|
|
@@ -37146,7 +37326,17 @@ class GodotServer {
|
|
|
37146
37326
|
};
|
|
37147
37327
|
}
|
|
37148
37328
|
const failed = report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ ...entry, path: suite.path })));
|
|
37149
|
-
const
|
|
37329
|
+
const orphans = orphansPrinted(said);
|
|
37330
|
+
const leftBehind = new Map(orphans.suites.map((suite) => [suite.path, suite.orphans]));
|
|
37331
|
+
const orphansIn = (path) => path === null ? 0 : leftBehind.get(path) ?? 0;
|
|
37332
|
+
const warnings = orphans.suites.map((suite) => ({
|
|
37333
|
+
kind: "orphans",
|
|
37334
|
+
path: suite.path,
|
|
37335
|
+
name: report.suites.find((entry) => entry.path === suite.path)?.name ?? suite.path,
|
|
37336
|
+
orphans: suite.orphans,
|
|
37337
|
+
message: `${suite.orphans} node${suite.orphans === 1 ? "" : "s"} were still in the tree when this suite finished. A node built in a test and neither freed nor added to the tree is the usual cause; free it, or add it with auto_free().`
|
|
37338
|
+
}));
|
|
37339
|
+
const unclean = report.suites.filter((suite) => suite.failures > 0 || suite.errors > 0 || suite.skipped > 0 || orphansIn(suite.path) > 0);
|
|
37150
37340
|
return this.jsonTextResponse({
|
|
37151
37341
|
passed: !hung && exitCode === 0 && report.failures === 0 && report.errors === 0,
|
|
37152
37342
|
verdict,
|
|
@@ -37157,6 +37347,9 @@ class GodotServer {
|
|
|
37157
37347
|
skipped: report.skipped,
|
|
37158
37348
|
time: report.time,
|
|
37159
37349
|
failed,
|
|
37350
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
37351
|
+
orphans: orphans.total > 0 ? orphans.total : undefined,
|
|
37352
|
+
note: verdict.startsWith("warnings") && warnings.length === 0 ? "gdUnit4 exits 101 for orphan nodes when nothing failed, and this run printed no count of them: orphan reporting may be off in the project settings." : undefined,
|
|
37160
37353
|
suites: unclean.map((suite) => ({
|
|
37161
37354
|
name: suite.name,
|
|
37162
37355
|
path: suite.path,
|
|
@@ -37164,6 +37357,7 @@ class GodotServer {
|
|
|
37164
37357
|
failures: suite.failures,
|
|
37165
37358
|
errors: suite.errors,
|
|
37166
37359
|
skipped: suite.skipped,
|
|
37360
|
+
orphans: orphansIn(suite.path) > 0 ? orphansIn(suite.path) : undefined,
|
|
37167
37361
|
time: suite.time
|
|
37168
37362
|
})),
|
|
37169
37363
|
suitesPassed: report.suites.length - unclean.length,
|
|
@@ -37290,7 +37484,7 @@ class GodotServer {
|
|
|
37290
37484
|
version: godotPath === null ? null : await this.godotVersion(godotPath)
|
|
37291
37485
|
},
|
|
37292
37486
|
game: {
|
|
37293
|
-
processActive: this.
|
|
37487
|
+
processActive: stillRunning(this.currentRun()),
|
|
37294
37488
|
playingInEditor: playing,
|
|
37295
37489
|
runtimeConnected: games.some((game) => game.reachable),
|
|
37296
37490
|
runtimes: games
|
|
@@ -37436,6 +37630,10 @@ class GodotServer {
|
|
|
37436
37630
|
if (sceneToRun && !sceneToRun.ok) {
|
|
37437
37631
|
return this.createErrorResponse(sceneToRun.reason, PATH_SOLUTIONS);
|
|
37438
37632
|
}
|
|
37633
|
+
const given = this.gameArguments(args);
|
|
37634
|
+
if (!given.ok) {
|
|
37635
|
+
return given.response;
|
|
37636
|
+
}
|
|
37439
37637
|
if (!sceneToRun && !hasMainScene(project.value.file)) {
|
|
37440
37638
|
return this.createErrorResponse("The project sets no main scene, so there is nothing to run.", [
|
|
37441
37639
|
"Pass scene to run one scene",
|
|
@@ -37452,9 +37650,9 @@ class GodotServer {
|
|
|
37452
37650
|
}
|
|
37453
37651
|
const sceneArgument = sceneToRun?.ok ? sceneToRun.relativePath : null;
|
|
37454
37652
|
if (op === "check") {
|
|
37455
|
-
return await this.checkBoot(engine.value, project.value.path, sceneArgument, args);
|
|
37653
|
+
return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value);
|
|
37456
37654
|
}
|
|
37457
|
-
if (this.
|
|
37655
|
+
if (stillRunning(this.currentRun())) {
|
|
37458
37656
|
this.logDebug("Ending the running game before starting another");
|
|
37459
37657
|
await this.endActiveGame();
|
|
37460
37658
|
}
|
|
@@ -37463,17 +37661,20 @@ class GodotServer {
|
|
|
37463
37661
|
platform: process.platform,
|
|
37464
37662
|
variables: process.env
|
|
37465
37663
|
});
|
|
37466
|
-
|
|
37664
|
+
const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
|
|
37665
|
+
if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0) {
|
|
37467
37666
|
return await this.playThroughEditor(sceneArgument, refreshed.value, project.value.path, alreadyPlaying);
|
|
37468
37667
|
}
|
|
37469
37668
|
const cmdArgs = runArguments({
|
|
37470
37669
|
projectPath: project.value.path,
|
|
37471
37670
|
headless,
|
|
37472
|
-
scene: sceneArgument
|
|
37671
|
+
scene: sceneArgument,
|
|
37672
|
+
userArgs: given.value
|
|
37473
37673
|
});
|
|
37474
37674
|
this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
37475
|
-
const started = this.
|
|
37675
|
+
const started = this.spawnKeptGame(engine.value, cmdArgs, project.value.path);
|
|
37476
37676
|
this.activeProcess = started;
|
|
37677
|
+
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." : "";
|
|
37477
37678
|
return this.jsonTextResponse({
|
|
37478
37679
|
started: true,
|
|
37479
37680
|
through: "gdharness",
|
|
@@ -37481,11 +37682,11 @@ class GodotServer {
|
|
|
37481
37682
|
arguments: cmdArgs,
|
|
37482
37683
|
refreshedClasses: refreshed.value,
|
|
37483
37684
|
runtime: await this.runtimeUp(project.value.path, alreadyPlaying),
|
|
37484
|
-
message:
|
|
37685
|
+
message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}`
|
|
37485
37686
|
});
|
|
37486
37687
|
}
|
|
37487
37688
|
async runtimeUp(projectPath, before) {
|
|
37488
|
-
if (!existsSync12(
|
|
37689
|
+
if (!existsSync12(join16(projectPath, RUNTIME_AUTOLOAD2.path))) {
|
|
37489
37690
|
return {
|
|
37490
37691
|
listening: false,
|
|
37491
37692
|
note: "this project has no gdharness_runtime addon, so the runtime_* tools have nothing to talk to"
|
|
@@ -37520,7 +37721,11 @@ class GodotServer {
|
|
|
37520
37721
|
}
|
|
37521
37722
|
const played = {
|
|
37522
37723
|
process: null,
|
|
37724
|
+
pid: null,
|
|
37523
37725
|
log,
|
|
37726
|
+
transcript: null,
|
|
37727
|
+
readOffset: 0,
|
|
37728
|
+
projectPath,
|
|
37524
37729
|
startedAt: Date.now(),
|
|
37525
37730
|
exitCode: null,
|
|
37526
37731
|
throughEditor: true,
|
|
@@ -37559,6 +37764,7 @@ class GodotServer {
|
|
|
37559
37764
|
async endActiveGame() {
|
|
37560
37765
|
const running = this.activeProcess;
|
|
37561
37766
|
this.activeProcess = null;
|
|
37767
|
+
clearRunRecord();
|
|
37562
37768
|
if (!running) {
|
|
37563
37769
|
return;
|
|
37564
37770
|
}
|
|
@@ -37566,14 +37772,26 @@ class GodotServer {
|
|
|
37566
37772
|
await this.handleViaBridge("stop_playing", {});
|
|
37567
37773
|
return;
|
|
37568
37774
|
}
|
|
37569
|
-
running.process
|
|
37775
|
+
if (running.process !== null) {
|
|
37776
|
+
running.process.kill();
|
|
37777
|
+
return;
|
|
37778
|
+
}
|
|
37779
|
+
if (running.pid !== null) {
|
|
37780
|
+
try {
|
|
37781
|
+
process.kill(running.pid);
|
|
37782
|
+
} catch {}
|
|
37783
|
+
}
|
|
37570
37784
|
}
|
|
37571
37785
|
spawnGame(godotPath, cmdArgs, env) {
|
|
37572
37786
|
const child = spawn(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
|
|
37573
37787
|
const log = new GameLog;
|
|
37574
37788
|
const started = {
|
|
37575
37789
|
process: child,
|
|
37790
|
+
pid: child.pid ?? null,
|
|
37576
37791
|
log,
|
|
37792
|
+
transcript: null,
|
|
37793
|
+
readOffset: 0,
|
|
37794
|
+
projectPath: null,
|
|
37577
37795
|
startedAt: Date.now(),
|
|
37578
37796
|
exitCode: null,
|
|
37579
37797
|
throughEditor: false,
|
|
@@ -37598,10 +37816,121 @@ class GodotServer {
|
|
|
37598
37816
|
});
|
|
37599
37817
|
return started;
|
|
37600
37818
|
}
|
|
37601
|
-
|
|
37819
|
+
spawnKeptGame(godotPath, cmdArgs, projectPath, env) {
|
|
37820
|
+
const startedAt = Date.now();
|
|
37821
|
+
const transcript = openTranscript(startedAt);
|
|
37822
|
+
let child;
|
|
37823
|
+
try {
|
|
37824
|
+
child = spawn(godotPath, cmdArgs, {
|
|
37825
|
+
stdio: ["ignore", transcript.fd, transcript.fd],
|
|
37826
|
+
detached: true,
|
|
37827
|
+
...env ? { env } : {}
|
|
37828
|
+
});
|
|
37829
|
+
} finally {
|
|
37830
|
+
closeSync(transcript.fd);
|
|
37831
|
+
}
|
|
37832
|
+
child.unref();
|
|
37833
|
+
const log = new GameLog;
|
|
37834
|
+
const started = {
|
|
37835
|
+
process: child,
|
|
37836
|
+
pid: child.pid ?? null,
|
|
37837
|
+
log,
|
|
37838
|
+
transcript: transcript.path,
|
|
37839
|
+
readOffset: 0,
|
|
37840
|
+
projectPath,
|
|
37841
|
+
startedAt,
|
|
37842
|
+
exitCode: null,
|
|
37843
|
+
throughEditor: false,
|
|
37844
|
+
brokeOn: null
|
|
37845
|
+
};
|
|
37846
|
+
child.on("exit", (code) => {
|
|
37847
|
+
this.logDebug(`Godot process exited with code ${code ?? "none"}`);
|
|
37848
|
+
this.drainTranscript(started);
|
|
37849
|
+
log.finish();
|
|
37850
|
+
started.exitCode = code ?? -1;
|
|
37851
|
+
});
|
|
37852
|
+
child.on("error", (err) => {
|
|
37853
|
+
console.error("Failed to start Godot process:", err);
|
|
37854
|
+
log.append("stderr", `${err.message}
|
|
37855
|
+
`);
|
|
37856
|
+
started.exitCode = -1;
|
|
37857
|
+
});
|
|
37858
|
+
if (started.pid !== null) {
|
|
37859
|
+
writeRunRecord({
|
|
37860
|
+
pid: started.pid,
|
|
37861
|
+
transcript: transcript.path,
|
|
37862
|
+
startedAt,
|
|
37863
|
+
projectPath,
|
|
37864
|
+
arguments: cmdArgs
|
|
37865
|
+
});
|
|
37866
|
+
}
|
|
37867
|
+
sweepTranscripts();
|
|
37868
|
+
return started;
|
|
37869
|
+
}
|
|
37870
|
+
drainTranscript(game) {
|
|
37871
|
+
if (game.transcript === null) {
|
|
37872
|
+
return;
|
|
37873
|
+
}
|
|
37874
|
+
let handle;
|
|
37875
|
+
try {
|
|
37876
|
+
handle = openSync2(game.transcript, "r");
|
|
37877
|
+
} catch {
|
|
37878
|
+
return;
|
|
37879
|
+
}
|
|
37880
|
+
try {
|
|
37881
|
+
const size = fstatSync(handle).size;
|
|
37882
|
+
if (size <= game.readOffset) {
|
|
37883
|
+
return;
|
|
37884
|
+
}
|
|
37885
|
+
const buffer = Buffer.alloc(size - game.readOffset);
|
|
37886
|
+
const read = readSync(handle, buffer, 0, buffer.length, game.readOffset);
|
|
37887
|
+
game.readOffset += read;
|
|
37888
|
+
game.log.append("transcript", buffer.subarray(0, read));
|
|
37889
|
+
} finally {
|
|
37890
|
+
closeSync(handle);
|
|
37891
|
+
}
|
|
37892
|
+
}
|
|
37893
|
+
adoptRecordedRun() {
|
|
37894
|
+
const record = readRunRecord();
|
|
37895
|
+
if (record === null || !this.couldBeOurs(record.projectPath)) {
|
|
37896
|
+
return null;
|
|
37897
|
+
}
|
|
37898
|
+
const adopted = {
|
|
37899
|
+
process: null,
|
|
37900
|
+
pid: record.pid,
|
|
37901
|
+
log: new GameLog,
|
|
37902
|
+
transcript: record.transcript,
|
|
37903
|
+
readOffset: 0,
|
|
37904
|
+
projectPath: record.projectPath === "" ? null : record.projectPath,
|
|
37905
|
+
startedAt: record.startedAt,
|
|
37906
|
+
exitCode: null,
|
|
37907
|
+
throughEditor: false,
|
|
37908
|
+
brokeOn: null
|
|
37909
|
+
};
|
|
37910
|
+
this.drainTranscript(adopted);
|
|
37911
|
+
if (!alive(record.pid)) {
|
|
37912
|
+
adopted.log.finish();
|
|
37913
|
+
adopted.exitCode = null;
|
|
37914
|
+
adopted.endedUnwatched = true;
|
|
37915
|
+
}
|
|
37916
|
+
this.activeProcess = adopted;
|
|
37917
|
+
return adopted;
|
|
37918
|
+
}
|
|
37919
|
+
couldBeOurs(project) {
|
|
37920
|
+
if (project === "") {
|
|
37921
|
+
return true;
|
|
37922
|
+
}
|
|
37923
|
+
const status = this.godotBridge.getStatus();
|
|
37924
|
+
const mine = this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
|
|
37925
|
+
return mine === null || samePath(mine, project);
|
|
37926
|
+
}
|
|
37927
|
+
currentRun() {
|
|
37928
|
+
return this.activeProcess ?? this.adoptRecordedRun();
|
|
37929
|
+
}
|
|
37930
|
+
async checkBoot(godotPath, projectPath, scene, args, userArgs) {
|
|
37602
37931
|
const frames = readPositiveNumber(args, "frames") ?? 3;
|
|
37603
37932
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 60000;
|
|
37604
|
-
const cmdArgs = runArguments({ projectPath, headless: true, scene, quitAfter: frames });
|
|
37933
|
+
const cmdArgs = runArguments({ projectPath, headless: true, scene, quitAfter: frames, userArgs });
|
|
37605
37934
|
const boot = this.spawnGame(godotPath, cmdArgs);
|
|
37606
37935
|
const hung = await new Promise((resolve) => {
|
|
37607
37936
|
const timer = setTimeout(() => {
|
|
@@ -37631,37 +37960,47 @@ class GodotServer {
|
|
|
37631
37960
|
});
|
|
37632
37961
|
}
|
|
37633
37962
|
handleGetDebugOutput(args) {
|
|
37634
|
-
|
|
37963
|
+
const run = this.currentRun();
|
|
37964
|
+
if (!run) {
|
|
37635
37965
|
return this.createErrorResponse("No game is running. Start one with editor_run.");
|
|
37636
37966
|
}
|
|
37637
|
-
this.drainEditorOutput(
|
|
37967
|
+
this.drainEditorOutput(run);
|
|
37968
|
+
this.drainTranscript(run);
|
|
37969
|
+
if (run.endedUnwatched !== true && !stillRunning(run)) {
|
|
37970
|
+
run.log.finish();
|
|
37971
|
+
run.endedUnwatched = run.exitCode === null;
|
|
37972
|
+
}
|
|
37638
37973
|
const severity = readString(args, "severity");
|
|
37639
|
-
const selected =
|
|
37974
|
+
const selected = run.log.select({
|
|
37640
37975
|
severity: severity === "error" || severity === "warning" ? severity : "info",
|
|
37641
37976
|
sinceLastCall: readBoolean(args, "sinceLastCall") ?? false,
|
|
37642
37977
|
contains: readNonEmptyString(args, "contains"),
|
|
37643
37978
|
limit: readPositiveNumber(args, "limit") ?? 200
|
|
37644
37979
|
});
|
|
37645
|
-
const halt =
|
|
37980
|
+
const halt = run.throughEditor ? this.dapClient?.whereItStopped() ?? null : null;
|
|
37646
37981
|
return this.jsonTextResponse({
|
|
37647
|
-
running:
|
|
37648
|
-
exitCode:
|
|
37649
|
-
through:
|
|
37650
|
-
pid:
|
|
37651
|
-
errors:
|
|
37652
|
-
warnings:
|
|
37653
|
-
clean:
|
|
37982
|
+
running: stillRunning(run),
|
|
37983
|
+
exitCode: run.exitCode,
|
|
37984
|
+
through: run.throughEditor ? "editor" : "gdharness",
|
|
37985
|
+
pid: run.pid,
|
|
37986
|
+
errors: run.log.count("error"),
|
|
37987
|
+
warnings: run.log.count("warning"),
|
|
37988
|
+
clean: run.log.count("error") === 0,
|
|
37654
37989
|
heldAt: halt,
|
|
37990
|
+
endedUnwatched: run.endedUnwatched === true ? true : undefined,
|
|
37991
|
+
project: !run.throughEditor && run.process === null ? run.projectPath ?? undefined : undefined,
|
|
37992
|
+
note: run.endedUnwatched === true ? "This run outlived the server that started it and is over now, so its exit code was never collected. Everything it printed is below, read back from its transcript." : undefined,
|
|
37655
37993
|
omitted: selected.omitted,
|
|
37656
37994
|
entries: selected.entries
|
|
37657
37995
|
});
|
|
37658
37996
|
}
|
|
37659
37997
|
async handleStopProject() {
|
|
37660
|
-
|
|
37998
|
+
const stopped = this.currentRun();
|
|
37999
|
+
if (!stopped) {
|
|
37661
38000
|
return this.createErrorResponse("No game is running. Start one with editor_run.");
|
|
37662
38001
|
}
|
|
37663
|
-
const stopped = this.activeProcess;
|
|
37664
38002
|
this.drainEditorOutput(stopped);
|
|
38003
|
+
this.drainTranscript(stopped);
|
|
37665
38004
|
this.logDebug("Stopping the running game");
|
|
37666
38005
|
await this.endActiveGame();
|
|
37667
38006
|
return this.jsonTextResponse({
|
|
@@ -37767,8 +38106,8 @@ class GodotServer {
|
|
|
37767
38106
|
return this.createErrorResponse(choice.problem);
|
|
37768
38107
|
}
|
|
37769
38108
|
const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
|
|
37770
|
-
const screenshotDir = expectsScreenshot ? mkdtempSync3(
|
|
37771
|
-
const screenshotPath = screenshotDir ?
|
|
38109
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync3(join16(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
|
|
38110
|
+
const screenshotPath = screenshotDir ? join16(screenshotDir, "capture.png") : null;
|
|
37772
38111
|
try {
|
|
37773
38112
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
|
|
37774
38113
|
if (!reply.ok) {
|
|
@@ -37789,12 +38128,12 @@ class GodotServer {
|
|
|
37789
38128
|
return {
|
|
37790
38129
|
content: [
|
|
37791
38130
|
{ type: "text", text: `Screenshot captured: ${dimensions}` },
|
|
37792
|
-
{ type: "image", data:
|
|
38131
|
+
{ type: "image", data: readFileSync14(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
37793
38132
|
]
|
|
37794
38133
|
};
|
|
37795
38134
|
} finally {
|
|
37796
38135
|
if (screenshotDir) {
|
|
37797
|
-
|
|
38136
|
+
rmSync9(screenshotDir, { recursive: true, force: true });
|
|
37798
38137
|
}
|
|
37799
38138
|
}
|
|
37800
38139
|
}
|
|
@@ -37819,12 +38158,14 @@ class GodotServer {
|
|
|
37819
38158
|
return this.createErrorResponse("runtime_inspect find needs at least one of className, script, namePattern, group, says.");
|
|
37820
38159
|
}
|
|
37821
38160
|
const property = readNonEmptyString(args, "property");
|
|
38161
|
+
const includeHidden = readBoolean(args, "includeHidden");
|
|
37822
38162
|
return await this.handleRuntimeCommand("find_nodes", {
|
|
37823
38163
|
...filters,
|
|
37824
38164
|
projectPath: args["projectPath"],
|
|
37825
38165
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
37826
38166
|
limit: readPositiveNumber(args, "limit") ?? 100,
|
|
37827
|
-
...property === undefined ? {} : { property }
|
|
38167
|
+
...property === undefined ? {} : { property },
|
|
38168
|
+
...includeHidden === undefined ? {} : { include_hidden: includeHidden }
|
|
37828
38169
|
});
|
|
37829
38170
|
}
|
|
37830
38171
|
async handleRuntimeWait(op, args) {
|
|
@@ -37875,6 +38216,7 @@ var init_server3 = __esm(() => {
|
|
|
37875
38216
|
init_ports();
|
|
37876
38217
|
init_project_scan();
|
|
37877
38218
|
init_resources();
|
|
38219
|
+
init_run_record();
|
|
37878
38220
|
init_runtime_client();
|
|
37879
38221
|
init_server_version();
|
|
37880
38222
|
init_setup();
|
|
@@ -37945,7 +38287,7 @@ var init_server3 = __esm(() => {
|
|
|
37945
38287
|
|
|
37946
38288
|
// src/cli.ts
|
|
37947
38289
|
import { existsSync as existsSync13 } from "fs";
|
|
37948
|
-
import { join as
|
|
38290
|
+
import { join as join17, resolve as resolve5 } from "path";
|
|
37949
38291
|
|
|
37950
38292
|
// src/errors.ts
|
|
37951
38293
|
class Refusal extends Error {
|
|
@@ -39336,6 +39678,11 @@ them. \`editor_rescan\` asks it to scan and names any class it still cannot reso
|
|
|
39336
39678
|
declaration and the cache can both be right while the editor stays blind to it. Change the
|
|
39337
39679
|
declaring script, or restart the editor.
|
|
39338
39680
|
|
|
39681
|
+
A diagnostic about a member rather than a type, \`Static function "x()" not found in base "Y"\`
|
|
39682
|
+
about something the engine compiles, is a different thing and is no longer stale here: each ask
|
|
39683
|
+
gives the document back when its answer arrives, so the next one is read off disk. If one turns
|
|
39684
|
+
up anyway, believe the run over the diagnostic and say so.
|
|
39685
|
+
|
|
39339
39686
|
## Refusals are useful
|
|
39340
39687
|
|
|
39341
39688
|
An unknown op, an argument the tool does not name, or a missing required one is refused with the
|
|
@@ -39448,7 +39795,7 @@ class UsageError extends Refusal {
|
|
|
39448
39795
|
function projectArgument(at) {
|
|
39449
39796
|
const named = args.slice(at).find((value) => !value.startsWith("--"));
|
|
39450
39797
|
const projectPath = resolve5(named ?? ".");
|
|
39451
|
-
if (!existsSync13(
|
|
39798
|
+
if (!existsSync13(join17(projectPath, "project.godot"))) {
|
|
39452
39799
|
throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
|
|
39453
39800
|
}
|
|
39454
39801
|
return projectPath;
|