gdharness 0.13.35 → 0.13.37
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 +556 -234
- package/build/index.js +396 -132
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -9846,9 +9846,23 @@ function runLine(runner, version, rest = "") {
|
|
|
9846
9846
|
}
|
|
9847
9847
|
|
|
9848
9848
|
// src/update-check.ts
|
|
9849
|
-
var
|
|
9849
|
+
var REGISTRY_ROOT = "https://registry.npmjs.org";
|
|
9850
|
+
function packageUrl(root) {
|
|
9851
|
+
return `${root.replace(/\/+$/, "")}/gdharness/latest`;
|
|
9852
|
+
}
|
|
9853
|
+
function registryFor(environment) {
|
|
9854
|
+
const named = environment["GDHARNESS_REGISTRY"] ?? "";
|
|
9855
|
+
if (named === "") {
|
|
9856
|
+
return packageUrl(REGISTRY_ROOT);
|
|
9857
|
+
}
|
|
9858
|
+
try {
|
|
9859
|
+
return new URL(named).protocol === "https:" ? packageUrl(named) : packageUrl(REGISTRY_ROOT);
|
|
9860
|
+
} catch {
|
|
9861
|
+
return packageUrl(REGISTRY_ROOT);
|
|
9862
|
+
}
|
|
9863
|
+
}
|
|
9850
9864
|
var RELEASES = "https://github.com/Aureliolo/gdharness/releases/tag";
|
|
9851
|
-
var CACHE_MS =
|
|
9865
|
+
var CACHE_MS = 10 * 60 * 1000;
|
|
9852
9866
|
var REQUEST_TIMEOUT_MS = 1e4;
|
|
9853
9867
|
var MAX_BODY_BYTES = 1 << 20;
|
|
9854
9868
|
var FIRST_RETRY_MS = 30000;
|
|
@@ -9923,8 +9937,8 @@ function isNewer(candidate, current) {
|
|
|
9923
9937
|
}
|
|
9924
9938
|
return !left.prerelease && right.prerelease;
|
|
9925
9939
|
}
|
|
9926
|
-
async function fetchLatest() {
|
|
9927
|
-
const response = await fetch(
|
|
9940
|
+
async function fetchLatest(registry) {
|
|
9941
|
+
const response = await fetch(registry, {
|
|
9928
9942
|
headers: { accept: "application/vnd.npm.install-v1+json, application/json" },
|
|
9929
9943
|
redirect: "error",
|
|
9930
9944
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
@@ -9956,16 +9970,19 @@ async function fetchLatest() {
|
|
|
9956
9970
|
class UpdateCheck {
|
|
9957
9971
|
latest = null;
|
|
9958
9972
|
checking = false;
|
|
9973
|
+
asked = false;
|
|
9959
9974
|
checkedAt = 0;
|
|
9960
9975
|
retryAt = 0;
|
|
9961
9976
|
backoffMs = FIRST_RETRY_MS;
|
|
9962
9977
|
enabled;
|
|
9963
9978
|
current;
|
|
9964
9979
|
cachePath;
|
|
9965
|
-
|
|
9980
|
+
ask;
|
|
9981
|
+
constructor(current, environment = process.env, ask = () => fetchLatest(registryFor(environment))) {
|
|
9966
9982
|
this.current = current;
|
|
9967
9983
|
this.enabled = (environment["GDHARNESS_NO_UPDATE_CHECK"] ?? "") === "";
|
|
9968
9984
|
this.cachePath = cacheFile(environment);
|
|
9985
|
+
this.ask = ask;
|
|
9969
9986
|
const cached = this.enabled ? readCache(this.cachePath) : null;
|
|
9970
9987
|
if (cached !== null) {
|
|
9971
9988
|
this.latest = cached.latest;
|
|
@@ -9973,11 +9990,12 @@ class UpdateCheck {
|
|
|
9973
9990
|
}
|
|
9974
9991
|
}
|
|
9975
9992
|
refresh(now = Date.now()) {
|
|
9976
|
-
if (!this.enabled || this.checking || now < this.retryAt || !this.stale(now)) {
|
|
9993
|
+
if (!this.enabled || this.checking || now < this.retryAt || this.asked && !this.stale(now)) {
|
|
9977
9994
|
return;
|
|
9978
9995
|
}
|
|
9996
|
+
this.asked = true;
|
|
9979
9997
|
this.checking = true;
|
|
9980
|
-
|
|
9998
|
+
this.ask().then((version) => {
|
|
9981
9999
|
if (version === null) {
|
|
9982
10000
|
this.scheduleRetry(now);
|
|
9983
10001
|
return;
|
|
@@ -10107,15 +10125,26 @@ function filledTemplate(where, message, godotVersion) {
|
|
|
10107
10125
|
].join(`
|
|
10108
10126
|
`);
|
|
10109
10127
|
}
|
|
10110
|
-
function defectReport(where, error,
|
|
10128
|
+
function defectReport(where, error, context = {}) {
|
|
10129
|
+
const { godotVersion, seenBefore = 0 } = context;
|
|
10111
10130
|
const message = errorMessage(error);
|
|
10112
10131
|
const title = `Defect ${defectSignature(where, message)}: ${where}`;
|
|
10113
10132
|
const url = `${NEW_ISSUE}?template=bug_report.md&title=${encodeURIComponent(title)}` + `&body=${encodeURIComponent(filledTemplate(where, message, godotVersion))}`;
|
|
10114
10133
|
const width = Math.max(...facts(where, message, godotVersion).map(([label]) => (label ?? "").length));
|
|
10115
10134
|
return [
|
|
10116
|
-
"gdharness failed in a way it does not model.
|
|
10117
|
-
"
|
|
10118
|
-
"
|
|
10135
|
+
"gdharness failed in a way it does not model. Whatever went wrong underneath, it reached you",
|
|
10136
|
+
"as a crash rather than as a refusal naming what would have worked, and that much is a defect",
|
|
10137
|
+
"in gdharness however the rest of it turns out.",
|
|
10138
|
+
"",
|
|
10139
|
+
...seenBefore === 0 ? [
|
|
10140
|
+
"Send the call once more before anything else. Some of what reaches this line is a file",
|
|
10141
|
+
"another process had open, or a connection that dropped, and those are gone by the next",
|
|
10142
|
+
"attempt. If the same Signature comes back, it is not one of those, and repeating it",
|
|
10143
|
+
"further only spends turns."
|
|
10144
|
+
] : [
|
|
10145
|
+
`You have had this exact failure ${seenBefore === 1 ? "once" : `${seenBefore} times`} in this session already, so it is a`,
|
|
10146
|
+
"state rather than a passing one, and sending the call again will not change it."
|
|
10147
|
+
],
|
|
10119
10148
|
"",
|
|
10120
10149
|
...facts(where, message, godotVersion).map(([label, value]) => ` ${(label ?? "").padEnd(width)} ${value ?? ""}`),
|
|
10121
10150
|
"",
|
|
@@ -10146,15 +10175,15 @@ import {
|
|
|
10146
10175
|
closeSync,
|
|
10147
10176
|
existsSync as existsSync9,
|
|
10148
10177
|
fstatSync,
|
|
10149
|
-
mkdirSync as
|
|
10178
|
+
mkdirSync as mkdirSync6,
|
|
10150
10179
|
mkdtempSync as mkdtempSync2,
|
|
10151
10180
|
openSync as openSync2,
|
|
10152
10181
|
readdirSync as readdirSync6,
|
|
10153
|
-
readFileSync as
|
|
10182
|
+
readFileSync as readFileSync12,
|
|
10154
10183
|
readSync
|
|
10155
10184
|
} from "node:fs";
|
|
10156
10185
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
10157
|
-
import { basename as basename2, dirname as
|
|
10186
|
+
import { basename as basename2, dirname as dirname5, join as join12, normalize as normalize2 } from "node:path";
|
|
10158
10187
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
10159
10188
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
10160
10189
|
import { promisify as promisify4 } from "node:util";
|
|
@@ -24886,7 +24915,7 @@ function staleAnalysisNote(contradicted, dependsOn) {
|
|
|
24886
24915
|
}
|
|
24887
24916
|
const types = new Set(contradicted.map((one) => one.type));
|
|
24888
24917
|
const declaring = [...new Set(contradicted.map((one) => one.declaredIn))].sort();
|
|
24889
|
-
const lever = dependsOn.length === 0 ? ", and the named types depend on no other global class, so that lever is not available here. " + "That is the common case rather than the awkward one: an edit near a type that depends on " + "something refreshes it before the fault is noticed, which is why a type with no " + "dependencies is where this turns up" : `, so changing ${[...dependsOn].sort().join("
|
|
24918
|
+
const lever = dependsOn.length === 0 ? ", and the named types depend on no other global class, so that lever is not available here. " + "That is the common case rather than the awkward one: an edit near a type that depends on " + "something refreshes it before the fault is noticed, which is why a type with no " + "dependencies is where this turns up" : `, so changing ${[...dependsOn].sort().join(" or ")} and then rescanning is worth a try`;
|
|
24890
24919
|
return `The editor is reporting against an older copy of ${types.size === 1 ? "a type" : "some types"} ` + "named under contradictedByTheFile. Each member listed is declared in the file the class cache " + "points at, so those diagnostics are wrong however the code is written. Run editor_rescan first: " + "it costs about half a second, and it has cleared this in every reproduction measured since the " + "scan timing was fixed: both in one project, the scan returning in 243ms and 275ms, and one in " + "another where the same call also rebuilt the copy. " + "The fault behind it is that a script the editor has loaded keeps the copy it built, and nothing " + `rebuilds that copy when the file changes; ${declaring.length === 1 ? `editor_rescan with reloadScript set to ${declaring[0]} recompiles that copy` : `reloadScript takes one script, so a call each for ${declaring.join(" and ")} recompiles those copies`} from the file into the same ` + "object and answers with the members it has afterwards under reloadedMethods. Read that and the " + "next diagnostics as two readings, because they are of two things: measured in one window on " + "one editor, the analyser resolved a call to a newly added method while the built copy of that " + "same script did not have it. So the reload is worth doing and is not what clears these; the " + "scan is. The other lever measured is that a held type is refreshed when something it " + `depends on changes rather than when it changes itself${lever}. editor_launch restart has always ` + "worked and costs a window. project_import refresh_classes does not, and answers added: [] while " + "this is happening.";
|
|
24891
24920
|
}
|
|
24892
24921
|
function uncachedClassNote(types) {
|
|
@@ -25574,6 +25603,11 @@ function announced(line) {
|
|
|
25574
25603
|
return { severity: match[2] === "WARNING" ? "warning" : "error", text: match[3] ?? "" };
|
|
25575
25604
|
}
|
|
25576
25605
|
var STREAMS = ["stdout", "stderr", "transcript"];
|
|
25606
|
+
var ADMITTED = {
|
|
25607
|
+
error: new Set(["error"]),
|
|
25608
|
+
warning: new Set(["error", "warning"]),
|
|
25609
|
+
info: new Set(["error", "warning", "info"])
|
|
25610
|
+
};
|
|
25577
25611
|
function forAnswer(entries) {
|
|
25578
25612
|
return entries.map((entry) => entry.detail.length === 0 ? { index: entry.index, severity: entry.severity, source: entry.source, text: entry.text } : entry);
|
|
25579
25613
|
}
|
|
@@ -25587,7 +25621,7 @@ class GameLog {
|
|
|
25587
25621
|
};
|
|
25588
25622
|
partial = { stdout: "", stderr: "", transcript: "" };
|
|
25589
25623
|
lastHeadline = null;
|
|
25590
|
-
|
|
25624
|
+
seen = { error: 0, warning: 0, info: 0 };
|
|
25591
25625
|
append(source, chunk) {
|
|
25592
25626
|
const decoded = typeof chunk === "string" ? chunk : this.decoders[source].write(chunk);
|
|
25593
25627
|
const lines = (this.partial[source] + decoded).split(`
|
|
@@ -25644,10 +25678,14 @@ class GameLog {
|
|
|
25644
25678
|
return this.entries.filter((entry) => entry.severity === severity).length;
|
|
25645
25679
|
}
|
|
25646
25680
|
select(options) {
|
|
25647
|
-
const floor = options.sinceLastCall ? this.
|
|
25648
|
-
|
|
25649
|
-
const wanted = new Set(options.severity === "error" ? ["error"] : options.severity === "warning" ? ["error", "warning"] : ["error", "warning", "info"]);
|
|
25681
|
+
const floor = options.sinceLastCall ? this.seen[options.severity] : 0;
|
|
25682
|
+
const wanted = ADMITTED[options.severity];
|
|
25650
25683
|
const needle = options.contains?.toLowerCase();
|
|
25684
|
+
if (needle === undefined) {
|
|
25685
|
+
for (const severity of wanted) {
|
|
25686
|
+
this.seen[severity] = this.entries.length;
|
|
25687
|
+
}
|
|
25688
|
+
}
|
|
25651
25689
|
const matching = this.entries.slice(floor).filter((entry) => {
|
|
25652
25690
|
if (!wanted.has(entry.severity)) {
|
|
25653
25691
|
return false;
|
|
@@ -26353,6 +26391,10 @@ function userDataIn(home, variables = process.env) {
|
|
|
26353
26391
|
}
|
|
26354
26392
|
return carried;
|
|
26355
26393
|
}
|
|
26394
|
+
function savesStayPut(platform = process.platform) {
|
|
26395
|
+
return platform === "darwin";
|
|
26396
|
+
}
|
|
26397
|
+
var SAVES_NOT_MOVED_NOTE = "On macOS the engine reads user:// off HOME and ignores the variables that move it elsewhere, measured on Godot 4.7.2, so this run wrote its saves where the player keeps theirs. A suite that saves a game has written into the same folder as the copy being played, and one that tidies up after itself may have removed real saves.";
|
|
26356
26398
|
|
|
26357
26399
|
// src/godot-path.ts
|
|
26358
26400
|
var run = promisify(execFile);
|
|
@@ -26361,47 +26403,65 @@ class GodotLocator {
|
|
|
26361
26403
|
answered = new Map;
|
|
26362
26404
|
found = null;
|
|
26363
26405
|
async find() {
|
|
26364
|
-
if (this.found !== null && await this.
|
|
26365
|
-
return this.found;
|
|
26406
|
+
if (this.found !== null && await this.whyNot(this.found) === null) {
|
|
26407
|
+
return { ok: true, path: this.found };
|
|
26366
26408
|
}
|
|
26367
26409
|
this.found = null;
|
|
26368
26410
|
const named = envValue("GODOT_PATH");
|
|
26369
26411
|
if (named !== undefined) {
|
|
26370
26412
|
const path = normalize(named);
|
|
26371
|
-
|
|
26413
|
+
const reason = await this.whyNot(path);
|
|
26414
|
+
if (reason === null) {
|
|
26372
26415
|
this.found = path;
|
|
26416
|
+
return { ok: true, path };
|
|
26373
26417
|
}
|
|
26374
|
-
return
|
|
26418
|
+
return { ok: false, named: path, reason };
|
|
26375
26419
|
}
|
|
26376
26420
|
for (const candidate of godotCandidates(process.platform, resolveHomeDirectory())) {
|
|
26377
26421
|
const path = normalize(candidate);
|
|
26378
|
-
if (await this.
|
|
26422
|
+
if (await this.whyNot(path) === null) {
|
|
26379
26423
|
this.found = path;
|
|
26380
|
-
return path;
|
|
26424
|
+
return { ok: true, path };
|
|
26381
26425
|
}
|
|
26382
26426
|
}
|
|
26383
|
-
return null;
|
|
26427
|
+
return { ok: false, named: null };
|
|
26384
26428
|
}
|
|
26385
|
-
static
|
|
26386
|
-
|
|
26387
|
-
|
|
26388
|
-
|
|
26389
|
-
|
|
26429
|
+
static refusal(located) {
|
|
26430
|
+
if (located.named === null) {
|
|
26431
|
+
return {
|
|
26432
|
+
message: "No Godot executable found.",
|
|
26433
|
+
advice: [
|
|
26434
|
+
"Set GODOT_PATH to the Godot 4 executable",
|
|
26435
|
+
"Or install Godot where it is usually found, such as /usr/bin/godot or C:\\Program Files\\Godot"
|
|
26436
|
+
]
|
|
26437
|
+
};
|
|
26438
|
+
}
|
|
26439
|
+
return {
|
|
26440
|
+
message: `GODOT_PATH is set to ${located.named}, which ${located.reason ?? "does not answer"}.`,
|
|
26441
|
+
advice: [
|
|
26442
|
+
"Point GODOT_PATH at a Godot 4 executable that answers --version",
|
|
26443
|
+
"Or unset it, and the usual install locations are searched instead"
|
|
26444
|
+
]
|
|
26445
|
+
};
|
|
26446
|
+
}
|
|
26447
|
+
async whyNot(path) {
|
|
26390
26448
|
const known = this.answered.get(path);
|
|
26391
26449
|
if (known !== undefined) {
|
|
26392
26450
|
return known;
|
|
26393
26451
|
}
|
|
26394
|
-
let
|
|
26395
|
-
if (path
|
|
26452
|
+
let reason;
|
|
26453
|
+
if (path !== "godot" && !existsSync5(path)) {
|
|
26454
|
+
reason = "does not exist";
|
|
26455
|
+
} else {
|
|
26396
26456
|
try {
|
|
26397
26457
|
await run(path, ["--version"]);
|
|
26398
|
-
|
|
26399
|
-
} catch {
|
|
26400
|
-
|
|
26458
|
+
reason = null;
|
|
26459
|
+
} catch (error) {
|
|
26460
|
+
reason = `does not answer --version: ${errorMessage(error)}`;
|
|
26401
26461
|
}
|
|
26402
26462
|
}
|
|
26403
|
-
this.answered.set(path,
|
|
26404
|
-
return
|
|
26463
|
+
this.answered.set(path, reason);
|
|
26464
|
+
return reason;
|
|
26405
26465
|
}
|
|
26406
26466
|
}
|
|
26407
26467
|
|
|
@@ -26659,15 +26719,36 @@ function filledTemplate2(where, message, godotVersion) {
|
|
|
26659
26719
|
].join(`
|
|
26660
26720
|
`);
|
|
26661
26721
|
}
|
|
26662
|
-
|
|
26722
|
+
|
|
26723
|
+
class DefectsSeen {
|
|
26724
|
+
counts = new Map;
|
|
26725
|
+
before(where, message) {
|
|
26726
|
+
const signature = defectSignature2(where, message);
|
|
26727
|
+
const seen = this.counts.get(signature) ?? 0;
|
|
26728
|
+
this.counts.set(signature, seen + 1);
|
|
26729
|
+
return seen;
|
|
26730
|
+
}
|
|
26731
|
+
}
|
|
26732
|
+
function defectReport2(where, error, context = {}) {
|
|
26733
|
+
const { godotVersion, seenBefore = 0 } = context;
|
|
26663
26734
|
const message = errorMessage(error);
|
|
26664
26735
|
const title = `Defect ${defectSignature2(where, message)}: ${where}`;
|
|
26665
26736
|
const url = `${NEW_ISSUE2}?template=bug_report.md&title=${encodeURIComponent(title)}` + `&body=${encodeURIComponent(filledTemplate2(where, message, godotVersion))}`;
|
|
26666
26737
|
const width = Math.max(...facts2(where, message, godotVersion).map(([label]) => (label ?? "").length));
|
|
26667
26738
|
return [
|
|
26668
|
-
"gdharness failed in a way it does not model.
|
|
26669
|
-
"
|
|
26670
|
-
"
|
|
26739
|
+
"gdharness failed in a way it does not model. Whatever went wrong underneath, it reached you",
|
|
26740
|
+
"as a crash rather than as a refusal naming what would have worked, and that much is a defect",
|
|
26741
|
+
"in gdharness however the rest of it turns out.",
|
|
26742
|
+
"",
|
|
26743
|
+
...seenBefore === 0 ? [
|
|
26744
|
+
"Send the call once more before anything else. Some of what reaches this line is a file",
|
|
26745
|
+
"another process had open, or a connection that dropped, and those are gone by the next",
|
|
26746
|
+
"attempt. If the same Signature comes back, it is not one of those, and repeating it",
|
|
26747
|
+
"further only spends turns."
|
|
26748
|
+
] : [
|
|
26749
|
+
`You have had this exact failure ${seenBefore === 1 ? "once" : `${seenBefore} times`} in this session already, so it is a`,
|
|
26750
|
+
"state rather than a passing one, and sending the call again will not change it."
|
|
26751
|
+
],
|
|
26671
26752
|
"",
|
|
26672
26753
|
...facts2(where, message, godotVersion).map(([label, value]) => ` ${(label ?? "").padEnd(width)} ${value ?? ""}`),
|
|
26673
26754
|
"",
|
|
@@ -27735,6 +27816,11 @@ function settingKeys(content) {
|
|
|
27735
27816
|
return named;
|
|
27736
27817
|
}
|
|
27737
27818
|
function settingsDroppedReport(before, after) {
|
|
27819
|
+
if (before === null || after === null) {
|
|
27820
|
+
return {
|
|
27821
|
+
settingsNote: `project.godot could not be read ${before === null ? "before" : "after"} this, so nothing can` + " be said about what the editor saved away. Opening a project rewrites the file and drops any" + " key sitting at its own default, and this is the only thing that would have named them." + " Read the file yourself if a setting was pinned at a default on purpose."
|
|
27822
|
+
};
|
|
27823
|
+
}
|
|
27738
27824
|
const dropped = [...before].filter(([key]) => !after.has(key));
|
|
27739
27825
|
if (dropped.length === 0) {
|
|
27740
27826
|
return {};
|
|
@@ -27848,16 +27934,55 @@ function setupResourceHandlers(mcp, getProjectPath) {
|
|
|
27848
27934
|
});
|
|
27849
27935
|
}
|
|
27850
27936
|
|
|
27937
|
+
// src/restart-note.ts
|
|
27938
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync8, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
27939
|
+
import { dirname as dirname3, join as join8 } from "node:path";
|
|
27940
|
+
function restartNotePath(projectPath) {
|
|
27941
|
+
return join8(projectPath, ".godot", "gdharness-restart.json");
|
|
27942
|
+
}
|
|
27943
|
+
function noteRestartBegun(note) {
|
|
27944
|
+
const path = restartNotePath(note.projectPath);
|
|
27945
|
+
try {
|
|
27946
|
+
mkdirSync3(dirname3(path), { recursive: true });
|
|
27947
|
+
writeFileSync4(path, `${JSON.stringify(note, null, 2)}
|
|
27948
|
+
`, "utf8");
|
|
27949
|
+
} catch (error) {
|
|
27950
|
+
console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
|
|
27951
|
+
}
|
|
27952
|
+
}
|
|
27953
|
+
function restartOwed(projectPath) {
|
|
27954
|
+
try {
|
|
27955
|
+
const parsed = JSON.parse(readFileSync8(restartNotePath(projectPath), "utf8"));
|
|
27956
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
27957
|
+
return null;
|
|
27958
|
+
}
|
|
27959
|
+
const said = parsed;
|
|
27960
|
+
if (typeof said.quitAt !== "string" || typeof said.byPid !== "number" || typeof said.ports !== "object") {
|
|
27961
|
+
return null;
|
|
27962
|
+
}
|
|
27963
|
+
return said;
|
|
27964
|
+
} catch {
|
|
27965
|
+
return null;
|
|
27966
|
+
}
|
|
27967
|
+
}
|
|
27968
|
+
function restartSettled(projectPath) {
|
|
27969
|
+
try {
|
|
27970
|
+
rmSync3(restartNotePath(projectPath), { force: true });
|
|
27971
|
+
} catch (error) {
|
|
27972
|
+
console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
|
|
27973
|
+
}
|
|
27974
|
+
}
|
|
27975
|
+
|
|
27851
27976
|
// src/run-record.ts
|
|
27852
27977
|
import { execFileSync } from "node:child_process";
|
|
27853
|
-
import { mkdirSync as
|
|
27854
|
-
import { basename, join as
|
|
27978
|
+
import { mkdirSync as mkdirSync4, openSync, readdirSync as readdirSync5, readFileSync as readFileSync10, rmSync as rmSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
27979
|
+
import { basename, join as join10 } from "node:path";
|
|
27855
27980
|
|
|
27856
27981
|
// src/runtime-client.ts
|
|
27857
|
-
import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as
|
|
27982
|
+
import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync9, unlinkSync } from "node:fs";
|
|
27858
27983
|
import { createConnection as createConnection3 } from "node:net";
|
|
27859
27984
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
27860
|
-
import { join as
|
|
27985
|
+
import { join as join9, resolve as resolve4 } from "node:path";
|
|
27861
27986
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
27862
27987
|
|
|
27863
27988
|
// src/tool-args.ts
|
|
@@ -27925,13 +28050,13 @@ function runtimeDirectory(variables = process.env) {
|
|
|
27925
28050
|
return explicit;
|
|
27926
28051
|
}
|
|
27927
28052
|
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
27928
|
-
return
|
|
28053
|
+
return join9(perUser ?? tmpdir3(), "gdharness");
|
|
27929
28054
|
}
|
|
27930
28055
|
function runtimeDirectories(variables = process.env) {
|
|
27931
28056
|
const candidates = [runtimeDirectory(variables)];
|
|
27932
28057
|
const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
|
|
27933
28058
|
for (const base of fallbacks) {
|
|
27934
|
-
candidates.push(
|
|
28059
|
+
candidates.push(join9(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
|
|
27935
28060
|
}
|
|
27936
28061
|
return [...new Set(candidates.map((path) => resolve4(path)))];
|
|
27937
28062
|
}
|
|
@@ -27946,7 +28071,7 @@ function processAlive(pid) {
|
|
|
27946
28071
|
function parseAnnouncement(file, pid) {
|
|
27947
28072
|
let fields;
|
|
27948
28073
|
try {
|
|
27949
|
-
fields = asParams(JSON.parse(
|
|
28074
|
+
fields = asParams(JSON.parse(readFileSync9(file, "utf8")));
|
|
27950
28075
|
} catch {
|
|
27951
28076
|
return { kind: "rubbish" };
|
|
27952
28077
|
}
|
|
@@ -27984,6 +28109,31 @@ function runtimesAnnounced(directories = runtimeDirectories()) {
|
|
|
27984
28109
|
function discoverRuntimes(directories = runtimeDirectories()) {
|
|
27985
28110
|
return runtimesAnnounced(directories).running;
|
|
27986
28111
|
}
|
|
28112
|
+
var GONE_WINDOW_MS = 120000;
|
|
28113
|
+
var GONE_KEPT = 32;
|
|
28114
|
+
var gone = [];
|
|
28115
|
+
function wentAway(file, pid) {
|
|
28116
|
+
let project = "";
|
|
28117
|
+
try {
|
|
28118
|
+
const fields = asParams(JSON.parse(readFileSync9(file, "utf8")));
|
|
28119
|
+
project = readString(readParams(fields, "project") ?? {}, "path") ?? "";
|
|
28120
|
+
} catch {
|
|
28121
|
+
return;
|
|
28122
|
+
}
|
|
28123
|
+
if (project === "") {
|
|
28124
|
+
return;
|
|
28125
|
+
}
|
|
28126
|
+
gone.unshift({ pid, project, noticedAt: Date.now() });
|
|
28127
|
+
gone.length = Math.min(gone.length, GONE_KEPT);
|
|
28128
|
+
}
|
|
28129
|
+
function announcedAndGone(projectPath) {
|
|
28130
|
+
if (projectPath === undefined) {
|
|
28131
|
+
return [];
|
|
28132
|
+
}
|
|
28133
|
+
const since = Date.now() - GONE_WINDOW_MS;
|
|
28134
|
+
const wanted = resolve4(projectPath);
|
|
28135
|
+
return gone.filter((one) => one.noticedAt >= since && resolve4(one.project) === wanted);
|
|
28136
|
+
}
|
|
27987
28137
|
function announcedIn(directory) {
|
|
27988
28138
|
if (!existsSync7(directory)) {
|
|
27989
28139
|
return [];
|
|
@@ -27994,10 +28144,14 @@ function announcedIn(directory) {
|
|
|
27994
28144
|
if (!match) {
|
|
27995
28145
|
continue;
|
|
27996
28146
|
}
|
|
27997
|
-
const file =
|
|
28147
|
+
const file = join9(directory, entry);
|
|
27998
28148
|
const pid = Number.parseInt(match[1] ?? "", 10);
|
|
27999
|
-
const
|
|
28149
|
+
const alive = processAlive(pid);
|
|
28150
|
+
const announced = alive ? parseAnnouncement(file, pid) : { kind: "rubbish" };
|
|
28000
28151
|
if (announced.kind === "rubbish") {
|
|
28152
|
+
if (!alive) {
|
|
28153
|
+
wentAway(file, pid);
|
|
28154
|
+
}
|
|
28001
28155
|
try {
|
|
28002
28156
|
unlinkSync(file);
|
|
28003
28157
|
} catch {}
|
|
@@ -28038,6 +28192,16 @@ function chooseRuntime(endpoints, projectPath, unspoken = []) {
|
|
|
28038
28192
|
if (unspoken.length > 0) {
|
|
28039
28193
|
return { problem: tooNew(unspoken) };
|
|
28040
28194
|
}
|
|
28195
|
+
const went = announcedAndGone(projectPath);
|
|
28196
|
+
const first = went[0];
|
|
28197
|
+
if (first !== undefined) {
|
|
28198
|
+
const ago = Math.max(1, Math.round((Date.now() - first.noticedAt) / 1000));
|
|
28199
|
+
const others = went.length - 1;
|
|
28200
|
+
const earlier = others === 0 ? "" : ` ${others} earlier ${others === 1 ? "one" : "ones"} went the same way.`;
|
|
28201
|
+
return {
|
|
28202
|
+
problem: `A game with the runtime addon announced itself and its process is gone: pid ${first.pid}` + `${first.project === "" ? "" : ` for ${first.project}`}, noticed ${ago}s ago.${earlier}` + " It quit or was ended rather than never starting, so editor_output has what it printed" + " on the way, including whatever it broke on. Start another with editor_run once you have read it."
|
|
28203
|
+
};
|
|
28204
|
+
}
|
|
28041
28205
|
return {
|
|
28042
28206
|
problem: "No game with the runtime addon is running. Start one with editor_run, or play the project from the editor with the addon enabled."
|
|
28043
28207
|
};
|
|
@@ -28155,33 +28319,33 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
|
|
|
28155
28319
|
|
|
28156
28320
|
// src/run-record.ts
|
|
28157
28321
|
function runsDirectory() {
|
|
28158
|
-
return
|
|
28322
|
+
return join10(runtimeDirectory(), "runs");
|
|
28159
28323
|
}
|
|
28160
28324
|
function recordPath() {
|
|
28161
|
-
return
|
|
28325
|
+
return join10(runsDirectory(), "run.json");
|
|
28162
28326
|
}
|
|
28163
28327
|
function recordPaths() {
|
|
28164
|
-
return runtimeDirectories().map((directory) =>
|
|
28328
|
+
return runtimeDirectories().map((directory) => join10(directory, "runs", "run.json"));
|
|
28165
28329
|
}
|
|
28166
28330
|
function openTranscript(startedAt) {
|
|
28167
|
-
|
|
28168
|
-
const path =
|
|
28331
|
+
mkdirSync4(runsDirectory(), { recursive: true });
|
|
28332
|
+
const path = join10(runsDirectory(), `run-${startedAt}.log`);
|
|
28169
28333
|
return { path, fd: openSync(path, "a") };
|
|
28170
28334
|
}
|
|
28171
28335
|
function writeRunRecord(record) {
|
|
28172
|
-
|
|
28173
|
-
|
|
28336
|
+
mkdirSync4(runsDirectory(), { recursive: true });
|
|
28337
|
+
writeFileSync5(recordPath(), JSON.stringify(record, null, 2), "utf8");
|
|
28174
28338
|
}
|
|
28175
28339
|
function editorNotePath() {
|
|
28176
|
-
return
|
|
28340
|
+
return join10(runsDirectory(), "editor-run.json");
|
|
28177
28341
|
}
|
|
28178
28342
|
function writeEditorRunNote(note) {
|
|
28179
|
-
|
|
28180
|
-
|
|
28343
|
+
mkdirSync4(runsDirectory(), { recursive: true });
|
|
28344
|
+
writeFileSync5(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
|
|
28181
28345
|
}
|
|
28182
28346
|
function readEditorRunNote() {
|
|
28183
28347
|
for (const directory of runtimeDirectories()) {
|
|
28184
|
-
const note = editorNoteAt(
|
|
28348
|
+
const note = editorNoteAt(join10(directory, "runs", "editor-run.json"));
|
|
28185
28349
|
if (note !== null) {
|
|
28186
28350
|
return note;
|
|
28187
28351
|
}
|
|
@@ -28191,7 +28355,7 @@ function readEditorRunNote() {
|
|
|
28191
28355
|
function editorNoteAt(path) {
|
|
28192
28356
|
let parsed;
|
|
28193
28357
|
try {
|
|
28194
|
-
parsed = JSON.parse(
|
|
28358
|
+
parsed = JSON.parse(readFileSync10(path, "utf8"));
|
|
28195
28359
|
} catch {
|
|
28196
28360
|
return null;
|
|
28197
28361
|
}
|
|
@@ -28214,7 +28378,7 @@ function recordRunEnded(pid, exitCode) {
|
|
|
28214
28378
|
return;
|
|
28215
28379
|
}
|
|
28216
28380
|
try {
|
|
28217
|
-
|
|
28381
|
+
writeFileSync5(path, JSON.stringify({ ...record, exitCode }, null, 2), "utf8");
|
|
28218
28382
|
} catch {}
|
|
28219
28383
|
}
|
|
28220
28384
|
function readRunRecord() {
|
|
@@ -28229,7 +28393,7 @@ function readRunRecord() {
|
|
|
28229
28393
|
function recordAt(path) {
|
|
28230
28394
|
let parsed;
|
|
28231
28395
|
try {
|
|
28232
|
-
parsed = JSON.parse(
|
|
28396
|
+
parsed = JSON.parse(readFileSync10(path, "utf8"));
|
|
28233
28397
|
} catch {
|
|
28234
28398
|
return null;
|
|
28235
28399
|
}
|
|
@@ -28256,23 +28420,32 @@ function recordAt(path) {
|
|
|
28256
28420
|
}
|
|
28257
28421
|
function runningAs(pid) {
|
|
28258
28422
|
if (process.platform === "win32") {
|
|
28259
|
-
const
|
|
28260
|
-
if (
|
|
28261
|
-
|
|
28423
|
+
const answer = windowsCommandLine(pid);
|
|
28424
|
+
if (answer !== null) {
|
|
28425
|
+
const [, ...rest] = answer.split(`
|
|
28426
|
+
`);
|
|
28427
|
+
const line = rest.join(`
|
|
28428
|
+
`).trim();
|
|
28429
|
+
const began = startedAt(pid, answer);
|
|
28430
|
+
if (line !== "") {
|
|
28431
|
+
return { kind: "commandLine", text: line, ...began === null ? {} : { startedAt: began } };
|
|
28432
|
+
}
|
|
28262
28433
|
}
|
|
28263
28434
|
const image = windowsImage(pid);
|
|
28264
28435
|
return image === null ? null : { kind: "image", text: image };
|
|
28265
28436
|
}
|
|
28266
28437
|
try {
|
|
28438
|
+
const began = startedAt(pid, null);
|
|
28439
|
+
const when = began === null ? {} : { startedAt: began };
|
|
28267
28440
|
if (process.platform === "linux") {
|
|
28268
|
-
const raw =
|
|
28269
|
-
return raw === "" ? null : { kind: "commandLine", text: raw };
|
|
28441
|
+
const raw = readFileSync10(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
28442
|
+
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
28270
28443
|
}
|
|
28271
28444
|
const args = execFileSync("ps", ["-p", String(pid), "-o", "args="], {
|
|
28272
28445
|
encoding: "utf8",
|
|
28273
28446
|
timeout: 15000
|
|
28274
28447
|
}).trim();
|
|
28275
|
-
return args === "" ? null : { kind: "commandLine", text: args };
|
|
28448
|
+
return args === "" ? null : { kind: "commandLine", text: args, ...when };
|
|
28276
28449
|
} catch {
|
|
28277
28450
|
return null;
|
|
28278
28451
|
}
|
|
@@ -28311,13 +28484,30 @@ function windowsCommandLine(pid) {
|
|
|
28311
28484
|
"-NoProfile",
|
|
28312
28485
|
"-NonInteractive",
|
|
28313
28486
|
"-Command",
|
|
28314
|
-
|
|
28487
|
+
`$p = Get-CimInstance Win32_Process -Filter "ProcessId=${pid}"; if ($p) { $p.CreationDate.ToUniversalTime().ToString("o"); $p.CommandLine }`
|
|
28315
28488
|
], { encoding: "utf8", timeout: 15000, windowsHide: true }).trim();
|
|
28316
28489
|
return answer === "" ? null : answer;
|
|
28317
28490
|
} catch {
|
|
28318
28491
|
return null;
|
|
28319
28492
|
}
|
|
28320
28493
|
}
|
|
28494
|
+
function startedAt(pid, windowsAnswer) {
|
|
28495
|
+
if (process.platform === "win32") {
|
|
28496
|
+
const when = Date.parse(windowsAnswer?.split(`
|
|
28497
|
+
`)[0]?.trim() ?? "");
|
|
28498
|
+
return Number.isNaN(when) ? null : when;
|
|
28499
|
+
}
|
|
28500
|
+
try {
|
|
28501
|
+
const said = execFileSync("ps", ["-p", String(pid), "-o", "lstart="], {
|
|
28502
|
+
encoding: "utf8",
|
|
28503
|
+
timeout: 15000
|
|
28504
|
+
}).trim();
|
|
28505
|
+
const when = Date.parse(said);
|
|
28506
|
+
return said === "" || Number.isNaN(when) ? null : when;
|
|
28507
|
+
} catch {
|
|
28508
|
+
return null;
|
|
28509
|
+
}
|
|
28510
|
+
}
|
|
28321
28511
|
function windowsImage(pid) {
|
|
28322
28512
|
try {
|
|
28323
28513
|
const csv = execFileSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
|
|
@@ -28337,10 +28527,14 @@ function couldStillBeTheRecordedRun(record) {
|
|
|
28337
28527
|
return judgeRun(record, runningAs(record.pid), "possible");
|
|
28338
28528
|
}
|
|
28339
28529
|
var AN_EDITOR = /(?:^|\s)(?:-e|--editor)(?:\s|$)/;
|
|
28530
|
+
var SAME_RUN_WINDOW_MS = 1e4;
|
|
28340
28531
|
function judgeRun(record, running, needed) {
|
|
28341
28532
|
if (running === null) {
|
|
28342
28533
|
return false;
|
|
28343
28534
|
}
|
|
28535
|
+
if (running.startedAt !== undefined && running.startedAt - record.startedAt > SAME_RUN_WINDOW_MS) {
|
|
28536
|
+
return false;
|
|
28537
|
+
}
|
|
28344
28538
|
const engine = record.command === undefined ? null : basename(record.command);
|
|
28345
28539
|
const said = process.platform === "win32" ? running.text.toLowerCase() : running.text;
|
|
28346
28540
|
const wanted = engine === null || process.platform !== "win32" ? engine : engine.toLowerCase();
|
|
@@ -28366,7 +28560,7 @@ function clearRunRecord(ours = () => true) {
|
|
|
28366
28560
|
continue;
|
|
28367
28561
|
}
|
|
28368
28562
|
try {
|
|
28369
|
-
|
|
28563
|
+
rmSync4(path);
|
|
28370
28564
|
} catch {}
|
|
28371
28565
|
}
|
|
28372
28566
|
}
|
|
@@ -28390,19 +28584,19 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
28390
28584
|
if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
|
|
28391
28585
|
continue;
|
|
28392
28586
|
}
|
|
28393
|
-
const path =
|
|
28587
|
+
const path = join10(directory, entry);
|
|
28394
28588
|
if (path === keep || ageMs(path, now) < keepMs) {
|
|
28395
28589
|
continue;
|
|
28396
28590
|
}
|
|
28397
28591
|
try {
|
|
28398
|
-
|
|
28592
|
+
rmSync4(path);
|
|
28399
28593
|
} catch {}
|
|
28400
28594
|
}
|
|
28401
28595
|
}
|
|
28402
28596
|
|
|
28403
28597
|
// src/setup.ts
|
|
28404
|
-
import { cpSync, existsSync as existsSync8, mkdirSync as
|
|
28405
|
-
import { dirname as
|
|
28598
|
+
import { cpSync, existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync11, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "node:fs";
|
|
28599
|
+
import { dirname as dirname4, join as join11 } from "node:path";
|
|
28406
28600
|
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
28407
28601
|
var RUNTIME_AUTOLOAD = {
|
|
28408
28602
|
name: "GdharnessRuntime",
|
|
@@ -28410,11 +28604,11 @@ var RUNTIME_AUTOLOAD = {
|
|
|
28410
28604
|
};
|
|
28411
28605
|
var VERSION_MARKER = ".gdharness-version";
|
|
28412
28606
|
function installedAddonVersion(projectPath) {
|
|
28413
|
-
const marker =
|
|
28607
|
+
const marker = join11(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
28414
28608
|
if (!existsSync8(marker)) {
|
|
28415
28609
|
return null;
|
|
28416
28610
|
}
|
|
28417
|
-
const said =
|
|
28611
|
+
const said = readFileSync11(marker, "utf8").trim();
|
|
28418
28612
|
return said === "" ? null : said;
|
|
28419
28613
|
}
|
|
28420
28614
|
|
|
@@ -28751,7 +28945,7 @@ var TOOL_SPECS = [
|
|
|
28751
28945
|
},
|
|
28752
28946
|
{
|
|
28753
28947
|
name: "project_test",
|
|
28754
|
-
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.",
|
|
28948
|
+
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. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. 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.",
|
|
28755
28949
|
parameters: {
|
|
28756
28950
|
projectPath: PROJECT_PATH,
|
|
28757
28951
|
path: {
|
|
@@ -29018,7 +29212,7 @@ var TOOL_SPECS = [
|
|
|
29018
29212
|
},
|
|
29019
29213
|
{
|
|
29020
29214
|
name: "editor_launch",
|
|
29021
|
-
description: "Opens the Godot editor on a project, in a window on this machine, or restarts the one already connected. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect; it saves open scenes on the way out and answers with the version that came back. Only an editor with a window can be restarted, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one.",
|
|
29215
|
+
description: "Opens the Godot editor on a project, in a window on this machine, or restarts the one already connected. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect; it saves open scenes on the way out and answers with the version that came back. Only an editor with a window can be restarted, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so under restartInterrupted in editor_status rather than waiting for an editor that is not coming, and open finishes it, saying so under finishesRestart.",
|
|
29022
29216
|
parameters: { projectPath: { ...PROJECT_PATH, ops: ["open"] } },
|
|
29023
29217
|
requires: [],
|
|
29024
29218
|
operations: {
|
|
@@ -29080,7 +29274,7 @@ var TOOL_SPECS = [
|
|
|
29080
29274
|
},
|
|
29081
29275
|
{
|
|
29082
29276
|
name: "editor_output",
|
|
29083
|
-
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:
|
|
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.",
|
|
29084
29278
|
parameters: {
|
|
29085
29279
|
severity: {
|
|
29086
29280
|
type: "string",
|
|
@@ -29089,7 +29283,7 @@ var TOOL_SPECS = [
|
|
|
29089
29283
|
},
|
|
29090
29284
|
sinceLastCall: {
|
|
29091
29285
|
type: "boolean",
|
|
29092
|
-
description: "Only entries
|
|
29286
|
+
description: "Only entries this has not already answered with at this severity. Default false. Measured per severity floor and not across them, so polling for errors leaves the rest of the output for a later read rather than consuming it, and a read with contains is a search over the whole run and consumes nothing."
|
|
29093
29287
|
},
|
|
29094
29288
|
cpu: {
|
|
29095
29289
|
type: "boolean",
|
|
@@ -29108,7 +29302,7 @@ var TOOL_SPECS = [
|
|
|
29108
29302
|
},
|
|
29109
29303
|
{
|
|
29110
29304
|
name: "editor_status",
|
|
29111
|
-
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. 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. 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: installing
|
|
29305
|
+
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. 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.',
|
|
29112
29306
|
parameters: {},
|
|
29113
29307
|
requires: []
|
|
29114
29308
|
},
|
|
@@ -29525,7 +29719,7 @@ function buildToolDefinitions() {
|
|
|
29525
29719
|
var UPDATE_NOTICE_EVERY = 500;
|
|
29526
29720
|
var FEEDBACK_NOTICE_EVERY = 250;
|
|
29527
29721
|
var run4 = promisify4(execFile4);
|
|
29528
|
-
var __dirname2 =
|
|
29722
|
+
var __dirname2 = dirname5(fileURLToPath2(import.meta.url));
|
|
29529
29723
|
var EDITOR_RESTART_TIMEOUT_MS = 90000;
|
|
29530
29724
|
var SLOWEST_FRAME_RATE = 20;
|
|
29531
29725
|
function patienceForFrames(frames, atLeast) {
|
|
@@ -29538,12 +29732,18 @@ function stillRunning(run) {
|
|
|
29538
29732
|
if (!run.throughEditor && run.process === null && run.pid !== null) {
|
|
29539
29733
|
return alive(run.pid);
|
|
29540
29734
|
}
|
|
29735
|
+
if (run.announcedPid !== undefined && !alive(run.announcedPid)) {
|
|
29736
|
+
return false;
|
|
29737
|
+
}
|
|
29541
29738
|
return true;
|
|
29542
29739
|
}
|
|
29543
29740
|
function runIsUp(run, editorSays) {
|
|
29544
29741
|
if (run?.throughEditor !== true) {
|
|
29545
29742
|
return stillRunning(run);
|
|
29546
29743
|
}
|
|
29744
|
+
if (run.announcedPid !== undefined && !alive(run.announcedPid)) {
|
|
29745
|
+
return false;
|
|
29746
|
+
}
|
|
29547
29747
|
return editorSays ?? stillRunning(run);
|
|
29548
29748
|
}
|
|
29549
29749
|
function runtimeVerdict(endpoint, after) {
|
|
@@ -29715,7 +29915,7 @@ function aboveTheRunner(entry) {
|
|
|
29715
29915
|
}
|
|
29716
29916
|
function elsewhereIn(projectPath, asked) {
|
|
29717
29917
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
29718
|
-
if (wanted === "" || existsSync9(
|
|
29918
|
+
if (wanted === "" || existsSync9(join12(projectPath, wanted))) {
|
|
29719
29919
|
return "";
|
|
29720
29920
|
}
|
|
29721
29921
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
@@ -29729,11 +29929,11 @@ function elsewhereIn(projectPath, asked) {
|
|
|
29729
29929
|
return near.length === 0 ? "" : ` The project does have ${there}.`;
|
|
29730
29930
|
}
|
|
29731
29931
|
function hasMainScene(projectFile) {
|
|
29732
|
-
const scene = parseProjectGodot(
|
|
29932
|
+
const scene = parseProjectGodot(readFileSync12(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
29733
29933
|
return typeof scene === "string" && scene !== "";
|
|
29734
29934
|
}
|
|
29735
29935
|
function editorPlaysHeadless(projectFile) {
|
|
29736
|
-
const runArgs = parseProjectGodot(
|
|
29936
|
+
const runArgs = parseProjectGodot(readFileSync12(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
29737
29937
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
29738
29938
|
}
|
|
29739
29939
|
function camelCased(params) {
|
|
@@ -29749,7 +29949,7 @@ function camelCased(params) {
|
|
|
29749
29949
|
class GodotServer {
|
|
29750
29950
|
mcp;
|
|
29751
29951
|
locator = new GodotLocator;
|
|
29752
|
-
operationsScript =
|
|
29952
|
+
operationsScript = join12(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
29753
29953
|
godotBridge;
|
|
29754
29954
|
tools = buildToolDefinitions();
|
|
29755
29955
|
activeProcess = null;
|
|
@@ -29757,6 +29957,8 @@ class GodotServer {
|
|
|
29757
29957
|
noticedUpdate = false;
|
|
29758
29958
|
callsSinceNotice = 0;
|
|
29759
29959
|
callsSinceFeedback = 0;
|
|
29960
|
+
defectsSeen = new DefectsSeen;
|
|
29961
|
+
lastGodotVersion = null;
|
|
29760
29962
|
lspClient = null;
|
|
29761
29963
|
dapClient = null;
|
|
29762
29964
|
bridgeStartupError = null;
|
|
@@ -29801,11 +30003,11 @@ class GodotServer {
|
|
|
29801
30003
|
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
29802
30004
|
}
|
|
29803
30005
|
async run() {
|
|
29804
|
-
const
|
|
29805
|
-
if (
|
|
29806
|
-
console.error(
|
|
30006
|
+
const located = await this.locator.find();
|
|
30007
|
+
if (!located.ok) {
|
|
30008
|
+
console.error(`[SERVER] ${GodotLocator.refusal(located).message} Until that is fixed every tool that runs the engine will say so.`);
|
|
29807
30009
|
} else {
|
|
29808
|
-
console.error(`[SERVER] Using Godot at: ${
|
|
30010
|
+
console.error(`[SERVER] Using Godot at: ${located.path}`);
|
|
29809
30011
|
}
|
|
29810
30012
|
const transport = new StdioServerTransport;
|
|
29811
30013
|
await this.mcp.server.connect(transport);
|
|
@@ -30005,7 +30207,19 @@ class GodotServer {
|
|
|
30005
30207
|
}
|
|
30006
30208
|
const where = op === "" ? tool : `${tool} op=${op}`;
|
|
30007
30209
|
console.error(`[SERVER] Unmodelled failure in ${where}:`, error);
|
|
30008
|
-
|
|
30210
|
+
const seenBefore = this.defectsSeen.before(where, errorMessage(error));
|
|
30211
|
+
return {
|
|
30212
|
+
content: [
|
|
30213
|
+
{
|
|
30214
|
+
type: "text",
|
|
30215
|
+
text: defectReport2(where, error, {
|
|
30216
|
+
seenBefore,
|
|
30217
|
+
godotVersion: this.lastGodotVersion ?? undefined
|
|
30218
|
+
})
|
|
30219
|
+
}
|
|
30220
|
+
],
|
|
30221
|
+
isError: true
|
|
30222
|
+
};
|
|
30009
30223
|
}
|
|
30010
30224
|
}
|
|
30011
30225
|
withUpdateNotice(answer) {
|
|
@@ -30366,21 +30580,19 @@ class GodotServer {
|
|
|
30366
30580
|
}
|
|
30367
30581
|
}
|
|
30368
30582
|
async engine() {
|
|
30369
|
-
const
|
|
30370
|
-
if (
|
|
30371
|
-
|
|
30372
|
-
|
|
30373
|
-
response: this.createErrorResponse("No Godot executable found.", GodotLocator.ADVICE)
|
|
30374
|
-
};
|
|
30583
|
+
const located = await this.locator.find();
|
|
30584
|
+
if (!located.ok) {
|
|
30585
|
+
const refusal = GodotLocator.refusal(located);
|
|
30586
|
+
return { ok: false, response: this.createErrorResponse(refusal.message, [...refusal.advice]) };
|
|
30375
30587
|
}
|
|
30376
|
-
return { ok: true, value:
|
|
30588
|
+
return { ok: true, value: located.path };
|
|
30377
30589
|
}
|
|
30378
30590
|
project(args) {
|
|
30379
30591
|
const path = readNonEmptyString(args, "projectPath");
|
|
30380
30592
|
if (path === undefined) {
|
|
30381
30593
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
30382
30594
|
}
|
|
30383
|
-
const file =
|
|
30595
|
+
const file = join12(path, "project.godot");
|
|
30384
30596
|
if (!existsSync9(file)) {
|
|
30385
30597
|
return {
|
|
30386
30598
|
ok: false,
|
|
@@ -30522,7 +30734,7 @@ class GodotServer {
|
|
|
30522
30734
|
}
|
|
30523
30735
|
const engine = await this.engine();
|
|
30524
30736
|
if (!engine.ok) {
|
|
30525
|
-
return
|
|
30737
|
+
return engine.response;
|
|
30526
30738
|
}
|
|
30527
30739
|
const projectPath = project.value.path;
|
|
30528
30740
|
const before = scriptsWithoutUid(projectPath);
|
|
@@ -30545,7 +30757,11 @@ class GodotServer {
|
|
|
30545
30757
|
async operation(operation, params, projectPath) {
|
|
30546
30758
|
const engine = await this.engine();
|
|
30547
30759
|
if (!engine.ok) {
|
|
30548
|
-
return {
|
|
30760
|
+
return {
|
|
30761
|
+
ok: false,
|
|
30762
|
+
message: engine.response.content.map((block) => block.text).join(" "),
|
|
30763
|
+
messages: []
|
|
30764
|
+
};
|
|
30549
30765
|
}
|
|
30550
30766
|
this.logDebug(`Running ${operation} in ${projectPath}: ${JSON.stringify(params)}`);
|
|
30551
30767
|
return await runOperation({ godotPath: engine.value, script: this.operationsScript, debug: GODOT_DEBUG_MODE_DEFAULT }, operation, params, projectPath);
|
|
@@ -30577,7 +30793,7 @@ class GodotServer {
|
|
|
30577
30793
|
if (!engine.ok) {
|
|
30578
30794
|
return engine.response;
|
|
30579
30795
|
}
|
|
30580
|
-
const application = parseProjectGodot(
|
|
30796
|
+
const application = parseProjectGodot(readFileSync12(project.value.file, "utf8"))["application"] ?? {};
|
|
30581
30797
|
const name = application["config/name"];
|
|
30582
30798
|
const mainScene = application["run/main_scene"];
|
|
30583
30799
|
const info = {
|
|
@@ -30638,18 +30854,18 @@ class GodotServer {
|
|
|
30638
30854
|
if (!engine.ok) {
|
|
30639
30855
|
return engine.response;
|
|
30640
30856
|
}
|
|
30641
|
-
const folder =
|
|
30857
|
+
const folder = dirname5(output.absolutePath);
|
|
30642
30858
|
try {
|
|
30643
|
-
|
|
30859
|
+
mkdirSync6(folder, { recursive: true });
|
|
30644
30860
|
} catch (error) {
|
|
30645
|
-
return this.createErrorResponse(`Cannot create ${
|
|
30861
|
+
return this.createErrorResponse(`Cannot create ${dirname5(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
30646
30862
|
}
|
|
30647
30863
|
const debug = readBoolean(args, "debug") ?? false;
|
|
30648
|
-
const exportLogs = mkdtempSync2(
|
|
30864
|
+
const exportLogs = mkdtempSync2(join12(tmpdir4(), "gdharness-export-"));
|
|
30649
30865
|
const exportArgs = [
|
|
30650
30866
|
"--headless",
|
|
30651
30867
|
"--log-file",
|
|
30652
|
-
|
|
30868
|
+
join12(exportLogs, "engine.log"),
|
|
30653
30869
|
"--path",
|
|
30654
30870
|
project.value.path,
|
|
30655
30871
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -30707,7 +30923,7 @@ class GodotServer {
|
|
|
30707
30923
|
return contained.response;
|
|
30708
30924
|
}
|
|
30709
30925
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
30710
|
-
if (!existsSync9(
|
|
30926
|
+
if (!existsSync9(join12(project.value.path, runner))) {
|
|
30711
30927
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
30712
30928
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
30713
30929
|
]);
|
|
@@ -30742,7 +30958,7 @@ class GodotServer {
|
|
|
30742
30958
|
];
|
|
30743
30959
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
30744
30960
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
30745
|
-
const userData = mkdtempSync2(
|
|
30961
|
+
const userData = mkdtempSync2(join12(tmpdir4(), "gdharness-tests-"));
|
|
30746
30962
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
30747
30963
|
const hung = await new Promise((resolve) => {
|
|
30748
30964
|
const timer = setTimeout(() => {
|
|
@@ -30758,14 +30974,14 @@ class GodotServer {
|
|
|
30758
30974
|
resolve(false);
|
|
30759
30975
|
});
|
|
30760
30976
|
});
|
|
30761
|
-
const reportsDir =
|
|
30977
|
+
const reportsDir = join12(project.value.path, ".godot", ...ours.split("/"));
|
|
30762
30978
|
let report = null;
|
|
30763
30979
|
let reportProblem = null;
|
|
30764
30980
|
try {
|
|
30765
30981
|
const written = existsSync9(reportsDir) ? readdirSync6(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
30766
30982
|
const newest = written.at(-1);
|
|
30767
30983
|
if (newest !== undefined) {
|
|
30768
|
-
report = parseJUnit(
|
|
30984
|
+
report = parseJUnit(readFileSync12(join12(reportsDir, newest, "results.xml"), "utf8"));
|
|
30769
30985
|
}
|
|
30770
30986
|
} catch (error) {
|
|
30771
30987
|
reportProblem = errorMessage(error);
|
|
@@ -30786,6 +31002,7 @@ class GodotServer {
|
|
|
30786
31002
|
const said = printed.map((entry) => entry.text);
|
|
30787
31003
|
const nothingRan = hung ? null : whyNoReport(said, asked);
|
|
30788
31004
|
const verdict = hung ? `hung: killed after ${timeoutMs} ms` : nothingRan ?? verdicts[exitCode ?? -1] ?? `exit ${exitCode ?? "unknown"}`;
|
|
31005
|
+
const savesNote = savesStayPut() && nothingRan === null ? SAVES_NOT_MOVED_NOTE : undefined;
|
|
30789
31006
|
if (report === null) {
|
|
30790
31007
|
const note = nothingRan === null ? `The test run wrote no report (${verdict}${reportProblem ? `; ${reportProblem}` : ""}).` : `No tests ran: ${verdict}.${elsewhereIn(project.value.path, asked)}`;
|
|
30791
31008
|
return {
|
|
@@ -30800,7 +31017,8 @@ class GodotServer {
|
|
|
30800
31017
|
exitCode,
|
|
30801
31018
|
hung,
|
|
30802
31019
|
arguments: cmdArgs,
|
|
30803
|
-
entries: forAnswer(printed.slice(0, 60))
|
|
31020
|
+
entries: forAnswer(printed.slice(0, 60)),
|
|
31021
|
+
savesNote
|
|
30804
31022
|
}, null, 2)
|
|
30805
31023
|
}
|
|
30806
31024
|
],
|
|
@@ -30833,6 +31051,7 @@ class GodotServer {
|
|
|
30833
31051
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
30834
31052
|
orphans: orphans.total > 0 ? orphans.total : undefined,
|
|
30835
31053
|
notRun: notRun > 0 ? notRun : undefined,
|
|
31054
|
+
savesNote,
|
|
30836
31055
|
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." : notRun > 0 ? `This run stopped at the first failure in each suite it failed in, so ${notRun} case${notRun === 1 ? "" : "s"} never ran and count as neither passed nor failed. Leave failFast out to run every case.` : undefined,
|
|
30837
31056
|
suites: unclean.map((suite) => ({
|
|
30838
31057
|
name: suite.name,
|
|
@@ -30855,7 +31074,8 @@ class GodotServer {
|
|
|
30855
31074
|
async godotVersion(godotPath) {
|
|
30856
31075
|
try {
|
|
30857
31076
|
const { stdout } = await run4(godotPath, ["--version"], { timeout: 1e4 });
|
|
30858
|
-
|
|
31077
|
+
this.lastGodotVersion = stdout.trim();
|
|
31078
|
+
return this.lastGodotVersion;
|
|
30859
31079
|
} catch (error) {
|
|
30860
31080
|
this.logDebug(`Godot did not answer --version: ${errorMessage(error)}`);
|
|
30861
31081
|
return null;
|
|
@@ -30901,7 +31121,7 @@ class GodotServer {
|
|
|
30901
31121
|
return null;
|
|
30902
31122
|
}
|
|
30903
31123
|
try {
|
|
30904
|
-
return
|
|
31124
|
+
return readFileSync12(contained.absolutePath, "utf8");
|
|
30905
31125
|
} catch {
|
|
30906
31126
|
return null;
|
|
30907
31127
|
}
|
|
@@ -30943,7 +31163,7 @@ class GodotServer {
|
|
|
30943
31163
|
return null;
|
|
30944
31164
|
}
|
|
30945
31165
|
try {
|
|
30946
|
-
return
|
|
31166
|
+
return readFileSync12(contained.absolutePath, "utf8");
|
|
30947
31167
|
} catch {
|
|
30948
31168
|
return null;
|
|
30949
31169
|
}
|
|
@@ -31013,16 +31233,37 @@ class GodotServer {
|
|
|
31013
31233
|
const status = this.godotBridge.getStatus();
|
|
31014
31234
|
return (status.connected ? status[field] : undefined) ?? fallback;
|
|
31015
31235
|
}
|
|
31236
|
+
restartLeftUnfinished(connected) {
|
|
31237
|
+
if (this.ownProject === null) {
|
|
31238
|
+
return null;
|
|
31239
|
+
}
|
|
31240
|
+
const owed = restartOwed(this.ownProject);
|
|
31241
|
+
if (owed === null) {
|
|
31242
|
+
return null;
|
|
31243
|
+
}
|
|
31244
|
+
if (connected) {
|
|
31245
|
+
restartSettled(this.ownProject);
|
|
31246
|
+
return null;
|
|
31247
|
+
}
|
|
31248
|
+
return owed;
|
|
31249
|
+
}
|
|
31016
31250
|
getEditorStatusPayload() {
|
|
31017
31251
|
const status = this.godotBridge.getStatus();
|
|
31018
31252
|
const isPortConflict = this.bridgeStartupError?.includes("EADDRINUSE") ?? false;
|
|
31019
31253
|
const stale = status.connected && status.addonVersion !== SERVER_VERSION;
|
|
31254
|
+
const unfinished = this.restartLeftUnfinished(status.connected);
|
|
31020
31255
|
return {
|
|
31021
31256
|
...status,
|
|
31022
31257
|
serverVersion: SERVER_VERSION,
|
|
31023
31258
|
addonIsStale: status.connected ? stale : undefined,
|
|
31024
31259
|
bridgeAvailable: this.bridgeStartupError === null,
|
|
31025
|
-
mayYetConnect: status.connected ? undefined : this.anEditorIsStillComing(status.listeningSince),
|
|
31260
|
+
mayYetConnect: status.connected ? undefined : unfinished !== null ? false : this.anEditorIsStillComing(status.listeningSince),
|
|
31261
|
+
restartInterrupted: unfinished === null ? undefined : {
|
|
31262
|
+
quitEditorPid: unfinished.editorPid,
|
|
31263
|
+
quitAt: unfinished.quitAt,
|
|
31264
|
+
byServerPid: unfinished.byPid,
|
|
31265
|
+
note: "A restart begun by a previous gdharness server asked this project's editor to quit, and that server was ended before it could start the editor again, so nothing is coming. editor_launch open starts one, and clears this."
|
|
31266
|
+
},
|
|
31026
31267
|
awaitingLaunchedEditor: status.connected || !this.launchedEditorIsUp() ? undefined : this.launchedEditor?.pid ?? undefined,
|
|
31027
31268
|
...status.connected ? this.whatTheLaunchedEditorDropped() : {},
|
|
31028
31269
|
startupError: this.bridgeStartupError,
|
|
@@ -31058,7 +31299,7 @@ class GodotServer {
|
|
|
31058
31299
|
}
|
|
31059
31300
|
}
|
|
31060
31301
|
async handleEditorStatus() {
|
|
31061
|
-
const
|
|
31302
|
+
const located = await this.locator.find();
|
|
31062
31303
|
const announced = runtimesAnnounced();
|
|
31063
31304
|
const games = await Promise.all(announced.running.map(async (endpoint) => {
|
|
31064
31305
|
const reply = await runtimeRequest(endpoint, "ping", {}, this.runtimeTimeoutMs());
|
|
@@ -31078,8 +31319,9 @@ class GodotServer {
|
|
|
31078
31319
|
debugPort: playing?.debugPort ?? this.godotBridge.getStatus().debugPort
|
|
31079
31320
|
},
|
|
31080
31321
|
godot: {
|
|
31081
|
-
path:
|
|
31082
|
-
version:
|
|
31322
|
+
path: located.ok ? located.path : null,
|
|
31323
|
+
version: located.ok ? await this.godotVersion(located.path) : null,
|
|
31324
|
+
problem: located.ok ? undefined : GodotLocator.refusal(located).message
|
|
31083
31325
|
},
|
|
31084
31326
|
game: {
|
|
31085
31327
|
processActive: runIsUp(this.currentRun(), playing === null ? null : playing.playing),
|
|
@@ -31098,12 +31340,12 @@ class GodotServer {
|
|
|
31098
31340
|
}
|
|
31099
31341
|
settingKeysOf(projectPath) {
|
|
31100
31342
|
if (projectPath === undefined) {
|
|
31101
|
-
return
|
|
31343
|
+
return null;
|
|
31102
31344
|
}
|
|
31103
31345
|
try {
|
|
31104
|
-
return settingKeys(
|
|
31346
|
+
return settingKeys(readFileSync12(join12(projectPath, "project.godot"), "utf8"));
|
|
31105
31347
|
} catch {
|
|
31106
|
-
return
|
|
31348
|
+
return null;
|
|
31107
31349
|
}
|
|
31108
31350
|
}
|
|
31109
31351
|
async handleRestartEditor() {
|
|
@@ -31163,12 +31405,21 @@ class GodotServer {
|
|
|
31163
31405
|
lsp: this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT),
|
|
31164
31406
|
dap: this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT)
|
|
31165
31407
|
};
|
|
31408
|
+
noteRestartBegun({
|
|
31409
|
+
projectPath,
|
|
31410
|
+
editorPid: editorPid ?? null,
|
|
31411
|
+
ports,
|
|
31412
|
+
quitAt: new Date().toISOString(),
|
|
31413
|
+
byPid: process.pid
|
|
31414
|
+
});
|
|
31166
31415
|
const asked = await this.handleViaBridge("quit_editor", {});
|
|
31167
31416
|
if (asked.isError === true) {
|
|
31417
|
+
restartSettled(projectPath);
|
|
31168
31418
|
return asked;
|
|
31169
31419
|
}
|
|
31170
31420
|
await this.waitForBridge(() => !alive(editorPid), Date.now() + EDITOR_RESTART_TIMEOUT_MS);
|
|
31171
31421
|
const opened = await this.openAnEditor(engine.value, projectPath, ports);
|
|
31422
|
+
restartSettled(projectPath);
|
|
31172
31423
|
if (opened.error !== null) {
|
|
31173
31424
|
return this.createErrorResponse(`The editor was asked to go and could not be started again: ${opened.error}`, [`editor_launch opens one on ${projectPath}`]);
|
|
31174
31425
|
}
|
|
@@ -31204,10 +31455,12 @@ class GodotServer {
|
|
|
31204
31455
|
this.logDebug(`Launching Godot editor for project: ${project.value.path}`);
|
|
31205
31456
|
const ports = await this.portsForAnEditor();
|
|
31206
31457
|
const before = this.settingKeysOf(project.value.path);
|
|
31458
|
+
const unfinished = restartOwed(project.value.path);
|
|
31207
31459
|
const opened = await this.openAnEditor(engine.value, project.value.path, ports);
|
|
31208
31460
|
if (opened.error !== null) {
|
|
31209
31461
|
return this.createErrorResponse(`Could not start the editor: ${opened.error}`);
|
|
31210
31462
|
}
|
|
31463
|
+
restartSettled(project.value.path);
|
|
31211
31464
|
this.launchedFrom = { projectPath: project.value.path, before };
|
|
31212
31465
|
return this.jsonTextResponse({
|
|
31213
31466
|
launched: true,
|
|
@@ -31215,7 +31468,8 @@ class GodotServer {
|
|
|
31215
31468
|
projectPath: project.value.path,
|
|
31216
31469
|
lspPort: ports.lsp,
|
|
31217
31470
|
dapPort: ports.dap,
|
|
31218
|
-
|
|
31471
|
+
finishesRestart: unfinished === null ? undefined : `This is the launch half of a restart begun at ${unfinished.quitAt} by server pid ${unfinished.byPid}, which quit editor pid ${unfinished.editorPid ?? "unknown"} and was ended before it could start one.`,
|
|
31472
|
+
settingsNote: before === null ? "project.godot could not be read before this editor was opened, so nothing here will be able to say what the import saved away. Opening a project rewrites the file and drops any key sitting at its own default." : before.size === 0 ? undefined : "Opening a project imports it and saves project.godot, and Godot drops any key sitting at its own default. editor_status names anything lost under settingsDropped once this editor has connected."
|
|
31219
31473
|
});
|
|
31220
31474
|
}
|
|
31221
31475
|
async openAnEditor(engine, projectPath, ports) {
|
|
@@ -31332,13 +31586,17 @@ class GodotServer {
|
|
|
31332
31586
|
return runIsUp(run, editorSays);
|
|
31333
31587
|
}
|
|
31334
31588
|
async runtimeUp(projectPath, before, budgetMs, withArgs) {
|
|
31335
|
-
if (!existsSync9(
|
|
31589
|
+
if (!existsSync9(join12(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
31336
31590
|
return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
|
|
31337
31591
|
}
|
|
31338
31592
|
const endpoint = await announcedSince(projectPath, before, {
|
|
31339
31593
|
budgetMs,
|
|
31340
31594
|
giveUp: () => this.dapClient?.isStopped() === true || !stillRunning(this.currentRun())
|
|
31341
31595
|
});
|
|
31596
|
+
const going = this.currentRun();
|
|
31597
|
+
if (endpoint !== null && going?.throughEditor === true && going.announcedPid === undefined) {
|
|
31598
|
+
going.announcedPid = endpoint.pid;
|
|
31599
|
+
}
|
|
31342
31600
|
return runtimeVerdict(endpoint, {
|
|
31343
31601
|
addon: true,
|
|
31344
31602
|
budgetMs,
|
|
@@ -31448,9 +31706,15 @@ class GodotServer {
|
|
|
31448
31706
|
running.log.record("warning", `This run was not ended here: pid ${running.pid} no longer answers as the run that was recorded, so nothing was signalled. If that process is still the game, end it yourself; if it is not, it belongs to something else.`);
|
|
31449
31707
|
return;
|
|
31450
31708
|
}
|
|
31709
|
+
const signalled = runningAs(running.pid);
|
|
31710
|
+
const described = signalled === null ? "nothing it would name" : `${signalled.kind}: ${signalled.text}`;
|
|
31711
|
+
let landed = true;
|
|
31451
31712
|
try {
|
|
31452
31713
|
process.kill(running.pid);
|
|
31453
|
-
} catch {
|
|
31714
|
+
} catch {
|
|
31715
|
+
landed = false;
|
|
31716
|
+
}
|
|
31717
|
+
running.log.record("info", landed ? `gdharness ended pid ${running.pid}, which the operating system described as ${described}.` : `gdharness signalled pid ${running.pid} and it was already gone; the operating system had described it as ${described}.`);
|
|
31454
31718
|
}
|
|
31455
31719
|
spawnGame(godotPath, cmdArgs, env) {
|
|
31456
31720
|
const child = spawn(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
|
|
@@ -31788,7 +32052,7 @@ class GodotServer {
|
|
|
31788
32052
|
notes.push(run.exitCode === 0 ? "This run quit on its own, cleanly: exit code 0." : "Nothing here ended this run: it stopped on its own or something outside this server stopped it.");
|
|
31789
32053
|
}
|
|
31790
32054
|
if (run.transcript !== null && selected.omitted > 0) {
|
|
31791
|
-
notes.push(`Everything this run has printed is in ${run.transcript}, uncapped and still being written. Read it for output, not for whether the run is alive: running above is asked of the operating system, and
|
|
32055
|
+
notes.push(`Everything this run has printed is in ${run.transcript}, uncapped and still being written. Read it for output, not for whether the run is alive: a transcript that stops growing is a run between prints. running above is asked of the operating system while there is a process to ask about, and for an editor-played run that announced no runtime it is the editor's answer instead, which can lag after the game has gone.`);
|
|
31792
32056
|
}
|
|
31793
32057
|
return this.jsonTextResponse({
|
|
31794
32058
|
running: stillRunning(run),
|
|
@@ -31990,8 +32254,8 @@ class GodotServer {
|
|
|
31990
32254
|
return this.createErrorResponse(choice.problem);
|
|
31991
32255
|
}
|
|
31992
32256
|
const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
|
|
31993
|
-
const screenshotDir = expectsScreenshot ? mkdtempSync2(
|
|
31994
|
-
const screenshotPath = screenshotDir ?
|
|
32257
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync2(join12(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
|
|
32258
|
+
const screenshotPath = screenshotDir ? join12(screenshotDir, "capture.png") : null;
|
|
31995
32259
|
try {
|
|
31996
32260
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
|
|
31997
32261
|
if (!reply.ok) {
|
|
@@ -32012,7 +32276,7 @@ class GodotServer {
|
|
|
32012
32276
|
return {
|
|
32013
32277
|
content: [
|
|
32014
32278
|
{ type: "text", text: `Screenshot captured: ${dimensions}` },
|
|
32015
|
-
{ type: "image", data:
|
|
32279
|
+
{ type: "image", data: readFileSync12(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
32016
32280
|
]
|
|
32017
32281
|
};
|
|
32018
32282
|
} finally {
|