gdharness 1.0.1 → 1.0.3
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 +276 -63
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +44 -9
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +123 -21
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +81 -46
- package/build/godot/operations/classdb_queries.gd +75 -0
- package/build/index.js +275 -61
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -10183,7 +10183,7 @@ import {
|
|
|
10183
10183
|
readSync
|
|
10184
10184
|
} from "node:fs";
|
|
10185
10185
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
10186
|
-
import { basename as basename3, dirname as
|
|
10186
|
+
import { basename as basename3, dirname as dirname7, join as join13, normalize as normalize2, relative as relative3 } from "node:path";
|
|
10187
10187
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
10188
10188
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
10189
10189
|
import { promisify as promisify5 } from "node:util";
|
|
@@ -25052,6 +25052,9 @@ function cacheWrittenAt(projectPath) {
|
|
|
25052
25052
|
return null;
|
|
25053
25053
|
}
|
|
25054
25054
|
}
|
|
25055
|
+
function cachedAtMissingPaths(cached, projectPath) {
|
|
25056
|
+
return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync2(join4(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
|
|
25057
|
+
}
|
|
25055
25058
|
function staleAgainst(cached, projectPath) {
|
|
25056
25059
|
return [...declaredClasses(projectPath)].filter(([name, path]) => cached.get(name) !== path).map(([name]) => name);
|
|
25057
25060
|
}
|
|
@@ -27710,20 +27713,61 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
27710
27713
|
}
|
|
27711
27714
|
|
|
27712
27715
|
// src/process-children.ts
|
|
27713
|
-
import { execFile as execFile3 } from "node:child_process";
|
|
27716
|
+
import { execFile as execFile3, execFileSync, spawnSync } from "node:child_process";
|
|
27714
27717
|
import { basename } from "node:path";
|
|
27715
27718
|
import { promisify as promisify3 } from "node:util";
|
|
27716
27719
|
var run3 = promisify3(execFile3);
|
|
27720
|
+
function startTimesOf(pids) {
|
|
27721
|
+
const began = new Map;
|
|
27722
|
+
if (pids.length === 0) {
|
|
27723
|
+
return began;
|
|
27724
|
+
}
|
|
27725
|
+
try {
|
|
27726
|
+
if (process.platform === "win32") {
|
|
27727
|
+
const filter = pids.map((pid) => `ProcessId=${pid}`).join(" OR ");
|
|
27728
|
+
const said = execFileSync("powershell", [
|
|
27729
|
+
"-NoProfile",
|
|
27730
|
+
"-NonInteractive",
|
|
27731
|
+
"-Command",
|
|
27732
|
+
`Get-CimInstance Win32_Process -Filter "${filter}" | ForEach-Object { "$($_.ProcessId) $(([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds())" }`
|
|
27733
|
+
], { encoding: "utf8", timeout: ASK_TIMEOUT_MS, windowsHide: true });
|
|
27734
|
+
for (const line of said.split(`
|
|
27735
|
+
`)) {
|
|
27736
|
+
const found = /^\s*(\d+)\s+(\d+)\s*$/.exec(line);
|
|
27737
|
+
if (found?.[1] !== undefined && found[2] !== undefined) {
|
|
27738
|
+
began.set(Number(found[1]), Number(found[2]));
|
|
27739
|
+
}
|
|
27740
|
+
}
|
|
27741
|
+
return began;
|
|
27742
|
+
}
|
|
27743
|
+
const said = spawnSync("ps", ["-p", pids.join(","), "-o", "pid=,lstart="], {
|
|
27744
|
+
encoding: "utf8",
|
|
27745
|
+
timeout: ASK_TIMEOUT_MS,
|
|
27746
|
+
env: { ...process.env, LC_ALL: "C" }
|
|
27747
|
+
}).stdout;
|
|
27748
|
+
for (const line of said.split(`
|
|
27749
|
+
`)) {
|
|
27750
|
+
const found = /^\s*(\d+)\s+(.+)$/.exec(line);
|
|
27751
|
+
const at = Date.parse(found?.[2]?.trim() ?? "");
|
|
27752
|
+
if (found?.[1] !== undefined && !Number.isNaN(at)) {
|
|
27753
|
+
began.set(Number(found[1]), at);
|
|
27754
|
+
}
|
|
27755
|
+
}
|
|
27756
|
+
} catch {}
|
|
27757
|
+
return began;
|
|
27758
|
+
}
|
|
27717
27759
|
var ASK_TIMEOUT_MS = 15000;
|
|
27718
27760
|
async function processTree() {
|
|
27719
27761
|
try {
|
|
27720
|
-
|
|
27721
|
-
|
|
27762
|
+
if (process.platform === "win32") {
|
|
27763
|
+
return parseProcessTable(await askWindows(), true);
|
|
27764
|
+
}
|
|
27765
|
+
return parseProcessTable(await askPosix());
|
|
27722
27766
|
} catch {
|
|
27723
27767
|
return;
|
|
27724
27768
|
}
|
|
27725
27769
|
}
|
|
27726
|
-
function parseProcessTable(printed) {
|
|
27770
|
+
function parseProcessTable(printed, started = false) {
|
|
27727
27771
|
const tree = new Map;
|
|
27728
27772
|
for (const line of printed.split(/\r?\n/)) {
|
|
27729
27773
|
const fields = /^\s*(\d+)\s+(\d+)(?:\s+(.*))?$/.exec(line);
|
|
@@ -27732,16 +27776,38 @@ function parseProcessTable(printed) {
|
|
|
27732
27776
|
}
|
|
27733
27777
|
const pid = Number(fields[1]);
|
|
27734
27778
|
const parent = Number(fields[2]);
|
|
27735
|
-
|
|
27736
|
-
|
|
27779
|
+
const rest = (fields[3] ?? "").trim();
|
|
27780
|
+
if (pid <= 0) {
|
|
27781
|
+
continue;
|
|
27782
|
+
}
|
|
27783
|
+
if (!started) {
|
|
27784
|
+
tree.set(pid, { parent, command: rest });
|
|
27785
|
+
continue;
|
|
27737
27786
|
}
|
|
27787
|
+
const timed = /^(\d+)(?:\s+(.*))?$/.exec(rest);
|
|
27788
|
+
if (timed === null) {
|
|
27789
|
+
continue;
|
|
27790
|
+
}
|
|
27791
|
+
const began = Number(timed[1]);
|
|
27792
|
+
tree.set(pid, { parent, command: (timed[2] ?? "").trim(), ...began > 0 ? { startedAt: began } : {} });
|
|
27738
27793
|
}
|
|
27739
27794
|
return tree;
|
|
27740
27795
|
}
|
|
27796
|
+
function linked(tree, child, parent) {
|
|
27797
|
+
const from = tree.get(child);
|
|
27798
|
+
const to = tree.get(parent);
|
|
27799
|
+
if (from === undefined || from.parent !== parent) {
|
|
27800
|
+
return false;
|
|
27801
|
+
}
|
|
27802
|
+
if (from.startedAt === undefined || to?.startedAt === undefined) {
|
|
27803
|
+
return true;
|
|
27804
|
+
}
|
|
27805
|
+
return from.startedAt >= to.startedAt;
|
|
27806
|
+
}
|
|
27741
27807
|
function childrenIn(tree, pid) {
|
|
27742
27808
|
const children = [];
|
|
27743
|
-
for (const
|
|
27744
|
-
if (
|
|
27809
|
+
for (const child of tree.keys()) {
|
|
27810
|
+
if (linked(tree, child, pid)) {
|
|
27745
27811
|
children.push(child);
|
|
27746
27812
|
}
|
|
27747
27813
|
}
|
|
@@ -27769,13 +27835,15 @@ function descendantsIn(tree, pid) {
|
|
|
27769
27835
|
function ancestorsIn(tree, pid, root) {
|
|
27770
27836
|
const chain = [];
|
|
27771
27837
|
const seen = new Set([pid]);
|
|
27838
|
+
let child = pid;
|
|
27772
27839
|
let parent = tree.get(pid)?.parent;
|
|
27773
|
-
while (parent !== undefined && !seen.has(parent)) {
|
|
27840
|
+
while (parent !== undefined && !seen.has(parent) && linked(tree, child, parent)) {
|
|
27774
27841
|
chain.push(parent);
|
|
27775
27842
|
if (parent === root) {
|
|
27776
27843
|
break;
|
|
27777
27844
|
}
|
|
27778
27845
|
seen.add(parent);
|
|
27846
|
+
child = parent;
|
|
27779
27847
|
parent = tree.get(parent)?.parent;
|
|
27780
27848
|
}
|
|
27781
27849
|
return chain;
|
|
@@ -27831,7 +27899,7 @@ async function askWindows() {
|
|
|
27831
27899
|
"-NoProfile",
|
|
27832
27900
|
"-NonInteractive",
|
|
27833
27901
|
"-Command",
|
|
27834
|
-
'Get-CimInstance Win32_Process | ForEach-Object { "$($_.ProcessId) $($_.ParentProcessId) $($_.CommandLine)" }'
|
|
27902
|
+
'Get-CimInstance Win32_Process | ForEach-Object { $began = 0; if ($_.CreationDate) { $began = ([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds() }; "$($_.ProcessId) $($_.ParentProcessId) $began $($_.CommandLine)" }'
|
|
27835
27903
|
], { timeout: ASK_TIMEOUT_MS, windowsHide: true, maxBuffer: 16 * 1024 * 1024 });
|
|
27836
27904
|
return stdout;
|
|
27837
27905
|
}
|
|
@@ -28289,7 +28357,7 @@ function restartSettled(projectPath) {
|
|
|
28289
28357
|
}
|
|
28290
28358
|
|
|
28291
28359
|
// src/run-record.ts
|
|
28292
|
-
import { execFileSync } from "node:child_process";
|
|
28360
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
28293
28361
|
import { mkdirSync as mkdirSync5, openSync, readdirSync as readdirSync5, readFileSync as readFileSync11, rmSync as rmSync5, statSync as statSync4, writeFileSync as writeFileSync6 } from "node:fs";
|
|
28294
28362
|
import { basename as basename2, join as join11 } from "node:path";
|
|
28295
28363
|
|
|
@@ -28297,7 +28365,7 @@ import { basename as basename2, join as join11 } from "node:path";
|
|
|
28297
28365
|
import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync10, statSync as statSync3, unlinkSync } from "node:fs";
|
|
28298
28366
|
import { createConnection as createConnection3 } from "node:net";
|
|
28299
28367
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
28300
|
-
import { join as join10, resolve as resolve4 } from "node:path";
|
|
28368
|
+
import { dirname as dirname5, join as join10, resolve as resolve4 } from "node:path";
|
|
28301
28369
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
28302
28370
|
|
|
28303
28371
|
// src/tool-args.ts
|
|
@@ -28471,11 +28539,35 @@ function announcedAndGone(projectPath) {
|
|
|
28471
28539
|
const wanted = resolve4(projectPath);
|
|
28472
28540
|
return gone.filter((one) => one.noticedAt >= since && resolve4(one.project) === wanted);
|
|
28473
28541
|
}
|
|
28542
|
+
var STARTED_AFTER_ANNOUNCING_MS = 2000;
|
|
28543
|
+
var CONFIRMED_FOR_MS = 60000;
|
|
28544
|
+
var JUDGED_AFTER_MS = 60000;
|
|
28545
|
+
var verdicts = new Map;
|
|
28546
|
+
function strangersAmong(candidates, now, startTimes = startTimesOf) {
|
|
28547
|
+
const toJudge = candidates.filter((one) => {
|
|
28548
|
+
if (now - one.writtenAt < JUDGED_AFTER_MS) {
|
|
28549
|
+
return false;
|
|
28550
|
+
}
|
|
28551
|
+
const known = verdicts.get(one.file);
|
|
28552
|
+
return known === undefined || known.writtenAt !== one.writtenAt || !known.stranger && now - known.judgedAt > CONFIRMED_FOR_MS;
|
|
28553
|
+
});
|
|
28554
|
+
if (toJudge.length > 0) {
|
|
28555
|
+
const began = startTimes(toJudge.map((one) => one.pid));
|
|
28556
|
+
for (const one of toJudge) {
|
|
28557
|
+
const at = began.get(one.pid);
|
|
28558
|
+
const stranger = at !== undefined && at - one.writtenAt > STARTED_AFTER_ANNOUNCING_MS;
|
|
28559
|
+
verdicts.set(one.file, { writtenAt: one.writtenAt, judgedAt: now, stranger });
|
|
28560
|
+
}
|
|
28561
|
+
}
|
|
28562
|
+
return new Set(candidates.filter((one) => verdicts.get(one.file)?.stranger === true).map((one) => one.file));
|
|
28563
|
+
}
|
|
28474
28564
|
function announcedIn(directory) {
|
|
28475
28565
|
if (!existsSync7(directory)) {
|
|
28476
28566
|
return [];
|
|
28477
28567
|
}
|
|
28478
28568
|
const found = [];
|
|
28569
|
+
const candidates = [];
|
|
28570
|
+
const entries = [];
|
|
28479
28571
|
for (const entry of readdirSync4(directory)) {
|
|
28480
28572
|
const report = ERROR_REPORT_PATTERN.exec(entry);
|
|
28481
28573
|
if (report) {
|
|
@@ -28489,20 +28581,60 @@ function announcedIn(directory) {
|
|
|
28489
28581
|
const file = join10(directory, entry);
|
|
28490
28582
|
const pid = Number.parseInt(match[1] ?? "", 10);
|
|
28491
28583
|
const alive = processAlive(pid);
|
|
28492
|
-
|
|
28584
|
+
entries.push({ file, pid, alive });
|
|
28585
|
+
if (alive) {
|
|
28586
|
+
try {
|
|
28587
|
+
candidates.push({ file, pid, writtenAt: statSync3(file).mtimeMs });
|
|
28588
|
+
} catch {}
|
|
28589
|
+
}
|
|
28590
|
+
}
|
|
28591
|
+
const strangers = strangersAmong(candidates, Date.now());
|
|
28592
|
+
for (const { file, pid, alive } of entries) {
|
|
28593
|
+
const announced = alive && !strangers.has(file) ? parseAnnouncement(file, pid) : { kind: "rubbish" };
|
|
28493
28594
|
if (announced.kind === "rubbish") {
|
|
28494
|
-
if (!alive) {
|
|
28595
|
+
if (!alive || strangers.has(file)) {
|
|
28495
28596
|
wentAway(file, pid);
|
|
28496
28597
|
}
|
|
28497
28598
|
try {
|
|
28498
28599
|
unlinkSync(file);
|
|
28499
28600
|
} catch {}
|
|
28601
|
+
verdicts.delete(file);
|
|
28500
28602
|
continue;
|
|
28501
28603
|
}
|
|
28502
28604
|
found.push(announced);
|
|
28503
28605
|
}
|
|
28606
|
+
const held = new Set(entries.map((one) => one.file));
|
|
28607
|
+
const here = join10(directory, ".");
|
|
28608
|
+
for (const file of verdicts.keys()) {
|
|
28609
|
+
if (dirname5(file) === here && !held.has(file)) {
|
|
28610
|
+
verdicts.delete(file);
|
|
28611
|
+
}
|
|
28612
|
+
}
|
|
28504
28613
|
return found;
|
|
28505
28614
|
}
|
|
28615
|
+
var ENDED_WITHIN_MS = 3000;
|
|
28616
|
+
async function announcementEnded(pid, directories = runtimeDirectories()) {
|
|
28617
|
+
const files = directories.map((directory) => join10(directory, `runtime-${pid}.json`)).filter((file) => existsSync7(file));
|
|
28618
|
+
const first = files[0];
|
|
28619
|
+
if (first === undefined) {
|
|
28620
|
+
return;
|
|
28621
|
+
}
|
|
28622
|
+
const until = Date.now() + ENDED_WITHIN_MS;
|
|
28623
|
+
while (processAlive(pid)) {
|
|
28624
|
+
const left = until - Date.now();
|
|
28625
|
+
if (left <= 0) {
|
|
28626
|
+
return;
|
|
28627
|
+
}
|
|
28628
|
+
await delay2(Math.min(LOOK_EVERY_MS, left));
|
|
28629
|
+
}
|
|
28630
|
+
wentAway(first, pid);
|
|
28631
|
+
for (const file of files) {
|
|
28632
|
+
try {
|
|
28633
|
+
unlinkSync(file);
|
|
28634
|
+
} catch {}
|
|
28635
|
+
verdicts.delete(file);
|
|
28636
|
+
}
|
|
28637
|
+
}
|
|
28506
28638
|
function sweepErrorReport(path, pid, now = Date.now()) {
|
|
28507
28639
|
if (processAlive(pid)) {
|
|
28508
28640
|
return;
|
|
@@ -28811,7 +28943,7 @@ function runningAs(pid) {
|
|
|
28811
28943
|
const raw = readFileSync11(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
28812
28944
|
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
28813
28945
|
}
|
|
28814
|
-
const args =
|
|
28946
|
+
const args = execFileSync2("ps", ["-p", String(pid), "-o", "args="], {
|
|
28815
28947
|
encoding: "utf8",
|
|
28816
28948
|
timeout: 15000
|
|
28817
28949
|
}).trim();
|
|
@@ -28823,20 +28955,14 @@ function runningAs(pid) {
|
|
|
28823
28955
|
function listeningPid(port) {
|
|
28824
28956
|
try {
|
|
28825
28957
|
if (process.platform === "win32") {
|
|
28826
|
-
|
|
28827
|
-
"-NoProfile",
|
|
28828
|
-
"-NonInteractive",
|
|
28829
|
-
"-Command",
|
|
28830
|
-
`(Get-NetTCPConnection -State Listen -LocalPort ${port} -ErrorAction SilentlyContinue | Select-Object -First 1).OwningProcess`
|
|
28831
|
-
], { encoding: "utf8", timeout: 15000 }).trim();
|
|
28832
|
-
return /^\d+$/.test(said) ? Number(said) : null;
|
|
28958
|
+
return listeningPidInNetstat(execFileSync2("netstat", ["-ano"], { encoding: "utf8", timeout: 15000 }), port);
|
|
28833
28959
|
}
|
|
28834
28960
|
for (const [command, args, pattern] of [
|
|
28835
28961
|
["lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], /^(\d+)/],
|
|
28836
28962
|
["ss", ["-ltnpH", `sport = :${port}`], /pid=(\d+)/]
|
|
28837
28963
|
]) {
|
|
28838
28964
|
try {
|
|
28839
|
-
const said =
|
|
28965
|
+
const said = execFileSync2(command, [...args], { encoding: "utf8", timeout: 15000 }).trim();
|
|
28840
28966
|
const found = pattern.exec(said);
|
|
28841
28967
|
if (found?.[1] !== undefined) {
|
|
28842
28968
|
return Number(found[1]);
|
|
@@ -28848,9 +28974,22 @@ function listeningPid(port) {
|
|
|
28848
28974
|
return null;
|
|
28849
28975
|
}
|
|
28850
28976
|
}
|
|
28977
|
+
function listeningPidInNetstat(printed, port) {
|
|
28978
|
+
for (const line of printed.split(/\r?\n/)) {
|
|
28979
|
+
const fields = /^\s*TCP\s+(\S+)\s+\S+\s+LISTENING\s+(\d+)\s*$/i.exec(line);
|
|
28980
|
+
if (fields === null) {
|
|
28981
|
+
continue;
|
|
28982
|
+
}
|
|
28983
|
+
const local = fields[1] ?? "";
|
|
28984
|
+
if (Number(local.slice(local.lastIndexOf(":") + 1)) === port) {
|
|
28985
|
+
return Number(fields[2]);
|
|
28986
|
+
}
|
|
28987
|
+
}
|
|
28988
|
+
return null;
|
|
28989
|
+
}
|
|
28851
28990
|
function windowsCommandLine(pid) {
|
|
28852
28991
|
try {
|
|
28853
|
-
const answer =
|
|
28992
|
+
const answer = execFileSync2("powershell", [
|
|
28854
28993
|
"-NoProfile",
|
|
28855
28994
|
"-NonInteractive",
|
|
28856
28995
|
"-Command",
|
|
@@ -28868,7 +29007,7 @@ function startedAt(pid, windowsAnswer) {
|
|
|
28868
29007
|
return Number.isNaN(when) ? null : when;
|
|
28869
29008
|
}
|
|
28870
29009
|
try {
|
|
28871
|
-
const said =
|
|
29010
|
+
const said = execFileSync2("ps", ["-p", String(pid), "-o", "lstart="], {
|
|
28872
29011
|
encoding: "utf8",
|
|
28873
29012
|
timeout: 15000
|
|
28874
29013
|
}).trim();
|
|
@@ -28880,7 +29019,7 @@ function startedAt(pid, windowsAnswer) {
|
|
|
28880
29019
|
}
|
|
28881
29020
|
function windowsImage(pid) {
|
|
28882
29021
|
try {
|
|
28883
|
-
const csv =
|
|
29022
|
+
const csv = execFileSync2("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
|
|
28884
29023
|
encoding: "utf8",
|
|
28885
29024
|
timeout: 15000,
|
|
28886
29025
|
windowsHide: true
|
|
@@ -28966,7 +29105,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
28966
29105
|
|
|
28967
29106
|
// src/setup.ts
|
|
28968
29107
|
import { cpSync, existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync12, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
|
|
28969
|
-
import { dirname as
|
|
29108
|
+
import { dirname as dirname6, join as join12 } from "node:path";
|
|
28970
29109
|
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
28971
29110
|
var RUNTIME_AUTOLOAD = {
|
|
28972
29111
|
name: "GdharnessRuntime",
|
|
@@ -29687,7 +29826,7 @@ var TOOL_SPECS = [
|
|
|
29687
29826
|
},
|
|
29688
29827
|
{
|
|
29689
29828
|
name: "editor_rescan",
|
|
29690
|
-
description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. The scan is a change-detecting walk rather than an unconditional reparse, so a file another engine has already imported reads as settled and the walk does not look inside it: its class_name then stays out of the list the editor resolves against, however many times you scan. Any class in that state is named under unseenByEditor, which is what no check on disk can see, since the declaration and the cache are both correct there and only the editor disagrees. The cure is this call on its own, with no change to the declaring script: measured against a real editor on three platforms, with idle waits of the same length ruled out so the scan is credited rather than the time it takes, and reproduced in a second project against its own reproduction in 203ms. editor_launch restart also does it and costs a window, and project_import refresh_classes does not: it rewrites the cache and does not touch what the editor is holding. A scan writes .godot/global_script_class_cache.cfg from the list the editor is holding, so a class the editor cannot resolve goes out of that file with it. That is not a reason to refuse the scan, because on a class the editor has merely not walked yet the scan is the cure; what the answer does instead is name the loss under cacheLost and rebuild the cache from the files, naming what came back under cacheRestored, so no fresh engine, CI run or clone inherits the short file. An editor that lost classes is still holding the short list, so the note says to restart it before scanning again. Needs the editor connected.",
|
|
29829
|
+
description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. The scan is a change-detecting walk rather than an unconditional reparse, so a file another engine has already imported reads as settled and the walk does not look inside it: its class_name then stays out of the list the editor resolves against, however many times you scan. Any class in that state is named under unseenByEditor, which is what no check on disk can see, since the declaration and the cache are both correct there and only the editor disagrees. The cure is this call on its own, with no change to the declaring script: measured against a real editor on three platforms, with idle waits of the same length ruled out so the scan is credited rather than the time it takes, and reproduced in a second project against its own reproduction in 203ms. editor_launch restart also does it and costs a window, and project_import refresh_classes does not: it rewrites the cache and does not touch what the editor is holding. A scan writes .godot/global_script_class_cache.cfg from the list the editor is holding, so a class the editor cannot resolve goes out of that file with it. That is not a reason to refuse the scan, because on a class the editor has merely not walked yet the scan is the cure; what the answer does instead is name the loss under cacheLost and rebuild the cache from the files, naming what came back under cacheRestored, so no fresh engine, CI run or clone inherits the short file. An editor that lost classes is still holding the short list, so the note says to restart it before scanning again. The other direction is guarded the same way: a class the editor still holds for a script that was renamed or deleted comes back into the cache at a path that is not there, and the next engine to read it fails on \"Could not parse global class\" in whichever correct script shares the bare name, so the cache is rebuilt without it and the answer names it under cacheDropped; the editor goes on holding it until it is restarted, and every scan until then ends in that rebuild. A script or shader written outside the editor has no .uid sidecar until something imports it, and the scan writes one beside it, which a project that commits sidecars needs before the commit. Needs the editor connected.",
|
|
29691
29830
|
parameters: {
|
|
29692
29831
|
projectPath: PROJECT_PATH,
|
|
29693
29832
|
timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." },
|
|
@@ -29723,12 +29862,20 @@ var TOOL_SPECS = [
|
|
|
29723
29862
|
},
|
|
29724
29863
|
instantiableOnly: { type: "boolean", description: "query: leave out abstract classes. Default false." },
|
|
29725
29864
|
className: { type: "string", description: "info, inheritance: the class." },
|
|
29726
|
-
includeInherited: { type: "boolean", description: "info: include inherited members. Default false." }
|
|
29865
|
+
includeInherited: { type: "boolean", description: "info: include inherited members. Default false." },
|
|
29866
|
+
member: {
|
|
29867
|
+
type: "string",
|
|
29868
|
+
ops: ["info"],
|
|
29869
|
+
description: 'info: one member by name, a method, property, signal, enum or constant, with its signature and which class declares it, looked for on the class and every ancestor. The whole of Control is seventy thousand characters; the question "does this engine have it, and what does it take" is this. A name nothing declares is refused with the members whose names contain it.'
|
|
29870
|
+
}
|
|
29727
29871
|
},
|
|
29728
29872
|
requires: ["projectPath"],
|
|
29729
29873
|
operations: {
|
|
29730
29874
|
query: { summary: "classes matching a filter or category", requires: [] },
|
|
29731
|
-
info: {
|
|
29875
|
+
info: {
|
|
29876
|
+
summary: "methods, properties, signals and enums of one class, or one member of it by name",
|
|
29877
|
+
requires: ["className"]
|
|
29878
|
+
},
|
|
29732
29879
|
inheritance: { summary: "ancestors and descendants of one class", requires: ["className"] }
|
|
29733
29880
|
},
|
|
29734
29881
|
defaultOperation: "query"
|
|
@@ -29747,13 +29894,19 @@ var TOOL_SPECS = [
|
|
|
29747
29894
|
property: {
|
|
29748
29895
|
type: "string",
|
|
29749
29896
|
ops: ["property", "find"],
|
|
29750
|
-
description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A negative number counts from the end. A step that is not there is named, and says what was there instead.'
|
|
29897
|
+
description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that takes no arguments and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. A step that is not there is named, and says what was there instead.'
|
|
29751
29898
|
},
|
|
29752
29899
|
depth: { type: "number", ops: ["tree"], description: "tree: levels to descend. Default 3." },
|
|
29753
29900
|
includeProperties: {
|
|
29754
29901
|
type: "boolean",
|
|
29755
29902
|
ops: ["tree"],
|
|
29756
|
-
description: "tree: include each node
|
|
29903
|
+
description: "tree: include every stored property of each node. Default false, and one row of a form answered seventy-six thousand characters with it; properties names the few that are wanted."
|
|
29904
|
+
},
|
|
29905
|
+
properties: {
|
|
29906
|
+
type: "array",
|
|
29907
|
+
items: { type: "string" },
|
|
29908
|
+
ops: ["tree"],
|
|
29909
|
+
description: "tree: the properties to read off each node, by name, so the text of every label and button under a panel is one call: a node that has not got one leaves it out. Colons read through what a node holds, as property does for find."
|
|
29757
29910
|
},
|
|
29758
29911
|
className: {
|
|
29759
29912
|
type: "string",
|
|
@@ -29822,7 +29975,7 @@ var TOOL_SPECS = [
|
|
|
29822
29975
|
property: {
|
|
29823
29976
|
type: "string",
|
|
29824
29977
|
ops: ["set"],
|
|
29825
|
-
description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end. The answer reads back off the same holder, so a write a typed container refused shows as an unchanged value.'
|
|
29978
|
+
description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end, and a step written as a call, "get_viewport():gui_embed_subwindows", walks through what a method taking no arguments returned. The answer reads back off the same holder, so a write a typed container refused shows as an unchanged value.'
|
|
29826
29979
|
},
|
|
29827
29980
|
value: {
|
|
29828
29981
|
ops: ["set"],
|
|
@@ -29832,7 +29985,7 @@ var TOOL_SPECS = [
|
|
|
29832
29985
|
method: {
|
|
29833
29986
|
type: "string",
|
|
29834
29987
|
ops: ["call"],
|
|
29835
|
-
description: 'call: which one to call. Colons call through what a node holds, "_game:run:advance", the same way a property is written through them,
|
|
29988
|
+
description: 'call: which one to call. Colons call through what a node holds, "_game:run:advance", the same way a property is written through them, a list or a map is stepped into by index or key, "_game:run:roster:0:retire", and a step written as a call walks through what a method taking no arguments returned, "get_viewport():gui_get_focus_owner".'
|
|
29836
29989
|
},
|
|
29837
29990
|
args: {
|
|
29838
29991
|
type: "array",
|
|
@@ -29892,7 +30045,7 @@ var TOOL_SPECS = [
|
|
|
29892
30045
|
pressed: {
|
|
29893
30046
|
type: "boolean",
|
|
29894
30047
|
ops: ["action", "key", "mouse_click"],
|
|
29895
|
-
description: "action, key: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. mouse_click is one raw event, so it is down unless you say false."
|
|
30048
|
+
description: "action, key: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. mouse_click is one raw event, so it is down unless you say false; a wheel button down is a whole step, since a wheel is never held."
|
|
29896
30049
|
},
|
|
29897
30050
|
strength: { type: "number", ops: ["action"], description: "action: 0 to 1. Default 1." },
|
|
29898
30051
|
keycode: {
|
|
@@ -29942,7 +30095,7 @@ var TOOL_SPECS = [
|
|
|
29942
30095
|
relativeX: {
|
|
29943
30096
|
type: "number",
|
|
29944
30097
|
ops: ["mouse_motion"],
|
|
29945
|
-
description: "mouse_motion: the movement the event carries. Left out, it is the distance from where the pointer
|
|
30098
|
+
description: "mouse_motion: the movement the event carries. Left out, it is the distance from where the last injected pointer event put the pointer, and none for the first; that is what a control that drags reads. Give it to send a motion the position does not show."
|
|
29946
30099
|
},
|
|
29947
30100
|
relativeY: {
|
|
29948
30101
|
type: "number",
|
|
@@ -29962,7 +30115,7 @@ var TOOL_SPECS = [
|
|
|
29962
30115
|
},
|
|
29963
30116
|
action: { summary: "press an action, or hold it", requires: ["action"] },
|
|
29964
30117
|
key: {
|
|
29965
|
-
summary: "press a key, or hold it. Not for a menu a click has opened: an OptionButton or PopupMenu pops up as a window of its own that a key sent to the game never reaches, so an arrow moves nothing in it and Enter closes it with nothing chosen. choose is what selects there",
|
|
30118
|
+
summary: "press a key, or hold it. Not for a menu a click has opened: an OptionButton or PopupMenu pops up as a window of its own that a key sent to the game never reaches, so an arrow moves nothing in it and Enter closes it with nothing chosen. choose is what selects there. The limit is that menu's own navigation, not windows in general: an embedded dialog whose owner reads keys in _input gets them, and a binding catcher up in one takes the key first time",
|
|
29966
30119
|
requires: ["keycode"]
|
|
29967
30120
|
},
|
|
29968
30121
|
text: {
|
|
@@ -29970,11 +30123,11 @@ var TOOL_SPECS = [
|
|
|
29970
30123
|
requires: ["text"]
|
|
29971
30124
|
},
|
|
29972
30125
|
mouse_click: {
|
|
29973
|
-
summary: "one mouse button event at a position, carrying the buttons held once it has happened. A drag is a press held with pressed true, motions, and a release with pressed false",
|
|
30126
|
+
summary: "one mouse button event at a position, carrying the buttons held once it has happened. A drag is a press held with pressed true, motions, and a release with pressed false. A wheel button is a whole step, press and release together as a mouse sends one, and the answer says released: a lone wheel press holds the viewport on the control that took it, so every click after it lands there",
|
|
29974
30127
|
requires: ["x", "y"]
|
|
29975
30128
|
},
|
|
29976
30129
|
mouse_motion: {
|
|
29977
|
-
summary: "move the mouse to a position, carrying the distance from where
|
|
30130
|
+
summary: "move the mouse to a position, carrying the distance from where the last injected event put it unless relativeX and relativeY say otherwise, and the buttons held, so a control that drags moves under a run of these between a held mouse_click and its release",
|
|
29978
30131
|
requires: ["x", "y"]
|
|
29979
30132
|
}
|
|
29980
30133
|
}
|
|
@@ -29992,12 +30145,21 @@ var TOOL_SPECS = [
|
|
|
29992
30145
|
},
|
|
29993
30146
|
nodePath: { type: "string", ops: ["signal", "until"], description: "signal, until: the node." },
|
|
29994
30147
|
signal: { type: "string", ops: ["signal"], description: "signal: the signal name." },
|
|
29995
|
-
property: {
|
|
30148
|
+
property: {
|
|
30149
|
+
type: "string",
|
|
30150
|
+
ops: ["until"],
|
|
30151
|
+
description: 'until: the property name, or a colon path through what the node holds, "_game:run:day", read the way runtime_inspect property reads one, a step written as a call included. Walked again on every frame, so a holder the game replaces while the wait is on is followed.'
|
|
30152
|
+
},
|
|
29996
30153
|
value: { ops: ["until"], description: "until: the value to wait for, fitted to the property's type." },
|
|
29997
30154
|
says: {
|
|
29998
30155
|
type: "string",
|
|
29999
30156
|
ops: ["until"],
|
|
30000
|
-
description: "until: wait for these words to appear anywhere under nodePath instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put. Case-insensitive, part of a line,
|
|
30157
|
+
description: "until: wait for these words to appear anywhere under nodePath instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put. Case-insensitive, part of a line, on a node the player can see unless includeHidden says otherwise; a label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Instead of, not as well as: a call carrying this and a property is refused, because they ask about different things and answering one of them silently is how a caller watches a screen believing they are watching a property."
|
|
30158
|
+
},
|
|
30159
|
+
includeHidden: {
|
|
30160
|
+
type: "boolean",
|
|
30161
|
+
ops: ["until"],
|
|
30162
|
+
description: "until, with says: count words on a hidden node too. Default false, since a wait for words is a wait for them to be shown: a button that exists hidden through a whole animation carries its words the whole time and satisfied the wait at once, and every wait on that screen fell back to counting frames."
|
|
30001
30163
|
},
|
|
30002
30164
|
timeoutMs: {
|
|
30003
30165
|
type: "number",
|
|
@@ -30112,7 +30274,7 @@ function buildToolDefinitions() {
|
|
|
30112
30274
|
var UPDATE_NOTICE_EVERY = 500;
|
|
30113
30275
|
var FEEDBACK_NOTICE_EVERY = 250;
|
|
30114
30276
|
var run5 = promisify5(execFile5);
|
|
30115
|
-
var __dirname2 =
|
|
30277
|
+
var __dirname2 = dirname7(fileURLToPath2(import.meta.url));
|
|
30116
30278
|
var EDITOR_RESTART_TIMEOUT_MS = 90000;
|
|
30117
30279
|
var HOLD_PING_MS = 2000;
|
|
30118
30280
|
var SLOWEST_FRAME_RATE = 20;
|
|
@@ -30151,13 +30313,17 @@ function stillRunning(run) {
|
|
|
30151
30313
|
}
|
|
30152
30314
|
return true;
|
|
30153
30315
|
}
|
|
30154
|
-
|
|
30316
|
+
var PLAY_STARTS_WITHIN_MS = 30000;
|
|
30317
|
+
function runIsUp(run, editorSays, now = Date.now()) {
|
|
30155
30318
|
if (run?.throughEditor !== true) {
|
|
30156
30319
|
return stillRunning(run);
|
|
30157
30320
|
}
|
|
30158
30321
|
if (run.announcedPid !== undefined && !alive(run.announcedPid)) {
|
|
30159
30322
|
return false;
|
|
30160
30323
|
}
|
|
30324
|
+
if (editorSays === false && run.seenPlaying !== true && now - run.startedAt < PLAY_STARTS_WITHIN_MS) {
|
|
30325
|
+
return true;
|
|
30326
|
+
}
|
|
30161
30327
|
return editorSays ?? stillRunning(run);
|
|
30162
30328
|
}
|
|
30163
30329
|
function endedPreviousRun(ended) {
|
|
@@ -30232,10 +30398,11 @@ function runtimeVerdict(endpoint, after) {
|
|
|
30232
30398
|
heldAt: after.heldAt
|
|
30233
30399
|
};
|
|
30234
30400
|
}
|
|
30401
|
+
const state = after.playingSeen === false ? "the editor has not yet reported the game as playing, which a restarted editor does not until its scan is over" : "the game is still running";
|
|
30235
30402
|
return {
|
|
30236
30403
|
listening: false,
|
|
30237
30404
|
mayYetAnnounce: after.running,
|
|
30238
|
-
note: after.running ? `nothing announced itself within ${after.budgetMs}ms and
|
|
30405
|
+
note: after.running ? `nothing announced itself within ${after.budgetMs}ms and ${state}, so it may announce a moment from now: editor_status says whether it has, and editor_run start takes runtimeWaitMs to wait longer than this${after.withArgs ? ", which a run carrying its own arguments may well need, since whatever they ask the game to do before its first frame is inside this wait" : ""}` : `nothing announced itself within ${after.budgetMs}ms and the game is no longer running, so nothing is going to: editor_output has what it printed on the way down`,
|
|
30239
30406
|
heldAt: null
|
|
30240
30407
|
};
|
|
30241
30408
|
}
|
|
@@ -30966,7 +31133,8 @@ class GodotServer {
|
|
|
30966
31133
|
...whichGame(args),
|
|
30967
31134
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
30968
31135
|
depth: readPositiveNumber(args, "depth") ?? 3,
|
|
30969
|
-
include_properties: readBoolean(args, "includeProperties") ?? false
|
|
31136
|
+
include_properties: readBoolean(args, "includeProperties") ?? false,
|
|
31137
|
+
properties: readArray(args, "properties") ?? []
|
|
30970
31138
|
});
|
|
30971
31139
|
case "find":
|
|
30972
31140
|
return await this.handleFindRuntimeNodes(args);
|
|
@@ -31368,11 +31536,11 @@ class GodotServer {
|
|
|
31368
31536
|
if (!engine.ok) {
|
|
31369
31537
|
return engine.response;
|
|
31370
31538
|
}
|
|
31371
|
-
const folder =
|
|
31539
|
+
const folder = dirname7(output.absolutePath);
|
|
31372
31540
|
try {
|
|
31373
31541
|
mkdirSync7(folder, { recursive: true });
|
|
31374
31542
|
} catch (error) {
|
|
31375
|
-
return this.createErrorResponse(`Cannot create ${
|
|
31543
|
+
return this.createErrorResponse(`Cannot create ${dirname7(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
31376
31544
|
}
|
|
31377
31545
|
const debug = readBoolean(args, "debug") ?? false;
|
|
31378
31546
|
const exportLogs = mkdtempSync2(join13(tmpdir4(), "gdharness-export-"));
|
|
@@ -31826,11 +31994,14 @@ class GodotServer {
|
|
|
31826
31994
|
return null;
|
|
31827
31995
|
}
|
|
31828
31996
|
try {
|
|
31829
|
-
const
|
|
31830
|
-
|
|
31831
|
-
|
|
31832
|
-
]);
|
|
31997
|
+
const askedAt = Date.now();
|
|
31998
|
+
const asked = this.godotBridge.invokeTool("playing_status", {});
|
|
31999
|
+
const answer = await Promise.race([asked, delay3(PLAYING_STATUS_MS).then(() => null)]);
|
|
31833
32000
|
if (answer === null) {
|
|
32001
|
+
this.logDebug(`playing_status did not answer within ${PLAYING_STATUS_MS}ms`);
|
|
32002
|
+
asked.then(() => {
|
|
32003
|
+
this.logDebug(`playing_status answered after ${Date.now() - askedAt}ms`);
|
|
32004
|
+
}).catch(() => {});
|
|
31834
32005
|
return null;
|
|
31835
32006
|
}
|
|
31836
32007
|
return {
|
|
@@ -32150,6 +32321,9 @@ class GodotServer {
|
|
|
32150
32321
|
let asking = null;
|
|
32151
32322
|
let editorAskedAt = 0;
|
|
32152
32323
|
let editorSaysGoing = true;
|
|
32324
|
+
let editorAsks = 0;
|
|
32325
|
+
const waitBegan = Date.now();
|
|
32326
|
+
const lastSaid = () => editorSaysGoing ? "playing" : "not playing";
|
|
32153
32327
|
const endpoint = await announcedSince(projectPath, before, {
|
|
32154
32328
|
budgetMs,
|
|
32155
32329
|
accept: (one) => this.isTheGameOf(this.currentRun(), one, projectPath),
|
|
@@ -32163,6 +32337,7 @@ class GodotServer {
|
|
|
32163
32337
|
}
|
|
32164
32338
|
if (asking === null && Date.now() - editorAskedAt >= RUN_POLL_MS) {
|
|
32165
32339
|
editorAskedAt = Date.now();
|
|
32340
|
+
editorAsks += 1;
|
|
32166
32341
|
asking = this.runStillGoing(run).then((going) => {
|
|
32167
32342
|
editorSaysGoing = going;
|
|
32168
32343
|
}).finally(() => {
|
|
@@ -32172,6 +32347,7 @@ class GodotServer {
|
|
|
32172
32347
|
return !editorSaysGoing;
|
|
32173
32348
|
}
|
|
32174
32349
|
});
|
|
32350
|
+
this.logDebug(`announce wait ended after ${Date.now() - waitBegan}ms: ${endpoint === null ? "nothing announced" : `pid ${endpoint.pid} announced`}, editor asked ${editorAsks} times, last said ${lastSaid()}`);
|
|
32175
32351
|
const going = this.currentRun();
|
|
32176
32352
|
if (endpoint !== null && going !== null && going.announcedPid === undefined) {
|
|
32177
32353
|
going.announcedPid = endpoint.pid;
|
|
@@ -32181,7 +32357,8 @@ class GodotServer {
|
|
|
32181
32357
|
budgetMs,
|
|
32182
32358
|
heldAt: this.dapClient?.whereItStopped() ?? null,
|
|
32183
32359
|
running: endpoint !== null || going !== null && await this.runStillGoing(going),
|
|
32184
|
-
withArgs
|
|
32360
|
+
withArgs,
|
|
32361
|
+
...going?.throughEditor === true ? { playingSeen: going.seenPlaying === true } : {}
|
|
32185
32362
|
});
|
|
32186
32363
|
}
|
|
32187
32364
|
async playThroughEditor(scene, refreshedClasses, projectPath, alreadyPlaying, runtimeWaitMs, ended) {
|
|
@@ -32211,6 +32388,7 @@ class GodotServer {
|
|
|
32211
32388
|
const startedAt = Date.now();
|
|
32212
32389
|
const transcript = openTranscript(startedAt);
|
|
32213
32390
|
closeSync(transcript.fd);
|
|
32391
|
+
const playAnswer = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
|
|
32214
32392
|
const played = {
|
|
32215
32393
|
process: null,
|
|
32216
32394
|
pid: null,
|
|
@@ -32222,7 +32400,8 @@ class GodotServer {
|
|
|
32222
32400
|
exitCode: null,
|
|
32223
32401
|
throughEditor: true,
|
|
32224
32402
|
brokeOn: null,
|
|
32225
|
-
announcedBefore: alreadyPlaying
|
|
32403
|
+
announcedBefore: alreadyPlaying,
|
|
32404
|
+
seenPlaying: readBoolean(playAnswer, "playing") === true
|
|
32226
32405
|
};
|
|
32227
32406
|
this.activeProcess = played;
|
|
32228
32407
|
writeEditorRunNote({ projectPath, transcript: transcript.path, startedAt });
|
|
@@ -32232,7 +32411,7 @@ class GodotServer {
|
|
|
32232
32411
|
through: "editor",
|
|
32233
32412
|
scene: scene === null ? "the main scene" : `res://${scene}`,
|
|
32234
32413
|
refreshedClasses,
|
|
32235
|
-
debugPort: readNumber(
|
|
32414
|
+
debugPort: readNumber(playAnswer, "debugPort"),
|
|
32236
32415
|
endedPreviousRun: endedPreviousRun(ended),
|
|
32237
32416
|
breakpoints: breakpoints.applied.length === 0 ? undefined : this.breakpointsAsProjectSpells(projectPath, breakpoints.applied),
|
|
32238
32417
|
breakpointsRefused: breakpoints.refused.length === 0 ? undefined : breakpoints.refused.map((one) => ({
|
|
@@ -32758,7 +32937,11 @@ class GodotServer {
|
|
|
32758
32937
|
if (announced !== undefined && editorSays === undefined) {
|
|
32759
32938
|
return stillRunning(run);
|
|
32760
32939
|
}
|
|
32761
|
-
|
|
32940
|
+
const said = editorSays === undefined ? (await this.editorPlayingState())?.playing ?? null : editorSays;
|
|
32941
|
+
if (said === true) {
|
|
32942
|
+
run.seenPlaying = true;
|
|
32943
|
+
}
|
|
32944
|
+
return runIsUp(run, said);
|
|
32762
32945
|
}
|
|
32763
32946
|
async handleGetDebugOutput(args, waitedMs) {
|
|
32764
32947
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
@@ -32841,11 +33024,19 @@ class GodotServer {
|
|
|
32841
33024
|
this.drainEditorOutput(stopped);
|
|
32842
33025
|
this.drainTranscript(stopped);
|
|
32843
33026
|
const wasRunning = await this.runStillGoing(stopped);
|
|
32844
|
-
const
|
|
33027
|
+
const announcedPid = this.announcedPidOf(stopped) ?? null;
|
|
33028
|
+
const endedPid = stopped.pid ?? announcedPid;
|
|
32845
33029
|
const children = readBoolean(args, "andChildren") === true && wasRunning ? await this.whatTheRunStarted(stopped) : null;
|
|
32846
33030
|
this.logDebug("Stopping the running game");
|
|
32847
33031
|
await this.endActiveGame("editor_run stop");
|
|
32848
33032
|
const ended = children === null ? null : endChildrenAmong(children);
|
|
33033
|
+
if (wasRunning) {
|
|
33034
|
+
for (const pid of new Set([endedPid, announcedPid])) {
|
|
33035
|
+
if (pid !== null) {
|
|
33036
|
+
await announcementEnded(pid);
|
|
33037
|
+
}
|
|
33038
|
+
}
|
|
33039
|
+
}
|
|
32849
33040
|
return this.jsonTextResponse({
|
|
32850
33041
|
stopped: true,
|
|
32851
33042
|
through: stopped.throughEditor ? "editor" : "gdharness",
|
|
@@ -32941,6 +33132,7 @@ class GodotServer {
|
|
|
32941
33132
|
const writtenBefore = projectPath === "" ? null : cacheWrittenAt(projectPath);
|
|
32942
33133
|
const blind = await this.classesTheEditorCannotSee(args);
|
|
32943
33134
|
const atRisk = before === null ? [] : blind.unseen.filter((one) => before.has(one.className));
|
|
33135
|
+
const ghosts = blind.stillHeld ?? [];
|
|
32944
33136
|
const first = await this.handleViaBridge("rescan_filesystem", args);
|
|
32945
33137
|
if (first.isError) {
|
|
32946
33138
|
return first;
|
|
@@ -32952,7 +33144,7 @@ class GodotServer {
|
|
|
32952
33144
|
const pending = Boolean(status["pending"]) && Date.now() - started < SCAN_START_MS;
|
|
32953
33145
|
busy = Boolean(status["scanning"]) || Boolean(status["importing"]) || pending;
|
|
32954
33146
|
}
|
|
32955
|
-
if (!busy && projectPath !== "" && (atRisk.length > 0 || blind.unchecked !== undefined)) {
|
|
33147
|
+
if (!busy && projectPath !== "" && (atRisk.length > 0 || ghosts.length > 0 || blind.unchecked !== undefined)) {
|
|
32956
33148
|
const until = Date.now() + CACHE_WRITE_MS;
|
|
32957
33149
|
while (Date.now() < until && cacheWrittenAt(projectPath) === writtenBefore) {
|
|
32958
33150
|
await new Promise((settle) => setTimeout(settle, 100));
|
|
@@ -32960,7 +33152,11 @@ class GodotServer {
|
|
|
32960
33152
|
}
|
|
32961
33153
|
const after = projectPath === "" || busy ? null : cachedClasses(projectPath);
|
|
32962
33154
|
const lost = before === null || after === null ? [] : [...before.keys()].filter((name) => !after.has(name));
|
|
32963
|
-
const
|
|
33155
|
+
const atMissingPaths = after === null ? [] : cachedAtMissingPaths(after, projectPath);
|
|
33156
|
+
const restored = lost.length > 0 || atMissingPaths.length > 0 ? await this.rebuildCacheAfterLoss(projectPath, lost) : [];
|
|
33157
|
+
const rebuilt = atMissingPaths.length > 0 ? cachedClasses(projectPath) : null;
|
|
33158
|
+
const dropped = rebuilt === null ? [] : atMissingPaths.filter((name) => !cachedAtMissingPaths(rebuilt, projectPath).includes(name));
|
|
33159
|
+
const stillAtMissingPaths = atMissingPaths.filter((name) => !dropped.includes(name));
|
|
32964
33160
|
const reloading = readNonEmptyString(args, "reloadScript");
|
|
32965
33161
|
let reloaded = {};
|
|
32966
33162
|
if (reloading !== undefined) {
|
|
@@ -32976,8 +33172,23 @@ class GodotServer {
|
|
|
32976
33172
|
const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
|
|
32977
33173
|
const unseen = checked.unseen;
|
|
32978
33174
|
const stillGone = lost.filter((name) => !restored.includes(name));
|
|
33175
|
+
const notes = [];
|
|
33176
|
+
if (busy) {
|
|
33177
|
+
notes.push("The editor was still scanning or importing when the wait ran out, so new files may not be visible yet.");
|
|
33178
|
+
} else if (stillGone.length > 0) {
|
|
33179
|
+
notes.push("The scan wrote the class cache from the list this editor is holding, that list is shorter than the file was, and rebuilding the cache from the files did not bring these back. Every engine that reads the cache next, including a test run, will report them as unknown identifiers. Restart the editor with editor_launch restart before scanning again.");
|
|
33180
|
+
} else if (lost.length > 0) {
|
|
33181
|
+
notes.push("The scan wrote the class cache from the list this editor is holding, which is shorter than the file was, so the cache was rebuilt from the files and these classes are back in it. Nothing was lost, but this editor is still holding the short list: do not rescan again until it has been restarted with editor_launch restart, because it will write the same list over the file each time.");
|
|
33182
|
+
} else if (unseen.length > 0) {
|
|
33183
|
+
notes.push("The scan finished and these classes are still not in the list the editor resolves against, so every use of them reads as an unknown identifier. Its walk skips a file another engine has already imported. Rescan again on its own, which is the measured cure and needs no change to the declaring script, or restart the editor with editor_launch restart.");
|
|
33184
|
+
}
|
|
33185
|
+
if (stillAtMissingPaths.length > 0) {
|
|
33186
|
+
notes.push(`The scan wrote the class cache with ${stillAtMissingPaths.join(", ")} at a path that is not on disk, and rebuilding the cache from the files did not take it out. The next engine to read the cache fails on "Could not parse global class" in whichever correct script shares the bare name. Restart the editor with editor_launch restart, then run project_import refresh_classes.`);
|
|
33187
|
+
} else if (dropped.length > 0) {
|
|
33188
|
+
notes.push(`The scan wrote the class cache from the list this editor is holding, and that list still has ${dropped.join(", ")} for a script that is gone, so the cache was rebuilt from the files without it. The editor goes on holding it and writes it back on every scan until it is restarted with editor_launch restart; until then a scan is followed by this same rebuild.`);
|
|
33189
|
+
}
|
|
32979
33190
|
return this.jsonTextResponse({
|
|
32980
|
-
ok: !busy && unseen.length === 0 && stillGone.length === 0 && checked.unchecked === undefined,
|
|
33191
|
+
ok: !busy && unseen.length === 0 && stillGone.length === 0 && stillAtMissingPaths.length === 0 && checked.unchecked === undefined,
|
|
32981
33192
|
stillWorking: busy,
|
|
32982
33193
|
waitedMs: Date.now() - started,
|
|
32983
33194
|
heldBeforeReload: reloaded.heldBefore,
|
|
@@ -32986,8 +33197,10 @@ class GodotServer {
|
|
|
32986
33197
|
unseenByEditor: unseen.length > 0 ? unseen : undefined,
|
|
32987
33198
|
cacheLost: lost.length > 0 ? lost : undefined,
|
|
32988
33199
|
cacheRestored: restored.length > 0 ? restored : undefined,
|
|
33200
|
+
cacheDropped: dropped.length > 0 ? dropped : undefined,
|
|
33201
|
+
cacheAtMissingPaths: stillAtMissingPaths.length > 0 ? stillAtMissingPaths : undefined,
|
|
32989
33202
|
classesUnchecked: checked.unchecked,
|
|
32990
|
-
note:
|
|
33203
|
+
note: notes.length > 0 ? notes.join(" ") : undefined
|
|
32991
33204
|
});
|
|
32992
33205
|
}
|
|
32993
33206
|
async rebuildCacheAfterLoss(projectPath, lost) {
|
|
@@ -33228,6 +33441,7 @@ class GodotServer {
|
|
|
33228
33441
|
property: readString(args, "property") ?? "",
|
|
33229
33442
|
value: args["value"],
|
|
33230
33443
|
...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
|
|
33444
|
+
...readBoolean(args, "includeHidden") === undefined ? {} : { include_hidden: readBoolean(args, "includeHidden") },
|
|
33231
33445
|
timeout_ms: timeoutMs
|
|
33232
33446
|
}, patience);
|
|
33233
33447
|
}
|