gdharness 1.1.13 → 1.1.14
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 +722 -358
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +8 -51
- package/build/godot/addons/gdharness_runtime/runtime_says.gd +97 -0
- package/build/godot/addons/gdharness_runtime/runtime_says.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +54 -12
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +4 -4
- package/build/index.js +603 -242
- package/build/keeper.js +111 -10
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -10084,9 +10084,10 @@ function markIfStale(answer, addonVersion, serverVersion, addonDigest, shippedDi
|
|
|
10084
10084
|
// src/issues.ts
|
|
10085
10085
|
var NEW_ISSUE = "https://github.com/Aureliolo/gdharness/issues/new";
|
|
10086
10086
|
var ENHANCEMENT_URL = `${NEW_ISSUE}?template=feature_request.md`;
|
|
10087
|
-
function feedbackNotice() {
|
|
10087
|
+
function feedbackNotice(every) {
|
|
10088
10088
|
return JSON.stringify({
|
|
10089
10089
|
gdharness_feedback: {
|
|
10090
|
+
why_here: `Asked once every ${every} answers, whatever the call was. It is not about the answer it comes with, which is complete as it stands.`,
|
|
10090
10091
|
what_to_do: "If this session has run into something gdharness cannot do, or a tool that should " + "exist and does not, tell the user what is missing and offer to open an enhancement " + "issue for them. Only if they say yes, and only for a real gap: a call you got wrong " + "is not one. If you cannot open an issue yourself, give them the link and what to " + "write in it.",
|
|
10091
10092
|
enhancement_url: ENHANCEMENT_URL
|
|
10092
10093
|
}
|
|
@@ -10200,29 +10201,27 @@ function defectReport(where, error, context = {}) {
|
|
|
10200
10201
|
}
|
|
10201
10202
|
|
|
10202
10203
|
// src/server.ts
|
|
10203
|
-
import { execFile as
|
|
10204
|
+
import { execFile as execFile6, spawn as spawn3 } from "node:child_process";
|
|
10204
10205
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
10205
10206
|
import {
|
|
10206
10207
|
appendFileSync,
|
|
10207
10208
|
closeSync as closeSync2,
|
|
10208
10209
|
constants,
|
|
10209
10210
|
copyFileSync,
|
|
10210
|
-
existsSync as
|
|
10211
|
+
existsSync as existsSync12,
|
|
10211
10212
|
fstatSync,
|
|
10212
10213
|
mkdirSync as mkdirSync10,
|
|
10213
|
-
mkdtempSync as mkdtempSync2,
|
|
10214
10214
|
openSync as openSync3,
|
|
10215
|
-
readdirSync as
|
|
10215
|
+
readdirSync as readdirSync8,
|
|
10216
10216
|
readFileSync as readFileSync17,
|
|
10217
10217
|
readSync as readSync2,
|
|
10218
10218
|
rmdirSync as rmdirSync2,
|
|
10219
10219
|
statSync as statSync8
|
|
10220
10220
|
} from "node:fs";
|
|
10221
|
-
import {
|
|
10222
|
-
import { basename as basename4, dirname as dirname11, extname as extname2, isAbsolute as isAbsolute3, join as join17, normalize as normalize2, relative as relative3 } from "node:path";
|
|
10221
|
+
import { basename as basename4, dirname as dirname11, extname as extname2, isAbsolute as isAbsolute3, join as join18, normalize as normalize2, relative as relative3 } from "node:path";
|
|
10223
10222
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
10224
10223
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
10225
|
-
import { promisify as
|
|
10224
|
+
import { promisify as promisify6 } from "node:util";
|
|
10226
10225
|
|
|
10227
10226
|
// node_modules/zod/v3/helpers/util.js
|
|
10228
10227
|
var util;
|
|
@@ -24787,6 +24786,20 @@ class StdioServerTransport {
|
|
|
24787
24786
|
}
|
|
24788
24787
|
}
|
|
24789
24788
|
|
|
24789
|
+
// src/alive.ts
|
|
24790
|
+
import process5 from "node:process";
|
|
24791
|
+
function alive(pid) {
|
|
24792
|
+
if (pid === undefined || pid === null) {
|
|
24793
|
+
return false;
|
|
24794
|
+
}
|
|
24795
|
+
try {
|
|
24796
|
+
process5.kill(pid, 0);
|
|
24797
|
+
return true;
|
|
24798
|
+
} catch (error) {
|
|
24799
|
+
return error.code === "EPERM";
|
|
24800
|
+
}
|
|
24801
|
+
}
|
|
24802
|
+
|
|
24790
24803
|
// src/boot-note.ts
|
|
24791
24804
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
|
|
24792
24805
|
import { dirname as dirname2, join as join3 } from "node:path";
|
|
@@ -24923,12 +24936,161 @@ function resolveWithinProject(projectPath, candidatePath) {
|
|
|
24923
24936
|
}
|
|
24924
24937
|
|
|
24925
24938
|
// src/process-children.ts
|
|
24926
|
-
import { execFile
|
|
24939
|
+
import { execFile } from "node:child_process";
|
|
24927
24940
|
import { basename } from "node:path";
|
|
24928
24941
|
import { promisify } from "node:util";
|
|
24942
|
+
|
|
24943
|
+
// src/windows-ask.ts
|
|
24944
|
+
import { spawn } from "node:child_process";
|
|
24945
|
+
import process6 from "node:process";
|
|
24946
|
+
var SCRIPT = [
|
|
24947
|
+
"[Console]::OutputEncoding = [Text.Encoding]::UTF8",
|
|
24948
|
+
"$in = [Console]::In",
|
|
24949
|
+
"while ($null -ne ($line = $in.ReadLine())) {",
|
|
24950
|
+
' $parts = $line.Split(" ", 2)',
|
|
24951
|
+
" try {",
|
|
24952
|
+
" $asked = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($parts[1]))",
|
|
24953
|
+
" $said = (& ([ScriptBlock]::Create($asked)) | Out-String -Width 1000000)",
|
|
24954
|
+
' $tag = "OK"',
|
|
24955
|
+
" } catch {",
|
|
24956
|
+
" $said = $_.Exception.Message",
|
|
24957
|
+
' $tag = "ERR"',
|
|
24958
|
+
" }",
|
|
24959
|
+
' [Console]::Out.Write($parts[0] + " " + $tag + " " + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($said)) + "`n")',
|
|
24960
|
+
" [Console]::Out.Flush()",
|
|
24961
|
+
"}"
|
|
24962
|
+
].join(`
|
|
24963
|
+
`);
|
|
24964
|
+
|
|
24965
|
+
class WindowsAskFailure extends Error {
|
|
24966
|
+
constructor(how) {
|
|
24967
|
+
super(`PowerShell ${how}`);
|
|
24968
|
+
}
|
|
24969
|
+
}
|
|
24970
|
+
var helper = null;
|
|
24971
|
+
var buffered = "";
|
|
24972
|
+
var next = 1;
|
|
24973
|
+
var out = null;
|
|
24974
|
+
var queued = [];
|
|
24975
|
+
var held = false;
|
|
24976
|
+
function holding(child, wanted) {
|
|
24977
|
+
if (held === wanted) {
|
|
24978
|
+
return;
|
|
24979
|
+
}
|
|
24980
|
+
held = wanted;
|
|
24981
|
+
const stdout = child.stdout;
|
|
24982
|
+
if (wanted) {
|
|
24983
|
+
stdout.ref?.();
|
|
24984
|
+
} else {
|
|
24985
|
+
stdout.unref?.();
|
|
24986
|
+
}
|
|
24987
|
+
}
|
|
24988
|
+
function running() {
|
|
24989
|
+
if (helper?.exitCode === null && helper.signalCode === null) {
|
|
24990
|
+
return helper;
|
|
24991
|
+
}
|
|
24992
|
+
const child = spawn("powershell.exe", [
|
|
24993
|
+
"-NoProfile",
|
|
24994
|
+
"-NonInteractive",
|
|
24995
|
+
"-ExecutionPolicy",
|
|
24996
|
+
"Bypass",
|
|
24997
|
+
"-EncodedCommand",
|
|
24998
|
+
Buffer.from(SCRIPT, "utf16le").toString("base64")
|
|
24999
|
+
], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
|
|
25000
|
+
buffered = "";
|
|
25001
|
+
child.stdout.setEncoding("utf8");
|
|
25002
|
+
child.stdout.on("data", (chunk) => {
|
|
25003
|
+
buffered += chunk;
|
|
25004
|
+
for (let end = buffered.indexOf(`
|
|
25005
|
+
`);end !== -1; end = buffered.indexOf(`
|
|
25006
|
+
`)) {
|
|
25007
|
+
answered(buffered.slice(0, end).trim());
|
|
25008
|
+
buffered = buffered.slice(end + 1);
|
|
25009
|
+
}
|
|
25010
|
+
});
|
|
25011
|
+
child.stderr.resume();
|
|
25012
|
+
child.stdin.on("error", () => {});
|
|
25013
|
+
const gone = () => {
|
|
25014
|
+
if (helper !== child) {
|
|
25015
|
+
return;
|
|
25016
|
+
}
|
|
25017
|
+
helper = null;
|
|
25018
|
+
if (out !== null) {
|
|
25019
|
+
clearTimeout(out.timer);
|
|
25020
|
+
out.question.reject(new WindowsAskFailure("exited before it answered"));
|
|
25021
|
+
out = null;
|
|
25022
|
+
}
|
|
25023
|
+
sendNext();
|
|
25024
|
+
};
|
|
25025
|
+
child.once("exit", gone);
|
|
25026
|
+
child.once("error", gone);
|
|
25027
|
+
child.unref();
|
|
25028
|
+
child.stdin.unref?.();
|
|
25029
|
+
child.stderr.unref?.();
|
|
25030
|
+
held = true;
|
|
25031
|
+
holding(child, false);
|
|
25032
|
+
helper = child;
|
|
25033
|
+
return child;
|
|
25034
|
+
}
|
|
25035
|
+
function answered(line) {
|
|
25036
|
+
const [id, tag, body] = line.split(" ");
|
|
25037
|
+
if (out === null || out.id !== Number(id)) {
|
|
25038
|
+
return;
|
|
25039
|
+
}
|
|
25040
|
+
const { question, timer } = out;
|
|
25041
|
+
clearTimeout(timer);
|
|
25042
|
+
out = null;
|
|
25043
|
+
const said = Buffer.from(body ?? "", "base64").toString("utf8");
|
|
25044
|
+
if (tag === "OK") {
|
|
25045
|
+
question.resolve(said);
|
|
25046
|
+
} else {
|
|
25047
|
+
question.reject(new WindowsAskFailure(`refused the question: ${said.trim() || "no reason given"}`));
|
|
25048
|
+
}
|
|
25049
|
+
sendNext();
|
|
25050
|
+
}
|
|
25051
|
+
function sendNext() {
|
|
25052
|
+
const question = queued.shift();
|
|
25053
|
+
if (question === undefined) {
|
|
25054
|
+
if (helper !== null) {
|
|
25055
|
+
holding(helper, false);
|
|
25056
|
+
}
|
|
25057
|
+
return;
|
|
25058
|
+
}
|
|
25059
|
+
const child = running();
|
|
25060
|
+
holding(child, true);
|
|
25061
|
+
const id = next;
|
|
25062
|
+
next += 1;
|
|
25063
|
+
const timer = setTimeout(() => {
|
|
25064
|
+
if (out?.id !== id) {
|
|
25065
|
+
return;
|
|
25066
|
+
}
|
|
25067
|
+
out = null;
|
|
25068
|
+
question.reject(new WindowsAskFailure(`did not answer within ${question.withinMs}ms`));
|
|
25069
|
+
if (helper === child) {
|
|
25070
|
+
helper = null;
|
|
25071
|
+
}
|
|
25072
|
+
child.kill();
|
|
25073
|
+
sendNext();
|
|
25074
|
+
}, question.withinMs);
|
|
25075
|
+
out = { id, question, timer };
|
|
25076
|
+
child.stdin.write(`${id} ${Buffer.from(question.script, "utf8").toString("base64")}
|
|
25077
|
+
`);
|
|
25078
|
+
}
|
|
25079
|
+
function askWindows(script, withinMs) {
|
|
25080
|
+
if (process6.platform !== "win32") {
|
|
25081
|
+
return Promise.reject(new WindowsAskFailure("is asked only on Windows"));
|
|
25082
|
+
}
|
|
25083
|
+
return new Promise((resolve, reject) => {
|
|
25084
|
+
queued.push({ script, withinMs, resolve, reject });
|
|
25085
|
+
if (out === null) {
|
|
25086
|
+
sendNext();
|
|
25087
|
+
}
|
|
25088
|
+
});
|
|
25089
|
+
}
|
|
25090
|
+
|
|
25091
|
+
// src/process-children.ts
|
|
24929
25092
|
var run = promisify(execFile);
|
|
24930
|
-
|
|
24931
|
-
function startTimesOf(pids) {
|
|
25093
|
+
async function startTimesOf(pids) {
|
|
24932
25094
|
const began = new Map;
|
|
24933
25095
|
if (pids.length === 0) {
|
|
24934
25096
|
return began;
|
|
@@ -24936,12 +25098,7 @@ function startTimesOf(pids) {
|
|
|
24936
25098
|
try {
|
|
24937
25099
|
if (process.platform === "win32") {
|
|
24938
25100
|
const filter = pids.map((pid) => `ProcessId=${pid}`).join(" OR ");
|
|
24939
|
-
const said =
|
|
24940
|
-
"-NoProfile",
|
|
24941
|
-
"-NonInteractive",
|
|
24942
|
-
"-Command",
|
|
24943
|
-
`Get-CimInstance Win32_Process -Filter "${filter}" | ForEach-Object { "$($_.ProcessId) $(([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds())" }`
|
|
24944
|
-
], { encoding: "utf8", timeout: ASK_TIMEOUT_MS, windowsHide: true });
|
|
25101
|
+
const said = await askWindows(`Get-CimInstance Win32_Process -Filter "${filter}" | ForEach-Object { "$($_.ProcessId) $(([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds())" }`, ASK_TIMEOUT_MS);
|
|
24945
25102
|
for (const line of said.split(`
|
|
24946
25103
|
`)) {
|
|
24947
25104
|
const found = /^\s*(\d+)\s+(\d+)\s*$/.exec(line);
|
|
@@ -24951,11 +25108,10 @@ function startTimesOf(pids) {
|
|
|
24951
25108
|
}
|
|
24952
25109
|
return began;
|
|
24953
25110
|
}
|
|
24954
|
-
const said =
|
|
24955
|
-
|
|
24956
|
-
|
|
24957
|
-
|
|
24958
|
-
}).stdout;
|
|
25111
|
+
const said = await printedBy("ps", ["-p", pids.join(","), "-o", "pid=,lstart="], {
|
|
25112
|
+
...process.env,
|
|
25113
|
+
LC_ALL: "C"
|
|
25114
|
+
});
|
|
24959
25115
|
for (const line of said.split(`
|
|
24960
25116
|
`)) {
|
|
24961
25117
|
const found = /^\s*(\d+)\s+(.+)$/.exec(line);
|
|
@@ -24967,18 +25123,41 @@ function startTimesOf(pids) {
|
|
|
24967
25123
|
} catch {}
|
|
24968
25124
|
return began;
|
|
24969
25125
|
}
|
|
25126
|
+
function printedBy(command, args, env) {
|
|
25127
|
+
return new Promise((resolve) => {
|
|
25128
|
+
execFile(command, [...args], { encoding: "utf8", timeout: ASK_TIMEOUT_MS, env }, (_error, stdout) => {
|
|
25129
|
+
resolve(stdout);
|
|
25130
|
+
});
|
|
25131
|
+
});
|
|
25132
|
+
}
|
|
24970
25133
|
var ASK_TIMEOUT_MS = 15000;
|
|
24971
25134
|
async function processTree() {
|
|
24972
25135
|
try {
|
|
24973
|
-
const tree = process.platform === "win32" ? parseProcessTable(await
|
|
25136
|
+
const tree = process.platform === "win32" ? parseProcessTable(await listWindows(), true) : parseProcessTable(await askPosix());
|
|
24974
25137
|
lastListingFailure = undefined;
|
|
24975
25138
|
return tree;
|
|
24976
25139
|
} catch (error) {
|
|
24977
|
-
lastListingFailure =
|
|
25140
|
+
lastListingFailure = listingFailure(error);
|
|
24978
25141
|
return;
|
|
24979
25142
|
}
|
|
24980
25143
|
}
|
|
24981
25144
|
var lastListingFailure;
|
|
25145
|
+
function listingFailure(error) {
|
|
25146
|
+
if (error instanceof WindowsAskFailure) {
|
|
25147
|
+
return error.message;
|
|
25148
|
+
}
|
|
25149
|
+
if (!(error instanceof Error)) {
|
|
25150
|
+
return String(error);
|
|
25151
|
+
}
|
|
25152
|
+
const failed = error;
|
|
25153
|
+
const how = failed.killed === true ? `was killed after the ${ASK_TIMEOUT_MS}ms budget${failed.signal ? ` (${failed.signal})` : ""}` : failed.signal ? `was ended by ${failed.signal}` : failed.code !== undefined && failed.code !== null ? `exited with ${failed.code}` : "failed";
|
|
25154
|
+
const said = typeof failed.stderr === "string" ? failed.stderr.trim() : "";
|
|
25155
|
+
const asked = basename((failed.cmd ?? "").split(" ")[0] ?? "") || "the listing";
|
|
25156
|
+
return `${asked} ${how}${said === "" ? ", with nothing on stderr" : `; stderr: ${said.slice(0, 400)}`}`;
|
|
25157
|
+
}
|
|
25158
|
+
function whyTheProcessTreeFailed() {
|
|
25159
|
+
return lastListingFailure;
|
|
25160
|
+
}
|
|
24982
25161
|
function parseProcessTable(printed, started = false) {
|
|
24983
25162
|
const tree = new Map;
|
|
24984
25163
|
for (const line of printed.split(/\r?\n/)) {
|
|
@@ -25108,14 +25287,8 @@ function commandWords(command) {
|
|
|
25108
25287
|
}
|
|
25109
25288
|
return words;
|
|
25110
25289
|
}
|
|
25111
|
-
|
|
25112
|
-
|
|
25113
|
-
"-NoProfile",
|
|
25114
|
-
"-NonInteractive",
|
|
25115
|
-
"-Command",
|
|
25116
|
-
`${POWERSHELL_UTF8}Get-CimInstance Win32_Process -Property ProcessId,ParentProcessId,CreationDate,CommandLine | ForEach-Object { $began = 0; if ($_.CreationDate) { $began = ([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds() }; "$($_.ProcessId) $($_.ParentProcessId) $began $($_.CommandLine)" }`
|
|
25117
|
-
], { timeout: ASK_TIMEOUT_MS, windowsHide: true, maxBuffer: 16 * 1024 * 1024 });
|
|
25118
|
-
return stdout;
|
|
25290
|
+
function listWindows() {
|
|
25291
|
+
return askWindows('Get-CimInstance Win32_Process -Property ProcessId,ParentProcessId,CreationDate,CommandLine | ForEach-Object { $began = 0; if ($_.CreationDate) { $began = ([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds() }; "$($_.ProcessId) $($_.ParentProcessId) $began $($_.CommandLine)" }', ASK_TIMEOUT_MS);
|
|
25119
25292
|
}
|
|
25120
25293
|
async function askPosix() {
|
|
25121
25294
|
const { stdout } = await run("ps", ["-e", "-o", "pid=,ppid=,args="], {
|
|
@@ -25234,14 +25407,6 @@ function reportAlreadyInTranscript(transcript, report) {
|
|
|
25234
25407
|
return end <= report.length && report.subarray(boundary, end).equals(copied) ? end : report.length;
|
|
25235
25408
|
}
|
|
25236
25409
|
var ERROR_REPORT_KEEP_MS = 60 * 60 * 1000;
|
|
25237
|
-
function processAlive(pid) {
|
|
25238
|
-
try {
|
|
25239
|
-
process.kill(pid, 0);
|
|
25240
|
-
return true;
|
|
25241
|
-
} catch (error) {
|
|
25242
|
-
return error.code === "EPERM";
|
|
25243
|
-
}
|
|
25244
|
-
}
|
|
25245
25410
|
function parseAnnouncement(file, pid) {
|
|
25246
25411
|
let fields;
|
|
25247
25412
|
try {
|
|
@@ -25328,6 +25493,7 @@ var STARTED_AFTER_ANNOUNCING_MS = 2000;
|
|
|
25328
25493
|
var CONFIRMED_FOR_MS = 60000;
|
|
25329
25494
|
var JUDGED_AFTER_MS = 60000;
|
|
25330
25495
|
var verdicts = new Map;
|
|
25496
|
+
var judging = null;
|
|
25331
25497
|
function strangersAmong(candidates, now, startTimes = startTimesOf) {
|
|
25332
25498
|
const toJudge = candidates.filter((one) => {
|
|
25333
25499
|
if (now - one.writtenAt < JUDGED_AFTER_MS) {
|
|
@@ -25336,15 +25502,21 @@ function strangersAmong(candidates, now, startTimes = startTimesOf) {
|
|
|
25336
25502
|
const known = verdicts.get(one.file);
|
|
25337
25503
|
return known === undefined || known.writtenAt !== one.writtenAt || !known.stranger && now - known.judgedAt > CONFIRMED_FOR_MS;
|
|
25338
25504
|
});
|
|
25339
|
-
if (toJudge.length > 0) {
|
|
25340
|
-
|
|
25341
|
-
|
|
25342
|
-
|
|
25343
|
-
|
|
25344
|
-
|
|
25345
|
-
|
|
25505
|
+
if (toJudge.length > 0 && judging === null) {
|
|
25506
|
+
judging = startTimes(toJudge.map((one) => one.pid)).then((began) => {
|
|
25507
|
+
for (const one of toJudge) {
|
|
25508
|
+
const at = began.get(one.pid);
|
|
25509
|
+
const stranger = at !== undefined && at - one.writtenAt > STARTED_AFTER_ANNOUNCING_MS;
|
|
25510
|
+
verdicts.set(one.file, { writtenAt: one.writtenAt, judgedAt: now, stranger });
|
|
25511
|
+
}
|
|
25512
|
+
}).catch(() => {}).finally(() => {
|
|
25513
|
+
judging = null;
|
|
25514
|
+
});
|
|
25346
25515
|
}
|
|
25347
|
-
return new Set(candidates.filter((one) =>
|
|
25516
|
+
return new Set(candidates.filter((one) => {
|
|
25517
|
+
const known = verdicts.get(one.file);
|
|
25518
|
+
return known?.stranger === true && known.writtenAt === one.writtenAt;
|
|
25519
|
+
}).map((one) => one.file));
|
|
25348
25520
|
}
|
|
25349
25521
|
function announcedIn(directory) {
|
|
25350
25522
|
if (!existsSync2(directory)) {
|
|
@@ -25365,17 +25537,17 @@ function announcedIn(directory) {
|
|
|
25365
25537
|
}
|
|
25366
25538
|
const file = join2(directory, entry);
|
|
25367
25539
|
const pid = Number.parseInt(match[1] ?? "", 10);
|
|
25368
|
-
const
|
|
25369
|
-
entries.push({ file, pid,
|
|
25370
|
-
if (
|
|
25540
|
+
const running = alive(pid);
|
|
25541
|
+
entries.push({ file, pid, running });
|
|
25542
|
+
if (running) {
|
|
25371
25543
|
try {
|
|
25372
25544
|
candidates.push({ file, pid, writtenAt: statSync(file).mtimeMs });
|
|
25373
25545
|
} catch {}
|
|
25374
25546
|
}
|
|
25375
25547
|
}
|
|
25376
25548
|
const strangers = strangersAmong(candidates, Date.now());
|
|
25377
|
-
for (const { file, pid,
|
|
25378
|
-
const gone = !
|
|
25549
|
+
for (const { file, pid, running } of entries) {
|
|
25550
|
+
const gone = !running || strangers.has(file);
|
|
25379
25551
|
const announced = gone ? { kind: "rubbish" } : parseAnnouncement(file, pid);
|
|
25380
25552
|
if (announced.kind === "rubbish") {
|
|
25381
25553
|
if (!gone) {
|
|
@@ -25407,7 +25579,7 @@ async function announcementEnded(pid, directories = runtimeDirectories()) {
|
|
|
25407
25579
|
return;
|
|
25408
25580
|
}
|
|
25409
25581
|
const until = Date.now() + ENDED_WITHIN_MS;
|
|
25410
|
-
while (
|
|
25582
|
+
while (alive(pid)) {
|
|
25411
25583
|
const left = until - Date.now();
|
|
25412
25584
|
if (left <= 0) {
|
|
25413
25585
|
return;
|
|
@@ -25423,7 +25595,7 @@ async function announcementEnded(pid, directories = runtimeDirectories()) {
|
|
|
25423
25595
|
}
|
|
25424
25596
|
}
|
|
25425
25597
|
function sweepErrorReport(path, pid, now = Date.now()) {
|
|
25426
|
-
if (
|
|
25598
|
+
if (alive(pid)) {
|
|
25427
25599
|
return;
|
|
25428
25600
|
}
|
|
25429
25601
|
try {
|
|
@@ -25853,6 +26025,15 @@ import { join as join6 } from "node:path";
|
|
|
25853
26025
|
var DECLARATION = /^(?:@[a-z_]+(?:\([^)\n]*\))?\s+)*class_name\s+([A-Za-z_][A-Za-z0-9_]*)/m;
|
|
25854
26026
|
function declaredClasses(projectPath) {
|
|
25855
26027
|
const declared = new Map;
|
|
26028
|
+
eachScript(projectPath, (script, source) => {
|
|
26029
|
+
const found = DECLARATION.exec(source);
|
|
26030
|
+
if (found?.[1]) {
|
|
26031
|
+
declared.set(found[1], script);
|
|
26032
|
+
}
|
|
26033
|
+
});
|
|
26034
|
+
return declared;
|
|
26035
|
+
}
|
|
26036
|
+
function eachScript(projectPath, each) {
|
|
25856
26037
|
const visit = (directory, prefix) => {
|
|
25857
26038
|
if (existsSync3(join6(directory, ".gdignore"))) {
|
|
25858
26039
|
return;
|
|
@@ -25865,15 +26046,53 @@ function declaredClasses(projectPath) {
|
|
|
25865
26046
|
if (entry.isDirectory()) {
|
|
25866
26047
|
visit(path, `${prefix}${entry.name}/`);
|
|
25867
26048
|
} else if (entry.isFile() && entry.name.endsWith(".gd")) {
|
|
25868
|
-
|
|
25869
|
-
if (found?.[1]) {
|
|
25870
|
-
declared.set(found[1], `res://${prefix}${entry.name}`);
|
|
25871
|
-
}
|
|
26049
|
+
each(`res://${prefix}${entry.name}`, readFileSync7(path, "utf8"));
|
|
25872
26050
|
}
|
|
25873
26051
|
}
|
|
25874
26052
|
};
|
|
25875
26053
|
visit(projectPath, "");
|
|
25876
|
-
|
|
26054
|
+
}
|
|
26055
|
+
function scriptsNaming(projectPath, classes) {
|
|
26056
|
+
const named = new Map(classes.map((name) => [name, ""]));
|
|
26057
|
+
const naming = [];
|
|
26058
|
+
eachScript(projectPath, (script, source) => {
|
|
26059
|
+
const declares = DECLARATION.exec(source)?.[1];
|
|
26060
|
+
if (declares !== undefined && named.has(declares)) {
|
|
26061
|
+
return;
|
|
26062
|
+
}
|
|
26063
|
+
const code = codeOf(source);
|
|
26064
|
+
if (classesNamedIn(code, named).length > 0) {
|
|
26065
|
+
naming.push({ script, tool: /^\s*@tool\b/m.test(code) });
|
|
26066
|
+
}
|
|
26067
|
+
});
|
|
26068
|
+
return naming.sort((a, b) => a.script.localeCompare(b.script));
|
|
26069
|
+
}
|
|
26070
|
+
function codeOf(source) {
|
|
26071
|
+
let code = "";
|
|
26072
|
+
let at = 0;
|
|
26073
|
+
while (at < source.length) {
|
|
26074
|
+
const here = source[at] ?? "";
|
|
26075
|
+
if (here === "#") {
|
|
26076
|
+
const end = source.indexOf(`
|
|
26077
|
+
`, at);
|
|
26078
|
+
at = end === -1 ? source.length : end;
|
|
26079
|
+
continue;
|
|
26080
|
+
}
|
|
26081
|
+
if (here === '"' || here === "'") {
|
|
26082
|
+
const fence = source.startsWith(here.repeat(3), at) ? here.repeat(3) : here;
|
|
26083
|
+
let end = at + fence.length;
|
|
26084
|
+
while (end < source.length && !source.startsWith(fence, end)) {
|
|
26085
|
+
end += source[end] === "\\" ? 2 : 1;
|
|
26086
|
+
}
|
|
26087
|
+
const literal = source.slice(at, Math.min(end + fence.length, source.length));
|
|
26088
|
+
code += literal.replace(/[^\n]/g, " ");
|
|
26089
|
+
at = end + fence.length;
|
|
26090
|
+
continue;
|
|
26091
|
+
}
|
|
26092
|
+
code += here;
|
|
26093
|
+
at += 1;
|
|
26094
|
+
}
|
|
26095
|
+
return code;
|
|
25877
26096
|
}
|
|
25878
26097
|
function cachedClasses(projectPath) {
|
|
25879
26098
|
const cache = join6(projectPath, ".godot", "global_script_class_cache.cfg");
|
|
@@ -26697,6 +26916,9 @@ class GodotDAPClient {
|
|
|
26697
26916
|
isConnected() {
|
|
26698
26917
|
return this.connected;
|
|
26699
26918
|
}
|
|
26919
|
+
connection() {
|
|
26920
|
+
return this.connected ? this.socket : null;
|
|
26921
|
+
}
|
|
26700
26922
|
isStopped() {
|
|
26701
26923
|
return this.halt !== null;
|
|
26702
26924
|
}
|
|
@@ -27837,14 +28059,48 @@ class GodotLocator {
|
|
|
27837
28059
|
|
|
27838
28060
|
// src/headless.ts
|
|
27839
28061
|
import { execFile as execFile3 } from "node:child_process";
|
|
27840
|
-
import {
|
|
27841
|
-
import {
|
|
27842
|
-
import { join as join11 } from "node:path";
|
|
28062
|
+
import { writeFileSync as writeFileSync7 } from "node:fs";
|
|
28063
|
+
import { join as join12 } from "node:path";
|
|
27843
28064
|
import { promisify as promisify3 } from "node:util";
|
|
27844
28065
|
|
|
27845
28066
|
// src/scratch.ts
|
|
27846
|
-
import { rmSync as rmSync4 } from "node:fs";
|
|
27847
|
-
|
|
28067
|
+
import { existsSync as existsSync8, mkdtempSync, readdirSync as readdirSync4, rmSync as rmSync4 } from "node:fs";
|
|
28068
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
28069
|
+
import { join as join11 } from "node:path";
|
|
28070
|
+
import process7 from "node:process";
|
|
28071
|
+
var SWEPT = { recursive: true, force: true };
|
|
28072
|
+
var AGAIN_AFTER_MS = [250, 500, 1000, 2000, 4000, 8000, 16000];
|
|
28073
|
+
var waiting = new Set;
|
|
28074
|
+
var atExit = new Set;
|
|
28075
|
+
var KINDS = [
|
|
28076
|
+
"desktop",
|
|
28077
|
+
"export",
|
|
28078
|
+
"import",
|
|
28079
|
+
"params",
|
|
28080
|
+
"runtime-screenshot",
|
|
28081
|
+
"tests"
|
|
28082
|
+
];
|
|
28083
|
+
function scratchDirectory(kind) {
|
|
28084
|
+
return mkdtempSync(join11(tmpdir3(), `gdharness-${kind}-${process7.pid}-`));
|
|
28085
|
+
}
|
|
28086
|
+
function sweepAbandonedScratch(directory = tmpdir3()) {
|
|
28087
|
+
let entries;
|
|
28088
|
+
try {
|
|
28089
|
+
entries = readdirSync4(directory);
|
|
28090
|
+
} catch {
|
|
28091
|
+
return;
|
|
28092
|
+
}
|
|
28093
|
+
const named = new RegExp(`^gdharness-(${KINDS.join("|")})-(\\d+)-[A-Za-z0-9]{6}$`);
|
|
28094
|
+
for (const entry of entries) {
|
|
28095
|
+
const owner = named.exec(entry)?.[2];
|
|
28096
|
+
if (owner === undefined || alive(Number(owner))) {
|
|
28097
|
+
continue;
|
|
28098
|
+
}
|
|
28099
|
+
try {
|
|
28100
|
+
remove(join11(directory, entry));
|
|
28101
|
+
} catch {}
|
|
28102
|
+
}
|
|
28103
|
+
}
|
|
27848
28104
|
function discardWith(remove, paths) {
|
|
27849
28105
|
const left = [];
|
|
27850
28106
|
for (const path of paths) {
|
|
@@ -27860,9 +28116,54 @@ function discardWith(remove, paths) {
|
|
|
27860
28116
|
return left;
|
|
27861
28117
|
}
|
|
27862
28118
|
function discard(...paths) {
|
|
27863
|
-
|
|
27864
|
-
|
|
27865
|
-
|
|
28119
|
+
const left = discardWith(remove, paths);
|
|
28120
|
+
for (const { path } of left) {
|
|
28121
|
+
tryAgain(path, 0);
|
|
28122
|
+
}
|
|
28123
|
+
return left;
|
|
28124
|
+
}
|
|
28125
|
+
function remove(path) {
|
|
28126
|
+
rmSync4(path, SWEPT);
|
|
28127
|
+
}
|
|
28128
|
+
function tryAgain(path, attempt) {
|
|
28129
|
+
if (attempt === 0) {
|
|
28130
|
+
if (waiting.has(path)) {
|
|
28131
|
+
return;
|
|
28132
|
+
}
|
|
28133
|
+
waiting.add(path);
|
|
28134
|
+
atExit.delete(path);
|
|
28135
|
+
lastTryOnExit();
|
|
28136
|
+
}
|
|
28137
|
+
const after = AGAIN_AFTER_MS[attempt];
|
|
28138
|
+
if (after === undefined || !existsSync8(path)) {
|
|
28139
|
+
waiting.delete(path);
|
|
28140
|
+
if (after === undefined) {
|
|
28141
|
+
atExit.add(path);
|
|
28142
|
+
}
|
|
28143
|
+
return;
|
|
28144
|
+
}
|
|
28145
|
+
setTimeout(() => {
|
|
28146
|
+
try {
|
|
28147
|
+
remove(path);
|
|
28148
|
+
waiting.delete(path);
|
|
28149
|
+
} catch {
|
|
28150
|
+
tryAgain(path, attempt + 1);
|
|
28151
|
+
}
|
|
28152
|
+
}, after).unref();
|
|
28153
|
+
}
|
|
28154
|
+
var lastTryWatched = false;
|
|
28155
|
+
function lastTryOnExit() {
|
|
28156
|
+
if (lastTryWatched) {
|
|
28157
|
+
return;
|
|
28158
|
+
}
|
|
28159
|
+
lastTryWatched = true;
|
|
28160
|
+
process7.once("exit", () => {
|
|
28161
|
+
for (const path of [...waiting, ...atExit]) {
|
|
28162
|
+
try {
|
|
28163
|
+
remove(path);
|
|
28164
|
+
} catch {}
|
|
28165
|
+
}
|
|
28166
|
+
});
|
|
27866
28167
|
}
|
|
27867
28168
|
|
|
27868
28169
|
// src/headless.ts
|
|
@@ -27905,9 +28206,9 @@ function reason(stdout, stderr) {
|
|
|
27905
28206
|
return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
|
|
27906
28207
|
}
|
|
27907
28208
|
async function runImport(godotPath, projectPath) {
|
|
27908
|
-
const logDir =
|
|
28209
|
+
const logDir = scratchDirectory("import");
|
|
27909
28210
|
try {
|
|
27910
|
-
const { stderr } = await run3(godotPath, ["--headless", "--log-file",
|
|
28211
|
+
const { stderr } = await run3(godotPath, ["--headless", "--log-file", join12(logDir, "engine.log"), "--path", projectPath, "--import"], { signal: callSignal() });
|
|
27911
28212
|
return { ok: true, messages: problems(stderr) };
|
|
27912
28213
|
} catch (error) {
|
|
27913
28214
|
if (error instanceof Error && "stdout" in error && "stderr" in error) {
|
|
@@ -27928,13 +28229,13 @@ async function runImport(godotPath, projectPath) {
|
|
|
27928
28229
|
}
|
|
27929
28230
|
}
|
|
27930
28231
|
async function runOperation(engine, operation, params, projectPath) {
|
|
27931
|
-
const paramsDir =
|
|
27932
|
-
const paramsFile =
|
|
28232
|
+
const paramsDir = scratchDirectory("params");
|
|
28233
|
+
const paramsFile = join12(paramsDir, `${operation}.json`);
|
|
27933
28234
|
writeFileSync7(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
|
|
27934
28235
|
const args = [
|
|
27935
28236
|
"--headless",
|
|
27936
28237
|
"--log-file",
|
|
27937
|
-
|
|
28238
|
+
join12(paramsDir, "engine.log"),
|
|
27938
28239
|
"--path",
|
|
27939
28240
|
projectPath,
|
|
27940
28241
|
"--script",
|
|
@@ -28888,7 +29189,7 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
28888
29189
|
}
|
|
28889
29190
|
|
|
28890
29191
|
// src/outside.ts
|
|
28891
|
-
import { spawn } from "node:child_process";
|
|
29192
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
28892
29193
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
28893
29194
|
function changesFrom(env, base) {
|
|
28894
29195
|
const changes = {};
|
|
@@ -28922,7 +29223,7 @@ function readLaunched(printed) {
|
|
|
28922
29223
|
}
|
|
28923
29224
|
async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = KEEPER_SCRIPT) {
|
|
28924
29225
|
return await new Promise((resolve) => {
|
|
28925
|
-
const launcher =
|
|
29226
|
+
const launcher = spawn2(runtime, [keeper, "launch"], {
|
|
28926
29227
|
stdio: ["pipe", "pipe", "pipe"],
|
|
28927
29228
|
windowsHide: true
|
|
28928
29229
|
});
|
|
@@ -28959,14 +29260,14 @@ var run4 = promisify4(execFile4);
|
|
|
28959
29260
|
var ASK_TIMEOUT_MS2 = 6000;
|
|
28960
29261
|
async function cpuSecondsOf(pid) {
|
|
28961
29262
|
try {
|
|
28962
|
-
return process.platform === "win32" ? await
|
|
29263
|
+
return process.platform === "win32" ? await secondsOnWindows(pid) : await askPosix2(pid);
|
|
28963
29264
|
} catch {
|
|
28964
29265
|
return;
|
|
28965
29266
|
}
|
|
28966
29267
|
}
|
|
28967
|
-
async function
|
|
28968
|
-
const
|
|
28969
|
-
const seconds = Number(
|
|
29268
|
+
async function secondsOnWindows(pid) {
|
|
29269
|
+
const said = await askWindows(`(Get-Process -Id ${pid}).CPU`, ASK_TIMEOUT_MS2);
|
|
29270
|
+
const seconds = Number(said.trim().replace(",", "."));
|
|
28970
29271
|
return Number.isFinite(seconds) ? seconds : undefined;
|
|
28971
29272
|
}
|
|
28972
29273
|
async function askPosix2(pid) {
|
|
@@ -28990,14 +29291,14 @@ function secondsFromClock(printed) {
|
|
|
28990
29291
|
}
|
|
28991
29292
|
|
|
28992
29293
|
// src/project-scan.ts
|
|
28993
|
-
import { existsSync as
|
|
28994
|
-
import { join as
|
|
29294
|
+
import { existsSync as existsSync9, readdirSync as readdirSync5, readFileSync as readFileSync11 } from "node:fs";
|
|
29295
|
+
import { join as join13 } from "node:path";
|
|
28995
29296
|
var SKIPPED = new Set(["node_modules"]);
|
|
28996
29297
|
function skipped(name) {
|
|
28997
29298
|
return name.startsWith(".") || SKIPPED.has(name);
|
|
28998
29299
|
}
|
|
28999
29300
|
function steppedOver(directory) {
|
|
29000
|
-
return
|
|
29301
|
+
return existsSync9(join13(directory, ".gdignore"));
|
|
29001
29302
|
}
|
|
29002
29303
|
var ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
|
|
29003
29304
|
function projectStructure(projectPath) {
|
|
@@ -29006,12 +29307,12 @@ function projectStructure(projectPath) {
|
|
|
29006
29307
|
if (steppedOver(directory)) {
|
|
29007
29308
|
return;
|
|
29008
29309
|
}
|
|
29009
|
-
for (const entry of
|
|
29310
|
+
for (const entry of readdirSync5(directory, { withFileTypes: true })) {
|
|
29010
29311
|
if (skipped(entry.name)) {
|
|
29011
29312
|
continue;
|
|
29012
29313
|
}
|
|
29013
29314
|
if (entry.isDirectory()) {
|
|
29014
|
-
visit(
|
|
29315
|
+
visit(join13(directory, entry.name));
|
|
29015
29316
|
} else if (entry.isFile()) {
|
|
29016
29317
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
29017
29318
|
if (extension === "tscn") {
|
|
@@ -29036,20 +29337,20 @@ function scriptsWithoutUid(projectPath) {
|
|
|
29036
29337
|
if (steppedOver(directory)) {
|
|
29037
29338
|
return;
|
|
29038
29339
|
}
|
|
29039
|
-
for (const entry of
|
|
29340
|
+
for (const entry of readdirSync5(directory, { withFileTypes: true })) {
|
|
29040
29341
|
if (skipped(entry.name)) {
|
|
29041
29342
|
continue;
|
|
29042
29343
|
}
|
|
29043
29344
|
const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
29044
29345
|
if (entry.isDirectory()) {
|
|
29045
|
-
visit(
|
|
29346
|
+
visit(join13(directory, entry.name), spelled);
|
|
29046
29347
|
continue;
|
|
29047
29348
|
}
|
|
29048
29349
|
if (!entry.isFile()) {
|
|
29049
29350
|
continue;
|
|
29050
29351
|
}
|
|
29051
29352
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
29052
|
-
if (CARRIES_A_UID.has(extension) && !
|
|
29353
|
+
if (CARRIES_A_UID.has(extension) && !existsSync9(join13(directory, `${entry.name}.uid`))) {
|
|
29053
29354
|
found.push(spelled);
|
|
29054
29355
|
}
|
|
29055
29356
|
}
|
|
@@ -29077,14 +29378,14 @@ function searchProject(projectPath, options) {
|
|
|
29077
29378
|
if (steppedOver(directory)) {
|
|
29078
29379
|
return;
|
|
29079
29380
|
}
|
|
29080
|
-
for (const entry of
|
|
29381
|
+
for (const entry of readdirSync5(directory, { withFileTypes: true })) {
|
|
29081
29382
|
if (full()) {
|
|
29082
29383
|
return;
|
|
29083
29384
|
}
|
|
29084
29385
|
if (skipped(entry.name)) {
|
|
29085
29386
|
continue;
|
|
29086
29387
|
}
|
|
29087
|
-
const entryPath =
|
|
29388
|
+
const entryPath = join13(directory, entry.name);
|
|
29088
29389
|
if (entry.isDirectory()) {
|
|
29089
29390
|
visit(entryPath);
|
|
29090
29391
|
continue;
|
|
@@ -29119,12 +29420,12 @@ function searchProject(projectPath, options) {
|
|
|
29119
29420
|
|
|
29120
29421
|
// src/reimport.ts
|
|
29121
29422
|
import { createHash as createHash2 } from "node:crypto";
|
|
29122
|
-
import { existsSync as
|
|
29123
|
-
import { basename as basename2, join as
|
|
29423
|
+
import { existsSync as existsSync10, readFileSync as readFileSync12, rmSync as rmSync5, statSync as statSync5, utimesSync, writeFileSync as writeFileSync8 } from "node:fs";
|
|
29424
|
+
import { basename as basename2, join as join14 } from "node:path";
|
|
29124
29425
|
function forceNextImport(projectPath, resourcePath) {
|
|
29125
29426
|
const relative = resourcePath.replace(/^res:\/\//, "");
|
|
29126
|
-
const sidecar =
|
|
29127
|
-
if (!
|
|
29427
|
+
const sidecar = join14(projectPath, `${relative}.import`);
|
|
29428
|
+
if (!existsSync10(sidecar)) {
|
|
29128
29429
|
return;
|
|
29129
29430
|
}
|
|
29130
29431
|
const { atime, mtimeMs } = statSync5(sidecar);
|
|
@@ -29134,7 +29435,7 @@ function forceNextImport(projectPath, resourcePath) {
|
|
|
29134
29435
|
}
|
|
29135
29436
|
function importRecord(projectPath, resourcePath) {
|
|
29136
29437
|
const digest = createHash2("md5").update(resourcePath).digest("hex");
|
|
29137
|
-
return
|
|
29438
|
+
return join14(projectPath, ".godot", "imported", `${basename2(resourcePath)}-${digest}.md5`);
|
|
29138
29439
|
}
|
|
29139
29440
|
|
|
29140
29441
|
// src/resources.ts
|
|
@@ -29380,9 +29681,9 @@ function setupResourceHandlers(mcp, getProjectPath) {
|
|
|
29380
29681
|
|
|
29381
29682
|
// src/restart-note.ts
|
|
29382
29683
|
import { mkdirSync as mkdirSync7, readFileSync as readFileSync14, rmSync as rmSync6, writeFileSync as writeFileSync9 } from "node:fs";
|
|
29383
|
-
import { dirname as dirname8, join as
|
|
29684
|
+
import { dirname as dirname8, join as join15 } from "node:path";
|
|
29384
29685
|
function restartNotePath(projectPath) {
|
|
29385
|
-
return
|
|
29686
|
+
return join15(projectPath, ".godot", "gdharness-restart.json");
|
|
29386
29687
|
}
|
|
29387
29688
|
function noteRestartBegun(note) {
|
|
29388
29689
|
const path = restartNotePath(note.projectPath);
|
|
@@ -29418,28 +29719,30 @@ function restartSettled(projectPath) {
|
|
|
29418
29719
|
}
|
|
29419
29720
|
|
|
29420
29721
|
// src/run-record.ts
|
|
29421
|
-
import {
|
|
29722
|
+
import { execFile as execFile5 } from "node:child_process";
|
|
29422
29723
|
import { createHash as createHash3 } from "node:crypto";
|
|
29423
29724
|
import {
|
|
29424
29725
|
mkdirSync as mkdirSync8,
|
|
29425
29726
|
openSync as openSync2,
|
|
29426
|
-
readdirSync as
|
|
29727
|
+
readdirSync as readdirSync6,
|
|
29427
29728
|
readFileSync as readFileSync15,
|
|
29428
29729
|
realpathSync as realpathSync3,
|
|
29429
29730
|
rmSync as rmSync7,
|
|
29430
29731
|
statSync as statSync6,
|
|
29431
29732
|
writeFileSync as writeFileSync10
|
|
29432
29733
|
} from "node:fs";
|
|
29433
|
-
import { basename as basename3, dirname as dirname9, join as
|
|
29734
|
+
import { basename as basename3, dirname as dirname9, join as join16, resolve as resolve5 } from "node:path";
|
|
29735
|
+
import { promisify as promisify5 } from "node:util";
|
|
29736
|
+
var execFileAsync = promisify5(execFile5);
|
|
29434
29737
|
function runsDirectory() {
|
|
29435
|
-
return
|
|
29738
|
+
return join16(runtimeDirectory(), "runs");
|
|
29436
29739
|
}
|
|
29437
29740
|
function realAsFarAsItExists(path) {
|
|
29438
29741
|
const missing = [];
|
|
29439
29742
|
let at = path;
|
|
29440
29743
|
for (;; ) {
|
|
29441
29744
|
try {
|
|
29442
|
-
return
|
|
29745
|
+
return join16(realpathSync3.native(at), ...missing);
|
|
29443
29746
|
} catch {
|
|
29444
29747
|
const parent = dirname9(at);
|
|
29445
29748
|
if (parent === at) {
|
|
@@ -29456,16 +29759,16 @@ function projectKey(projectPath) {
|
|
|
29456
29759
|
return createHash3("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
|
|
29457
29760
|
}
|
|
29458
29761
|
function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
29459
|
-
return
|
|
29762
|
+
return join16(directory, "runs", `note-${projectKey(projectPath)}.json`);
|
|
29460
29763
|
}
|
|
29461
29764
|
var SHARED_NOTE = "run.json";
|
|
29462
29765
|
function openTranscript(startedAt) {
|
|
29463
29766
|
mkdirSync8(runsDirectory(), { recursive: true });
|
|
29464
|
-
const path =
|
|
29767
|
+
const path = join16(runsDirectory(), `run-${startedAt}.log`);
|
|
29465
29768
|
return { path, fd: openSync2(path, "a") };
|
|
29466
29769
|
}
|
|
29467
29770
|
function editorNotePath(projectPath, directory = runtimeDirectory()) {
|
|
29468
|
-
return
|
|
29771
|
+
return join16(directory, "runs", `editor-${projectKey(projectPath)}.json`);
|
|
29469
29772
|
}
|
|
29470
29773
|
function writeEditorRunNote(note) {
|
|
29471
29774
|
mkdirSync8(runsDirectory(), { recursive: true });
|
|
@@ -29473,7 +29776,7 @@ function writeEditorRunNote(note) {
|
|
|
29473
29776
|
}
|
|
29474
29777
|
function readEditorRunNote(projectPath) {
|
|
29475
29778
|
for (const directory of runtimeDirectories()) {
|
|
29476
|
-
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(
|
|
29779
|
+
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join16(directory, "runs", "editor-run.json"));
|
|
29477
29780
|
if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
|
|
29478
29781
|
return note;
|
|
29479
29782
|
}
|
|
@@ -29505,7 +29808,7 @@ function readRunRecord(projectPath) {
|
|
|
29505
29808
|
if (own !== null) {
|
|
29506
29809
|
return own;
|
|
29507
29810
|
}
|
|
29508
|
-
const shared = recordAt(
|
|
29811
|
+
const shared = recordAt(join16(directory, "runs", SHARED_NOTE));
|
|
29509
29812
|
if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
|
|
29510
29813
|
return shared;
|
|
29511
29814
|
}
|
|
@@ -29517,13 +29820,13 @@ function everyRunRecord() {
|
|
|
29517
29820
|
for (const directory of runtimeDirectories()) {
|
|
29518
29821
|
let entries;
|
|
29519
29822
|
try {
|
|
29520
|
-
entries =
|
|
29823
|
+
entries = readdirSync6(join16(directory, "runs"));
|
|
29521
29824
|
} catch {
|
|
29522
29825
|
continue;
|
|
29523
29826
|
}
|
|
29524
29827
|
for (const entry of entries) {
|
|
29525
29828
|
if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
|
|
29526
|
-
const record = recordAt(
|
|
29829
|
+
const record = recordAt(join16(directory, "runs", entry));
|
|
29527
29830
|
if (record !== null) {
|
|
29528
29831
|
found.push(record);
|
|
29529
29832
|
}
|
|
@@ -29561,49 +29864,47 @@ function recordAt(path) {
|
|
|
29561
29864
|
...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {}
|
|
29562
29865
|
};
|
|
29563
29866
|
}
|
|
29564
|
-
function runningAs(pid) {
|
|
29867
|
+
async function runningAs(pid) {
|
|
29565
29868
|
if (process.platform === "win32") {
|
|
29566
|
-
const answer = windowsCommandLine(pid);
|
|
29869
|
+
const answer = await windowsCommandLine(pid);
|
|
29567
29870
|
if (answer !== null) {
|
|
29568
29871
|
const [, ...rest] = answer.split(`
|
|
29569
29872
|
`);
|
|
29570
29873
|
const line = rest.join(`
|
|
29571
29874
|
`).trim();
|
|
29572
|
-
const began = startedAt(pid, answer);
|
|
29875
|
+
const began = await startedAt(pid, answer);
|
|
29573
29876
|
if (line !== "") {
|
|
29574
29877
|
return { kind: "commandLine", text: line, ...began === null ? {} : { startedAt: began } };
|
|
29575
29878
|
}
|
|
29576
29879
|
}
|
|
29577
|
-
const image = windowsImage(pid);
|
|
29880
|
+
const image = await windowsImage(pid);
|
|
29578
29881
|
return image === null ? null : { kind: "image", text: image };
|
|
29579
29882
|
}
|
|
29580
29883
|
try {
|
|
29581
|
-
const began = startedAt(pid, null);
|
|
29884
|
+
const began = await startedAt(pid, null);
|
|
29582
29885
|
const when = began === null ? {} : { startedAt: began };
|
|
29583
29886
|
if (process.platform === "linux") {
|
|
29584
29887
|
const raw = readFileSync15(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
29585
29888
|
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
29586
29889
|
}
|
|
29587
|
-
const args =
|
|
29588
|
-
encoding: "utf8",
|
|
29589
|
-
timeout: 15000
|
|
29590
|
-
}).trim();
|
|
29890
|
+
const args = (await execFileAsync("ps", ["-p", String(pid), "-o", "args="], { encoding: "utf8", timeout: 15000 })).stdout.trim();
|
|
29591
29891
|
return args === "" ? null : { kind: "commandLine", text: args, ...when };
|
|
29592
29892
|
} catch {
|
|
29593
29893
|
return null;
|
|
29594
29894
|
}
|
|
29595
29895
|
}
|
|
29596
|
-
function listeningPid(port) {
|
|
29896
|
+
async function listeningPid(port) {
|
|
29597
29897
|
try {
|
|
29598
29898
|
if (process.platform === "win32") {
|
|
29599
|
-
|
|
29899
|
+
const { stdout } = await execFileAsync("netstat", ["-ano"], { encoding: "utf8", timeout: 15000 });
|
|
29900
|
+
return listeningPidInNetstat(stdout, port);
|
|
29600
29901
|
}
|
|
29601
29902
|
for (const [command, args, pattern] of [
|
|
29602
29903
|
["lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], /^(\d+)/],
|
|
29603
29904
|
["ss", ["-ltnpH", `sport = :${port}`], /pid=(\d+)/]
|
|
29604
29905
|
]) {
|
|
29605
29906
|
try {
|
|
29606
|
-
const said =
|
|
29907
|
+
const said = (await execFileAsync(command, [...args], { encoding: "utf8", timeout: 15000 })).stdout.trim();
|
|
29607
29908
|
const found = pattern.exec(said);
|
|
29608
29909
|
if (found?.[1] !== undefined) {
|
|
29609
29910
|
return Number(found[1]);
|
|
@@ -29628,39 +29929,31 @@ function listeningPidInNetstat(printed, port) {
|
|
|
29628
29929
|
}
|
|
29629
29930
|
return null;
|
|
29630
29931
|
}
|
|
29631
|
-
function windowsCommandLine(pid) {
|
|
29932
|
+
async function windowsCommandLine(pid) {
|
|
29632
29933
|
try {
|
|
29633
|
-
const answer =
|
|
29634
|
-
"-NoProfile",
|
|
29635
|
-
"-NonInteractive",
|
|
29636
|
-
"-Command",
|
|
29637
|
-
`${POWERSHELL_UTF8}$p = Get-CimInstance Win32_Process -Filter "ProcessId=${pid}"; if ($p) { $p.CreationDate.ToUniversalTime().ToString("o"); $p.CommandLine }`
|
|
29638
|
-
], { encoding: "utf8", timeout: 15000, windowsHide: true }).trim();
|
|
29934
|
+
const answer = (await askWindows(`$p = Get-CimInstance Win32_Process -Filter "ProcessId=${pid}"; if ($p) { $p.CreationDate.ToUniversalTime().ToString("o"); $p.CommandLine }`, 15000)).trim();
|
|
29639
29935
|
return answer === "" ? null : answer;
|
|
29640
29936
|
} catch {
|
|
29641
29937
|
return null;
|
|
29642
29938
|
}
|
|
29643
29939
|
}
|
|
29644
|
-
function startedAt(pid, windowsAnswer) {
|
|
29940
|
+
async function startedAt(pid, windowsAnswer) {
|
|
29645
29941
|
if (process.platform === "win32") {
|
|
29646
29942
|
const when = Date.parse(windowsAnswer?.split(`
|
|
29647
29943
|
`)[0]?.trim() ?? "");
|
|
29648
29944
|
return Number.isNaN(when) ? null : when;
|
|
29649
29945
|
}
|
|
29650
29946
|
try {
|
|
29651
|
-
const said =
|
|
29652
|
-
encoding: "utf8",
|
|
29653
|
-
timeout: 15000
|
|
29654
|
-
}).trim();
|
|
29947
|
+
const said = (await execFileAsync("ps", ["-p", String(pid), "-o", "lstart="], { encoding: "utf8", timeout: 15000 })).stdout.trim();
|
|
29655
29948
|
const when = Date.parse(said);
|
|
29656
29949
|
return said === "" || Number.isNaN(when) ? null : when;
|
|
29657
29950
|
} catch {
|
|
29658
29951
|
return null;
|
|
29659
29952
|
}
|
|
29660
29953
|
}
|
|
29661
|
-
function windowsImage(pid) {
|
|
29954
|
+
async function windowsImage(pid) {
|
|
29662
29955
|
try {
|
|
29663
|
-
const csv =
|
|
29956
|
+
const { stdout: csv } = await execFileAsync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
|
|
29664
29957
|
encoding: "utf8",
|
|
29665
29958
|
timeout: 15000,
|
|
29666
29959
|
windowsHide: true
|
|
@@ -29670,11 +29963,11 @@ function windowsImage(pid) {
|
|
|
29670
29963
|
return null;
|
|
29671
29964
|
}
|
|
29672
29965
|
}
|
|
29673
|
-
function stillTheRecordedRun(record) {
|
|
29674
|
-
return judgeRun(record, runningAs(record.pid), "confirmed");
|
|
29966
|
+
async function stillTheRecordedRun(record) {
|
|
29967
|
+
return judgeRun(record, await runningAs(record.pid), "confirmed");
|
|
29675
29968
|
}
|
|
29676
|
-
function couldStillBeTheRecordedRun(record) {
|
|
29677
|
-
return judgeRun(record, runningAs(record.pid), "possible");
|
|
29969
|
+
async function couldStillBeTheRecordedRun(record) {
|
|
29970
|
+
return judgeRun(record, await runningAs(record.pid), "possible");
|
|
29678
29971
|
}
|
|
29679
29972
|
var SAME_RUN_WINDOW_MS = 1e4;
|
|
29680
29973
|
function judgeRun(record, running, needed) {
|
|
@@ -29702,8 +29995,8 @@ function judgeRun(record, running, needed) {
|
|
|
29702
29995
|
const project = process.platform === "win32" ? record.projectPath.toLowerCase() : record.projectPath;
|
|
29703
29996
|
return said.includes(project);
|
|
29704
29997
|
}
|
|
29705
|
-
function stillTheAnnouncedGame(pid, projectPath, announcedAt) {
|
|
29706
|
-
return judgeAnnouncedGame(runningAs(pid), projectPath, announcedAt);
|
|
29998
|
+
async function stillTheAnnouncedGame(pid, projectPath, announcedAt) {
|
|
29999
|
+
return judgeAnnouncedGame(await runningAs(pid), projectPath, announcedAt);
|
|
29707
30000
|
}
|
|
29708
30001
|
function judgeAnnouncedGame(running, projectPath, announcedAt) {
|
|
29709
30002
|
if (running?.kind !== "commandLine" || running.startedAt === undefined) {
|
|
@@ -29720,7 +30013,7 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
|
|
|
29720
30013
|
}
|
|
29721
30014
|
function clearRunRecord(projectPath) {
|
|
29722
30015
|
for (const directory of runtimeDirectories()) {
|
|
29723
|
-
const shared =
|
|
30016
|
+
const shared = join16(directory, "runs", SHARED_NOTE);
|
|
29724
30017
|
const sharedRecord = recordAt(shared);
|
|
29725
30018
|
const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
|
|
29726
30019
|
for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
|
|
@@ -29742,7 +30035,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
29742
30035
|
const kept = new Set(everyRunRecord().map((record) => record.transcript));
|
|
29743
30036
|
let entries;
|
|
29744
30037
|
try {
|
|
29745
|
-
entries =
|
|
30038
|
+
entries = readdirSync6(directory);
|
|
29746
30039
|
} catch {
|
|
29747
30040
|
return;
|
|
29748
30041
|
}
|
|
@@ -29750,7 +30043,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
29750
30043
|
if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
|
|
29751
30044
|
continue;
|
|
29752
30045
|
}
|
|
29753
|
-
const path =
|
|
30046
|
+
const path = join16(directory, entry);
|
|
29754
30047
|
if (kept.has(path) || ageMs(path, now) < keepMs) {
|
|
29755
30048
|
continue;
|
|
29756
30049
|
}
|
|
@@ -29764,16 +30057,16 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
29764
30057
|
import { createHash as createHash4 } from "node:crypto";
|
|
29765
30058
|
import {
|
|
29766
30059
|
cpSync,
|
|
29767
|
-
existsSync as
|
|
30060
|
+
existsSync as existsSync11,
|
|
29768
30061
|
mkdirSync as mkdirSync9,
|
|
29769
|
-
readdirSync as
|
|
30062
|
+
readdirSync as readdirSync7,
|
|
29770
30063
|
readFileSync as readFileSync16,
|
|
29771
30064
|
rmdirSync,
|
|
29772
30065
|
rmSync as rmSync8,
|
|
29773
30066
|
statSync as statSync7,
|
|
29774
30067
|
writeFileSync as writeFileSync11
|
|
29775
30068
|
} from "node:fs";
|
|
29776
|
-
import { dirname as dirname10, join as
|
|
30069
|
+
import { dirname as dirname10, join as join17 } from "node:path";
|
|
29777
30070
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
29778
30071
|
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
29779
30072
|
var EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
|
|
@@ -29786,11 +30079,11 @@ var DIGEST_MARKER = ".gdharness-digest";
|
|
|
29786
30079
|
function editorAddonDigest(from = shippedAddonsDirectory()) {
|
|
29787
30080
|
const hash = createHash4("sha256");
|
|
29788
30081
|
const walk = (directory, relative) => {
|
|
29789
|
-
for (const name of
|
|
30082
|
+
for (const name of readdirSync7(directory).sort()) {
|
|
29790
30083
|
if (name === VERSION_MARKER || name === DIGEST_MARKER) {
|
|
29791
30084
|
continue;
|
|
29792
30085
|
}
|
|
29793
|
-
const path =
|
|
30086
|
+
const path = join17(directory, name);
|
|
29794
30087
|
const inside = `${relative}/${name}`;
|
|
29795
30088
|
if (statSync7(path).isDirectory()) {
|
|
29796
30089
|
walk(path, inside);
|
|
@@ -29804,7 +30097,7 @@ function editorAddonDigest(from = shippedAddonsDirectory()) {
|
|
|
29804
30097
|
}
|
|
29805
30098
|
};
|
|
29806
30099
|
for (const name of EDITOR_PLUGINS) {
|
|
29807
|
-
walk(
|
|
30100
|
+
walk(join17(from, name), name);
|
|
29808
30101
|
}
|
|
29809
30102
|
return hash.digest("hex");
|
|
29810
30103
|
}
|
|
@@ -29820,11 +30113,11 @@ function shippedEditorDigest() {
|
|
|
29820
30113
|
return shippedDigest ?? undefined;
|
|
29821
30114
|
}
|
|
29822
30115
|
function shippedAddonsDirectory() {
|
|
29823
|
-
return
|
|
30116
|
+
return join17(dirname10(fileURLToPath3(import.meta.url)), "godot", "addons");
|
|
29824
30117
|
}
|
|
29825
30118
|
function installedAddonVersion(projectPath) {
|
|
29826
|
-
const marker =
|
|
29827
|
-
if (!
|
|
30119
|
+
const marker = join17(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
30120
|
+
if (!existsSync11(marker)) {
|
|
29828
30121
|
return null;
|
|
29829
30122
|
}
|
|
29830
30123
|
const said = readFileSync16(marker, "utf8").trim();
|
|
@@ -30479,7 +30772,11 @@ var TOOL_SPECS = [
|
|
|
30479
30772
|
name: "editor_run",
|
|
30480
30773
|
description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. args hands the game its own flags, the ones it reads with OS.get_cmdline_user_args(), and a run carrying any is started by this server rather than by the editor. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. start and check both wait first for a scan the open editor is running and for the class cache it writes just after, since a game booted in between resolves no global class, as does every other engine this server starts on the project; waitedForEditorScanMs says how long, and scanNote says when a scan ran past thirty seconds and the game started during it. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice. When it is not listening, mayYetAnnounce says whether that is final: false is a runtime that is not coming, true is a game still on its way up, which editor_status will see and runtimeWaitMs waits longer for. A start this server made also answers with transcript, the file both the run's streams are written to, so a watch on it can be armed off the start rather than off a second call.",
|
|
30481
30774
|
parameters: {
|
|
30482
|
-
projectPath: {
|
|
30775
|
+
projectPath: {
|
|
30776
|
+
...PROJECT_PATH,
|
|
30777
|
+
ops: ["start", "check", "stop", "wait"],
|
|
30778
|
+
description: "Absolute path to the project directory, the one holding project.godot. stop, wait: optional, the project whose run is meant; a run of another project is refused rather than touched."
|
|
30779
|
+
},
|
|
30483
30780
|
scene: {
|
|
30484
30781
|
type: "string",
|
|
30485
30782
|
ops: ["start", "check"],
|
|
@@ -30611,7 +30908,7 @@ var TOOL_SPECS = [
|
|
|
30611
30908
|
},
|
|
30612
30909
|
{
|
|
30613
30910
|
name: "editor_rescan",
|
|
30614
|
-
description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. A scan or an import the editor is already running is waited out first rather than scanned over, which would start a second import over it, and the answer says how long under waitedForEditorScanMs. 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.",
|
|
30911
|
+
description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. A scan or an import the editor is already running is waited out first rather than scanned over, which would start a second import over it, and the answer says how long under waitedForEditorScanMs. 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 class the scan brings in is named under broughtIn, and every script naming it is reloaded in the editor and named under dependentsReloaded: what the editor compiled from them while it could not see the class stays compiled that way after the scan, and a script extending a new base class went on reporting \"Could not find base class\" until reloaded. One that would not reload is named under dependentsNotReloaded and the call is not ok. 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.",
|
|
30615
30912
|
parameters: {
|
|
30616
30913
|
projectPath: PROJECT_PATH,
|
|
30617
30914
|
timeoutMs: {
|
|
@@ -30717,7 +31014,7 @@ var TOOL_SPECS = [
|
|
|
30717
31014
|
says: {
|
|
30718
31015
|
type: "string",
|
|
30719
31016
|
ops: ["find"],
|
|
30720
|
-
description:
|
|
31017
|
+
description: 'find: part of what the node has written on it as drawn, the way the text op reads it, case-insensitively, which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match. A bare word is a contains; write a glob and it is one, matched against the whole of what the node says, the same as namePattern, and a glob that finds nothing says under note how many nodes it would find open at both ends. A label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Alternatives separated by | match when any one of them does, each by these rules ("*won*|*lost*"); write \\| for a bar that is part of the words.'
|
|
30721
31018
|
},
|
|
30722
31019
|
limit: {
|
|
30723
31020
|
type: "integer",
|
|
@@ -30872,7 +31169,7 @@ var TOOL_SPECS = [
|
|
|
30872
31169
|
says: {
|
|
30873
31170
|
type: "string",
|
|
30874
31171
|
ops: ["click"],
|
|
30875
|
-
description: "click: the words on the control to click, matched the way runtime_inspect find matches says (a plain word is contained in the text, a pattern with * or ? is a glob over it), among the controls on screen, and pressed in the same frame it was found in, so a panel rebuilding between a find and a click cannot free it first. Words on a label inside a button press the button. A control with something drawn over its centre that would take the pointer, such as a screen shown over it, is not on screen. The matches are ranked the way a person picks what to press: a button before text that cannot be pressed, then a control whose whole text is the words before one saying them as part of more, and the best one is pressed; found.picked says why. Several matches at the best rank are refused with each one listed, and index picks one."
|
|
31172
|
+
description: "click: the words on the control to click, matched the way runtime_inspect find matches says (a plain word is contained in the text, a pattern with * or ? is a glob over it, and | separates alternatives), among the controls on screen, and pressed in the same frame it was found in, so a panel rebuilding between a find and a click cannot free it first. Words on a label inside a button press the button. A control with something drawn over its centre that would take the pointer, such as a screen shown over it, is not on screen. The matches are ranked the way a person picks what to press: a button before text that cannot be pressed, then a control whose whole text, or one whole line of it such as the title on a card, is the words before one saying them as part of more, and the best one is pressed; found.picked says why. Several matches at the best rank are refused with each one listed, and index picks one."
|
|
30876
31173
|
},
|
|
30877
31174
|
action: {
|
|
30878
31175
|
type: "string",
|
|
@@ -31003,7 +31300,7 @@ var TOOL_SPECS = [
|
|
|
31003
31300
|
says: {
|
|
31004
31301
|
type: "string",
|
|
31005
31302
|
ops: ["until"],
|
|
31006
|
-
description:
|
|
31303
|
+
description: 'until: wait for these words to appear anywhere under nodePath, or anywhere in the game when no nodePath is given, 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. Matched the way runtime_inspect find matches says: case-insensitive, a plain word anywhere in the text, a pattern with * or ? a glob over the whole of it, and alternatives separated by | met by whichever appears first ("*won*|*lost*"); 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. It looks every frame while a look is cheap and less often on a screen large enough for a look to cost a real share of a frame, so the game keeps its speed; the answer counts the frames the wait spanned and the looks it took.'
|
|
31007
31304
|
},
|
|
31008
31305
|
includeHidden: {
|
|
31009
31306
|
type: "boolean",
|
|
@@ -31124,7 +31421,7 @@ function buildToolDefinitions() {
|
|
|
31124
31421
|
// src/server.ts
|
|
31125
31422
|
var UPDATE_NOTICE_EVERY = 500;
|
|
31126
31423
|
var FEEDBACK_NOTICE_EVERY = 250;
|
|
31127
|
-
var run5 =
|
|
31424
|
+
var run5 = promisify6(execFile6);
|
|
31128
31425
|
var __dirname2 = dirname11(fileURLToPath4(import.meta.url));
|
|
31129
31426
|
var EDITOR_RESTART_TIMEOUT_MS = 90000;
|
|
31130
31427
|
var HOLD_PING_MS = 2000;
|
|
@@ -31140,15 +31437,15 @@ function captureDestinationRefusal(outputPath, projects) {
|
|
|
31140
31437
|
return `outputPath ${outputPath} does not end in .png, which is what a capture is.`;
|
|
31141
31438
|
}
|
|
31142
31439
|
const directory = realPathOr(dirname11(outputPath));
|
|
31143
|
-
if (!
|
|
31440
|
+
if (!existsSync12(directory)) {
|
|
31144
31441
|
return `outputPath's directory ${dirname11(outputPath)} does not exist; a capture creates no directories.`;
|
|
31145
31442
|
}
|
|
31146
|
-
const where =
|
|
31443
|
+
const where = join18(directory, basename4(outputPath));
|
|
31147
31444
|
const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
31148
31445
|
if (inside !== undefined) {
|
|
31149
31446
|
return `outputPath ${outputPath} is inside the project at ${inside}; a capture is saved outside it, so it never becomes part of the game.`;
|
|
31150
31447
|
}
|
|
31151
|
-
if (
|
|
31448
|
+
if (existsSync12(where)) {
|
|
31152
31449
|
return `outputPath ${outputPath} already exists; a capture is not written over a file. Name a new one.`;
|
|
31153
31450
|
}
|
|
31154
31451
|
return null;
|
|
@@ -31308,7 +31605,8 @@ function aboutTheChildren(ended, throughEditor) {
|
|
|
31308
31605
|
return throughEditor ? `Anything that game started for itself, with OS.create_process or otherwise, is not the editor's to stop and was not signalled; ${reaches}` : `Anything that game started for itself, with OS.create_process or otherwise, is a separate process this stop did not signal; ${reaches}`;
|
|
31309
31606
|
}
|
|
31310
31607
|
if (ended.unknown === "platform") {
|
|
31311
|
-
|
|
31608
|
+
const why = whyTheProcessTreeFailed();
|
|
31609
|
+
return `andChildren was asked for and this platform would not list what the game had started, so nothing else was ended.${why === undefined ? "" : ` The listing failed: ${why}.`}`;
|
|
31312
31610
|
}
|
|
31313
31611
|
if (ended.unknown === "no process") {
|
|
31314
31612
|
return "andChildren was asked for and this run has no process id to list under: the editor plays it and its game announced none, so nothing else was ended.";
|
|
@@ -31391,17 +31689,6 @@ function runtimeVerdict(endpoint, after) {
|
|
|
31391
31689
|
heldAt: null
|
|
31392
31690
|
};
|
|
31393
31691
|
}
|
|
31394
|
-
function alive(pid) {
|
|
31395
|
-
if (pid === undefined || pid === null) {
|
|
31396
|
-
return false;
|
|
31397
|
-
}
|
|
31398
|
-
try {
|
|
31399
|
-
process.kill(pid, 0);
|
|
31400
|
-
return true;
|
|
31401
|
-
} catch (error) {
|
|
31402
|
-
return error.code === "EPERM";
|
|
31403
|
-
}
|
|
31404
|
-
}
|
|
31405
31692
|
function isOfType(value, kind) {
|
|
31406
31693
|
switch (kind) {
|
|
31407
31694
|
case "string":
|
|
@@ -31649,13 +31936,13 @@ function aboveTheRunner(entry) {
|
|
|
31649
31936
|
}
|
|
31650
31937
|
function elsewhereIn(projectPath, asked) {
|
|
31651
31938
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
31652
|
-
if (wanted === "" ||
|
|
31939
|
+
if (wanted === "" || existsSync12(join18(projectPath, wanted))) {
|
|
31653
31940
|
return "";
|
|
31654
31941
|
}
|
|
31655
31942
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
31656
31943
|
let near = [];
|
|
31657
31944
|
try {
|
|
31658
|
-
near =
|
|
31945
|
+
near = readdirSync8(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
|
|
31659
31946
|
} catch {
|
|
31660
31947
|
return "";
|
|
31661
31948
|
}
|
|
@@ -31671,7 +31958,7 @@ function runtimeAddonAnnouncesEditors(projectPath) {
|
|
|
31671
31958
|
return false;
|
|
31672
31959
|
}
|
|
31673
31960
|
try {
|
|
31674
|
-
return readFileSync17(
|
|
31961
|
+
return readFileSync17(join18(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
|
|
31675
31962
|
} catch {
|
|
31676
31963
|
return false;
|
|
31677
31964
|
}
|
|
@@ -31696,7 +31983,7 @@ function needsItsOwnProcess(asked) {
|
|
|
31696
31983
|
class GodotServer {
|
|
31697
31984
|
mcp;
|
|
31698
31985
|
locator = new GodotLocator;
|
|
31699
|
-
operationsScript =
|
|
31986
|
+
operationsScript = join18(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
31700
31987
|
godotBridge;
|
|
31701
31988
|
tools = buildToolDefinitions();
|
|
31702
31989
|
activeProcess = null;
|
|
@@ -31708,6 +31995,7 @@ class GodotServer {
|
|
|
31708
31995
|
lastGodotVersion = null;
|
|
31709
31996
|
lspClient = null;
|
|
31710
31997
|
dapClient = null;
|
|
31998
|
+
adapterPassed = null;
|
|
31711
31999
|
breakpointsSeededOn = null;
|
|
31712
32000
|
bridgeStartupError = null;
|
|
31713
32001
|
bridgeRetry = null;
|
|
@@ -31766,6 +32054,7 @@ class GodotServer {
|
|
|
31766
32054
|
this.handleShutdown("stdin:end", 0);
|
|
31767
32055
|
});
|
|
31768
32056
|
console.error("gdharness running on stdio");
|
|
32057
|
+
sweepAbandonedScratch();
|
|
31769
32058
|
try {
|
|
31770
32059
|
await this.takeOverFromAPredecessor();
|
|
31771
32060
|
await this.godotBridge.start();
|
|
@@ -32092,7 +32381,7 @@ class GodotServer {
|
|
|
32092
32381
|
this.callsSinceFeedback = 0;
|
|
32093
32382
|
return {
|
|
32094
32383
|
...answer,
|
|
32095
|
-
content: [...answer.content, { type: "text", text: feedbackNotice() }]
|
|
32384
|
+
content: [...answer.content, { type: "text", text: feedbackNotice(FEEDBACK_NOTICE_EVERY) }]
|
|
32096
32385
|
};
|
|
32097
32386
|
}
|
|
32098
32387
|
validateArguments(spec, args) {
|
|
@@ -32436,8 +32725,8 @@ class GodotServer {
|
|
|
32436
32725
|
if (path === undefined) {
|
|
32437
32726
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
32438
32727
|
}
|
|
32439
|
-
const file =
|
|
32440
|
-
if (!
|
|
32728
|
+
const file = join18(path, "project.godot");
|
|
32729
|
+
if (!existsSync12(file)) {
|
|
32441
32730
|
return {
|
|
32442
32731
|
ok: false,
|
|
32443
32732
|
response: this.createErrorResponse(`Not a Godot project: ${path}`, [
|
|
@@ -32598,13 +32887,13 @@ class GodotServer {
|
|
|
32598
32887
|
let sawAScan = false;
|
|
32599
32888
|
let scanEndedAt = null;
|
|
32600
32889
|
for (;; ) {
|
|
32601
|
-
const asked = Date.now();
|
|
32602
32890
|
let status;
|
|
32603
32891
|
try {
|
|
32604
32892
|
status = asParams(await this.godotBridge.invokeTool("scan_status", {}));
|
|
32605
32893
|
} catch {
|
|
32606
32894
|
return { waitedMs: Date.now() - started, stillScanning: false };
|
|
32607
32895
|
}
|
|
32896
|
+
const answered = Date.now();
|
|
32608
32897
|
if (status["scanning"] === true || status["importing"] === true || status["pending"] === true) {
|
|
32609
32898
|
sawAScan = true;
|
|
32610
32899
|
if (Date.now() - started >= SCAN_WAIT_MS) {
|
|
@@ -32614,7 +32903,7 @@ class GodotServer {
|
|
|
32614
32903
|
continue;
|
|
32615
32904
|
}
|
|
32616
32905
|
const since = readNumber(status, "sinceScanFinishedMs");
|
|
32617
|
-
scanEndedAt = since !== undefined && since >= 0 ?
|
|
32906
|
+
scanEndedAt = since !== undefined && since >= 0 ? answered - since : sawAScan ? answered : null;
|
|
32618
32907
|
break;
|
|
32619
32908
|
}
|
|
32620
32909
|
let waitedForTheWrite = false;
|
|
@@ -33033,11 +33322,11 @@ class GodotServer {
|
|
|
33033
33322
|
return this.createErrorResponse(`Cannot create ${dirname11(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
33034
33323
|
}
|
|
33035
33324
|
const debug = readBoolean(args, "debug") ?? false;
|
|
33036
|
-
const exportLogs =
|
|
33325
|
+
const exportLogs = scratchDirectory("export");
|
|
33037
33326
|
const exportArgs = [
|
|
33038
33327
|
"--headless",
|
|
33039
33328
|
"--log-file",
|
|
33040
|
-
|
|
33329
|
+
join18(exportLogs, "engine.log"),
|
|
33041
33330
|
"--path",
|
|
33042
33331
|
project.value.path,
|
|
33043
33332
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -33069,7 +33358,7 @@ class GodotServer {
|
|
|
33069
33358
|
log.finish();
|
|
33070
33359
|
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
33071
33360
|
const verdict = {
|
|
33072
|
-
exported: ending.exitCode === 0 && log.count("error") === 0 &&
|
|
33361
|
+
exported: ending.exitCode === 0 && log.count("error") === 0 && existsSync12(output.absolutePath),
|
|
33073
33362
|
preset,
|
|
33074
33363
|
outputPath: output.relativePath,
|
|
33075
33364
|
debug,
|
|
@@ -33105,7 +33394,7 @@ class GodotServer {
|
|
|
33105
33394
|
return contained.response;
|
|
33106
33395
|
}
|
|
33107
33396
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
33108
|
-
if (!
|
|
33397
|
+
if (!existsSync12(join18(project.value.path, runner))) {
|
|
33109
33398
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
33110
33399
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
33111
33400
|
]);
|
|
@@ -33141,7 +33430,7 @@ class GodotServer {
|
|
|
33141
33430
|
];
|
|
33142
33431
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
33143
33432
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
33144
|
-
const userData =
|
|
33433
|
+
const userData = scratchDirectory("tests");
|
|
33145
33434
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
33146
33435
|
const console2 = [];
|
|
33147
33436
|
let lastSaid = Date.now();
|
|
@@ -33168,14 +33457,17 @@ class GodotServer {
|
|
|
33168
33457
|
resolve(false);
|
|
33169
33458
|
});
|
|
33170
33459
|
});
|
|
33171
|
-
|
|
33460
|
+
if (timedOut && run.process.pid !== undefined) {
|
|
33461
|
+
await untilGone([run.process.pid]);
|
|
33462
|
+
}
|
|
33463
|
+
const reportsDir = join18(project.value.path, ".godot", ...ours.split("/"));
|
|
33172
33464
|
let report = null;
|
|
33173
33465
|
let reportProblem = null;
|
|
33174
33466
|
try {
|
|
33175
|
-
const written =
|
|
33467
|
+
const written = existsSync12(reportsDir) ? readdirSync8(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
33176
33468
|
const newest = written.at(-1);
|
|
33177
33469
|
if (newest !== undefined) {
|
|
33178
|
-
report = parseJUnit(readFileSync17(
|
|
33470
|
+
report = parseJUnit(readFileSync17(join18(reportsDir, newest, "results.xml"), "utf8"));
|
|
33179
33471
|
}
|
|
33180
33472
|
} catch (error) {
|
|
33181
33473
|
reportProblem = errorMessage(error);
|
|
@@ -33333,7 +33625,7 @@ class GodotServer {
|
|
|
33333
33625
|
const cached = cachedClasses(projectPath);
|
|
33334
33626
|
const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
|
|
33335
33627
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
33336
|
-
if (!contained.ok || !
|
|
33628
|
+
if (!contained.ok || !existsSync12(contained.absolutePath)) {
|
|
33337
33629
|
return null;
|
|
33338
33630
|
}
|
|
33339
33631
|
try {
|
|
@@ -33387,7 +33679,7 @@ class GodotServer {
|
|
|
33387
33679
|
}
|
|
33388
33680
|
sourceOfClass(projectPath, resourcePath) {
|
|
33389
33681
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
33390
|
-
if (!contained.ok || !
|
|
33682
|
+
if (!contained.ok || !existsSync12(contained.absolutePath)) {
|
|
33391
33683
|
return null;
|
|
33392
33684
|
}
|
|
33393
33685
|
try {
|
|
@@ -33408,28 +33700,32 @@ class GodotServer {
|
|
|
33408
33700
|
async handleDAP(toolName, args) {
|
|
33409
33701
|
return handleDAPTool(this.dap(), toolName, args);
|
|
33410
33702
|
}
|
|
33411
|
-
adapterBelongsElsewhere() {
|
|
33703
|
+
async adapterBelongsElsewhere() {
|
|
33412
33704
|
const status = this.godotBridge.getStatus();
|
|
33413
33705
|
const ours = status.editorPid;
|
|
33414
33706
|
if (ours === undefined) {
|
|
33415
33707
|
return null;
|
|
33416
33708
|
}
|
|
33709
|
+
const connection = this.dapClient?.connection() ?? null;
|
|
33710
|
+
if (connection !== null && this.adapterPassed?.connection === connection && this.adapterPassed.editorPid === ours) {
|
|
33711
|
+
return null;
|
|
33712
|
+
}
|
|
33417
33713
|
const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
|
|
33418
|
-
const
|
|
33714
|
+
const askedAt = Date.now();
|
|
33715
|
+
const holder = await listeningPid(port);
|
|
33716
|
+
this.logDebug(`Asked who holds debug adapter port ${port} in ${Date.now() - askedAt}ms: ${holder ?? "not said"}`);
|
|
33419
33717
|
if (holder === null || holder === ours) {
|
|
33718
|
+
this.adapterPassed = connection === null ? null : { connection, editorPid: ours };
|
|
33420
33719
|
return null;
|
|
33421
33720
|
}
|
|
33422
33721
|
return `The debug adapter on port ${port} belongs to process ${holder}, and the editor on this server's bridge is process ${ours}. Godot gives every editor the same debug adapter port by default, so the one this editor names in its settings is not necessarily the one it holds. Playing the game now would put another editor's console and another project's debugger under this project's name.`;
|
|
33423
33722
|
}
|
|
33424
|
-
|
|
33425
|
-
return anEditorIsStillComing(listeningSince, this.launchedEditorIsUp());
|
|
33426
|
-
}
|
|
33427
|
-
launchedEditorIsUp() {
|
|
33723
|
+
async launchedEditorIsUp() {
|
|
33428
33724
|
const launched = this.launchedEditor;
|
|
33429
33725
|
if (launched === null || !alive(launched.pid)) {
|
|
33430
33726
|
return false;
|
|
33431
33727
|
}
|
|
33432
|
-
const now = startTimesOf([launched.pid]).get(launched.pid);
|
|
33728
|
+
const now = (await startTimesOf([launched.pid])).get(launched.pid);
|
|
33433
33729
|
return launched.startedAt === null || now === undefined || now === launched.startedAt;
|
|
33434
33730
|
}
|
|
33435
33731
|
dap() {
|
|
@@ -33518,8 +33814,9 @@ class GodotServer {
|
|
|
33518
33814
|
}
|
|
33519
33815
|
return owed;
|
|
33520
33816
|
}
|
|
33521
|
-
getEditorStatusPayload() {
|
|
33817
|
+
async getEditorStatusPayload() {
|
|
33522
33818
|
const status = this.godotBridge.getStatus();
|
|
33819
|
+
const launchedIsUp = !status.connected && await this.launchedEditorIsUp();
|
|
33523
33820
|
const isPortConflict = this.bridgeStartupError?.includes("EADDRINUSE") ?? false;
|
|
33524
33821
|
const stale = status.connected && editorIsStale(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest());
|
|
33525
33822
|
const unfinished = this.restartLeftUnfinished(status.connected);
|
|
@@ -33528,14 +33825,14 @@ class GodotServer {
|
|
|
33528
33825
|
serverVersion: SERVER_VERSION,
|
|
33529
33826
|
addonIsStale: status.connected ? stale : undefined,
|
|
33530
33827
|
bridgeAvailable: this.bridgeStartupError === null,
|
|
33531
|
-
mayYetConnect: status.connected ? undefined : unfinished !== null ? false :
|
|
33828
|
+
mayYetConnect: status.connected ? undefined : unfinished !== null ? false : anEditorIsStillComing(status.listeningSince, launchedIsUp),
|
|
33532
33829
|
restartInterrupted: unfinished === null ? undefined : {
|
|
33533
33830
|
quitEditorPid: unfinished.editorPid,
|
|
33534
33831
|
quitAt: unfinished.quitAt,
|
|
33535
33832
|
byServerPid: unfinished.byPid,
|
|
33536
33833
|
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."
|
|
33537
33834
|
},
|
|
33538
|
-
awaitingLaunchedEditor: status.connected || !
|
|
33835
|
+
awaitingLaunchedEditor: status.connected || !launchedIsUp ? undefined : this.launchedEditor?.pid ?? undefined,
|
|
33539
33836
|
...status.connected ? this.whatTheLaunchedEditorDropped() : {},
|
|
33540
33837
|
startupError: this.bridgeStartupError,
|
|
33541
33838
|
portNote: status.portWanted === undefined ? undefined : `Port ${status.portWanted}, the one this server was configured with, was held ${this.notHandedOverBy === null ? "by another process" : `by pid ${this.notHandedOverBy}, the gdharness server this one replaced, which had not let go of it ${HANDOVER_MS / 1000} seconds after being told it was replaced`} when the bridge started, so it took ${status.port} and announced that where the editor looks for it${this.announcedAt === null ? "" : `, ${this.announcedAt}`}. The editor finds it there; the next server started takes ${status.portWanted} again if it is free.`,
|
|
@@ -33624,7 +33921,7 @@ class GodotServer {
|
|
|
33624
33921
|
const asked = (async () => {
|
|
33625
33922
|
const playing = await this.editorPlayingState();
|
|
33626
33923
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
33627
|
-
const current = this.currentRun();
|
|
33924
|
+
const current = await this.currentRun();
|
|
33628
33925
|
const active = current === null ? false : await this.runStillGoing(current, playing?.playing ?? null);
|
|
33629
33926
|
return {
|
|
33630
33927
|
playing,
|
|
@@ -33632,14 +33929,15 @@ class GodotServer {
|
|
|
33632
33929
|
hold: current === null ? {} : await this.holdOf(current, active)
|
|
33633
33930
|
};
|
|
33634
33931
|
})();
|
|
33635
|
-
const [games, { playing, active, hold }, scan] = await Promise.all([
|
|
33932
|
+
const [games, { playing, active, hold }, scan, editorStatus] = await Promise.all([
|
|
33636
33933
|
pinged,
|
|
33637
33934
|
asked,
|
|
33638
|
-
this.editorScanState()
|
|
33935
|
+
this.editorScanState(),
|
|
33936
|
+
this.getEditorStatusPayload()
|
|
33639
33937
|
]);
|
|
33640
33938
|
return this.jsonTextResponse({
|
|
33641
33939
|
editor: {
|
|
33642
|
-
...
|
|
33940
|
+
...editorStatus,
|
|
33643
33941
|
debugPort: playing?.debugPort ?? this.godotBridge.getStatus().debugPort,
|
|
33644
33942
|
scan
|
|
33645
33943
|
},
|
|
@@ -33669,7 +33967,7 @@ class GodotServer {
|
|
|
33669
33967
|
return null;
|
|
33670
33968
|
}
|
|
33671
33969
|
try {
|
|
33672
|
-
return settingKeys(readFileSync17(
|
|
33970
|
+
return settingKeys(readFileSync17(join18(projectPath, "project.godot"), "utf8"));
|
|
33673
33971
|
} catch {
|
|
33674
33972
|
return null;
|
|
33675
33973
|
}
|
|
@@ -33818,7 +34116,7 @@ class GodotServer {
|
|
|
33818
34116
|
writeEditorLogNote(projectPath, launched.pid);
|
|
33819
34117
|
this.launchedEditor = {
|
|
33820
34118
|
pid: launched.pid,
|
|
33821
|
-
startedAt: startTimesOf([launched.pid]).get(launched.pid) ?? null
|
|
34119
|
+
startedAt: (await startTimesOf([launched.pid])).get(launched.pid) ?? null
|
|
33822
34120
|
};
|
|
33823
34121
|
return { pid: launched.pid, error: null };
|
|
33824
34122
|
}
|
|
@@ -33866,13 +34164,15 @@ class GodotServer {
|
|
|
33866
34164
|
}
|
|
33867
34165
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
33868
34166
|
let ended = null;
|
|
33869
|
-
const before = this.currentRun();
|
|
34167
|
+
const before = await this.currentRun();
|
|
33870
34168
|
if (before !== null && await this.runStillGoing(before)) {
|
|
33871
34169
|
this.logDebug("Ending the running game before starting another");
|
|
34170
|
+
const endingFrom = Date.now();
|
|
33872
34171
|
ended = {
|
|
33873
34172
|
pid: before.pid ?? this.announcedPidOf(before) ?? null,
|
|
33874
34173
|
ending: await this.endActiveGame("editor_run start, which ends the run that was going before it starts another", true)
|
|
33875
34174
|
};
|
|
34175
|
+
this.logDebug(`The running game was ${ended.ending} after ${Date.now() - endingFrom}ms`);
|
|
33876
34176
|
}
|
|
33877
34177
|
const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
|
|
33878
34178
|
const wereHereFirst = this.everyRuntimeOfOurs();
|
|
@@ -33922,7 +34222,7 @@ class GodotServer {
|
|
|
33922
34222
|
}
|
|
33923
34223
|
async runtimeUp(projectPath, before, budget, withArgs) {
|
|
33924
34224
|
const budgetMs = budget.ms;
|
|
33925
|
-
if (!
|
|
34225
|
+
if (!existsSync12(join18(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
33926
34226
|
return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
|
|
33927
34227
|
}
|
|
33928
34228
|
let asking = null;
|
|
@@ -33933,12 +34233,12 @@ class GodotServer {
|
|
|
33933
34233
|
const lastSaid = () => editorSaysGoing ? "playing" : "not playing";
|
|
33934
34234
|
const endpoint = await announcedSince(projectPath, before, {
|
|
33935
34235
|
budgetMs,
|
|
33936
|
-
accept: (one) => this.isTheGameOf(this.
|
|
34236
|
+
accept: (one) => this.isTheGameOf(this.activeProcess, one, projectPath),
|
|
33937
34237
|
giveUp: () => {
|
|
33938
34238
|
if (this.dapClient?.isStopped() === true) {
|
|
33939
34239
|
return true;
|
|
33940
34240
|
}
|
|
33941
|
-
const run = this.
|
|
34241
|
+
const run = this.activeProcess;
|
|
33942
34242
|
if (run === null || !run.throughEditor) {
|
|
33943
34243
|
return !stillRunning(run);
|
|
33944
34244
|
}
|
|
@@ -33956,7 +34256,7 @@ class GodotServer {
|
|
|
33956
34256
|
});
|
|
33957
34257
|
const asked = editorAsks === 0 ? "editor not asked" : `editor asked ${editorAsks === 1 ? "once" : `${editorAsks} times`}, last said ${lastSaid()}`;
|
|
33958
34258
|
this.logDebug(`announce wait ended after ${Date.now() - waitBegan}ms: ${endpoint === null ? "nothing announced" : `pid ${endpoint.pid} announced`}, ${asked}`);
|
|
33959
|
-
const going = this.currentRun();
|
|
34259
|
+
const going = await this.currentRun();
|
|
33960
34260
|
if (endpoint !== null && going !== null && going.announcedPid === undefined) {
|
|
33961
34261
|
this.tie(going, endpoint);
|
|
33962
34262
|
}
|
|
@@ -33997,8 +34297,10 @@ class GodotServer {
|
|
|
33997
34297
|
}
|
|
33998
34298
|
async playThroughEditor(scene, refreshedClasses, scanned, projectPath, alreadyPlaying, runtimeWaitMs, ended) {
|
|
33999
34299
|
const log = new GameLog;
|
|
34300
|
+
const connectingFrom = Date.now();
|
|
34000
34301
|
try {
|
|
34001
34302
|
await this.dap().connect();
|
|
34303
|
+
this.logDebug(`The debug adapter was connected after ${Date.now() - connectingFrom}ms`);
|
|
34002
34304
|
this.dapClient?.getOutput(true);
|
|
34003
34305
|
} catch (error) {
|
|
34004
34306
|
return this.createErrorResponse(`The editor is connected but its debug adapter is not: ${errorMessage(error)}`, [
|
|
@@ -34006,7 +34308,7 @@ class GodotServer {
|
|
|
34006
34308
|
"GDHARNESS_DAP_PORT points this server at another one"
|
|
34007
34309
|
]);
|
|
34008
34310
|
}
|
|
34009
|
-
const stranger = this.adapterBelongsElsewhere();
|
|
34311
|
+
const stranger = await this.adapterBelongsElsewhere();
|
|
34010
34312
|
if (stranger !== null) {
|
|
34011
34313
|
return this.createErrorResponse(stranger, [
|
|
34012
34314
|
"editor_status names the editor on the bridge and the port it says it serves",
|
|
@@ -34014,7 +34316,9 @@ class GodotServer {
|
|
|
34014
34316
|
"Closing the other editor, or moving its debug adapter port in its editor settings, frees this one"
|
|
34015
34317
|
]);
|
|
34016
34318
|
}
|
|
34319
|
+
const sendingFrom = Date.now();
|
|
34017
34320
|
const breakpoints = await this.dapHoldingBreakpointsOf(projectPath).reapplyBreakpoints();
|
|
34321
|
+
this.logDebug(`Breakpoints were sent again after ${Date.now() - sendingFrom}ms`);
|
|
34018
34322
|
const answer = await this.handleViaBridge("play_scene", scene === null ? {} : { scenePath: `res://${scene}` });
|
|
34019
34323
|
if (answer.isError === true) {
|
|
34020
34324
|
return answer;
|
|
@@ -34200,13 +34504,13 @@ class GodotServer {
|
|
|
34200
34504
|
arguments: [],
|
|
34201
34505
|
...running.command === undefined ? {} : { command: running.command }
|
|
34202
34506
|
};
|
|
34203
|
-
if (!stillTheRecordedRun(startedAs)) {
|
|
34507
|
+
if (!await stillTheRecordedRun(startedAs)) {
|
|
34204
34508
|
clearTheRecord();
|
|
34205
34509
|
running.endedHere = null;
|
|
34206
34510
|
running.log.record("warning", `This run was not ended here: pid ${running.pid} no longer answers as the process the run was started as, so nothing was signalled. If that process is still the game, end it yourself; if it is not, it belongs to something else.`);
|
|
34207
34511
|
return "refused";
|
|
34208
34512
|
}
|
|
34209
|
-
const signalled = runningAs(running.pid);
|
|
34513
|
+
const signalled = await runningAs(running.pid);
|
|
34210
34514
|
const described = signalled === null ? "nothing it would name" : `${signalled.kind}: ${signalled.text}`;
|
|
34211
34515
|
let landed = true;
|
|
34212
34516
|
try {
|
|
@@ -34227,7 +34531,7 @@ class GodotServer {
|
|
|
34227
34531
|
return gone ? "gone" : "lingering";
|
|
34228
34532
|
}
|
|
34229
34533
|
spawnGame(godotPath, cmdArgs, env) {
|
|
34230
|
-
const child =
|
|
34534
|
+
const child = spawn3(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
|
|
34231
34535
|
const cancelled = callSignal();
|
|
34232
34536
|
if (cancelled !== undefined) {
|
|
34233
34537
|
const end = () => {
|
|
@@ -34394,8 +34698,8 @@ class GodotServer {
|
|
|
34394
34698
|
}
|
|
34395
34699
|
return run.announcedPid;
|
|
34396
34700
|
}
|
|
34397
|
-
ownGameAmong(running, projectPath) {
|
|
34398
|
-
const run = this.currentRun();
|
|
34701
|
+
async ownGameAmong(running, projectPath) {
|
|
34702
|
+
const run = await this.currentRun();
|
|
34399
34703
|
if (run === null || !stillRunning(run)) {
|
|
34400
34704
|
return;
|
|
34401
34705
|
}
|
|
@@ -34406,7 +34710,7 @@ class GodotServer {
|
|
|
34406
34710
|
return its !== undefined && running.some((one) => one.pid === its) ? its : undefined;
|
|
34407
34711
|
}
|
|
34408
34712
|
async ownGameThroughTheTree(running, projectPath) {
|
|
34409
|
-
const run = this.currentRun();
|
|
34713
|
+
const run = await this.currentRun();
|
|
34410
34714
|
if (run === null || !stillRunning(run) || run.announcedPid !== undefined) {
|
|
34411
34715
|
return;
|
|
34412
34716
|
}
|
|
@@ -34492,7 +34796,7 @@ class GodotServer {
|
|
|
34492
34796
|
const editor = this.godotBridge.getStatus().editorPid;
|
|
34493
34797
|
return endpoint.editorPid === undefined || endpoint.editorPid !== editor;
|
|
34494
34798
|
}
|
|
34495
|
-
adoptRecordedRun() {
|
|
34799
|
+
async adoptRecordedRun() {
|
|
34496
34800
|
const project = this.ourProject();
|
|
34497
34801
|
const record = project === null ? null : readRunRecord(project);
|
|
34498
34802
|
if (record === null || !this.couldBeOurs(record.projectPath)) {
|
|
@@ -34513,7 +34817,7 @@ class GodotServer {
|
|
|
34513
34817
|
pickedUp: true
|
|
34514
34818
|
};
|
|
34515
34819
|
this.drainTranscript(adopted);
|
|
34516
|
-
if (!alive(record.pid) || !couldStillBeTheRecordedRun(record)) {
|
|
34820
|
+
if (!alive(record.pid) || !await couldStillBeTheRecordedRun(record)) {
|
|
34517
34821
|
adopted.log.finish();
|
|
34518
34822
|
adopted.exitCode = record.exitCode ?? null;
|
|
34519
34823
|
adopted.exitSignal = record.exitSignal ?? null;
|
|
@@ -34594,8 +34898,8 @@ class GodotServer {
|
|
|
34594
34898
|
const status = this.godotBridge.getStatus();
|
|
34595
34899
|
return this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
|
|
34596
34900
|
}
|
|
34597
|
-
currentRun() {
|
|
34598
|
-
return this.activeProcess ?? this.adoptRecordedRun();
|
|
34901
|
+
async currentRun() {
|
|
34902
|
+
return this.activeProcess ?? await this.adoptRecordedRun();
|
|
34599
34903
|
}
|
|
34600
34904
|
async checkBoot(godotPath, projectPath, scene, args, userArgs, scanned, asked = {}) {
|
|
34601
34905
|
const frames = readPositiveNumber(args, "frames") ?? 3;
|
|
@@ -34634,10 +34938,14 @@ class GodotServer {
|
|
|
34634
34938
|
}
|
|
34635
34939
|
async handleWaitForRun(args) {
|
|
34636
34940
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
34637
|
-
const run = this.currentRun();
|
|
34941
|
+
const run = await this.currentRun();
|
|
34638
34942
|
if (!run) {
|
|
34639
34943
|
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
34640
34944
|
}
|
|
34945
|
+
const elsewhere = this.anotherProjectsRun(run.projectPath, args, "waited on");
|
|
34946
|
+
if (elsewhere !== null) {
|
|
34947
|
+
return elsewhere;
|
|
34948
|
+
}
|
|
34641
34949
|
const budgetMs = readPositiveNumber(args, "timeoutMs") ?? WAIT_FOR_RUN_MS;
|
|
34642
34950
|
const until = Date.now() + budgetMs;
|
|
34643
34951
|
const cancelled = callSignal();
|
|
@@ -34684,7 +34992,7 @@ class GodotServer {
|
|
|
34684
34992
|
await delay3(50);
|
|
34685
34993
|
}
|
|
34686
34994
|
}
|
|
34687
|
-
handleEditorConsole(args) {
|
|
34995
|
+
async handleEditorConsole(args) {
|
|
34688
34996
|
const status = this.godotBridge.getStatus();
|
|
34689
34997
|
const projectPath = status.projectPath ?? this.ownProject ?? undefined;
|
|
34690
34998
|
if (projectPath === undefined) {
|
|
@@ -34721,10 +35029,11 @@ class GodotServer {
|
|
|
34721
35029
|
const filtered = severity !== "info" || contains !== undefined;
|
|
34722
35030
|
const grouped = bursts(everything, readNonNegativeNumber(args, "before") ?? 3, filtered ? (entry) => answersTo(entry, severity, contains) : undefined);
|
|
34723
35031
|
const shown = grouped.slice(0, MOST_GROUPS);
|
|
35032
|
+
const stillComing = !status.connected && await this.launchedEditorIsUp();
|
|
34724
35033
|
return this.jsonTextResponse({
|
|
34725
35034
|
editorPid: editorPid ?? undefined,
|
|
34726
35035
|
projectPath,
|
|
34727
|
-
...status.connected ? {} :
|
|
35036
|
+
...status.connected ? {} : stillComing ? { stillConnecting: true } : { exitedBeforeConnecting: true },
|
|
34728
35037
|
capturedIn: console2.path,
|
|
34729
35038
|
...console2.of === undefined ? {} : {
|
|
34730
35039
|
readFromTheStart: console2.readBytes,
|
|
@@ -34748,7 +35057,7 @@ class GodotServer {
|
|
|
34748
35057
|
return this.handleEditorConsole(args);
|
|
34749
35058
|
}
|
|
34750
35059
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
34751
|
-
const run = this.currentRun();
|
|
35060
|
+
const run = await this.currentRun();
|
|
34752
35061
|
if (!run) {
|
|
34753
35062
|
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
34754
35063
|
}
|
|
@@ -34787,7 +35096,7 @@ class GodotServer {
|
|
|
34787
35096
|
notes.push(endedWithoutACode({
|
|
34788
35097
|
throughEditor: run.throughEditor,
|
|
34789
35098
|
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
34790
|
-
couldAnnounce: run.projectPath !== null &&
|
|
35099
|
+
couldAnnounce: run.projectPath !== null && existsSync12(join18(run.projectPath, RUNTIME_AUTOLOAD.path)),
|
|
34791
35100
|
endedHere: typeof run.endedHere === "string",
|
|
34792
35101
|
pickedUp: run.pickedUp === true
|
|
34793
35102
|
}));
|
|
@@ -34825,16 +35134,27 @@ class GodotServer {
|
|
|
34825
35134
|
entries: forAnswer(selected.entries)
|
|
34826
35135
|
});
|
|
34827
35136
|
}
|
|
35137
|
+
anotherProjectsRun(runsProject, args, act) {
|
|
35138
|
+
const named = readNonEmptyString(args, "projectPath");
|
|
35139
|
+
if (named === undefined || runsProject === null || isSameDirectory(runsProject, named)) {
|
|
35140
|
+
return null;
|
|
35141
|
+
}
|
|
35142
|
+
return this.createErrorResponse(`The run this server holds is of ${runsProject}, not ${named}, so nothing was ${act}. A run of ${named} is held by the server that started it, in that project's own session.`);
|
|
35143
|
+
}
|
|
34828
35144
|
async handleStopProject(args) {
|
|
34829
35145
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
34830
|
-
const stopped = this.currentRun();
|
|
35146
|
+
const stopped = await this.currentRun();
|
|
34831
35147
|
const named = readNumber(args, "pid");
|
|
34832
35148
|
if (named !== undefined && (stopped === null || named !== stopped.pid && named !== this.announcedPidOf(stopped))) {
|
|
34833
|
-
return await this.stopAnnouncedGame(named, readBoolean(args, "andChildren") === true);
|
|
35149
|
+
return await this.stopAnnouncedGame(named, readBoolean(args, "andChildren") === true, args);
|
|
34834
35150
|
}
|
|
34835
35151
|
if (!stopped) {
|
|
34836
35152
|
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
34837
35153
|
}
|
|
35154
|
+
const elsewhere = this.anotherProjectsRun(stopped.projectPath, args, "stopped");
|
|
35155
|
+
if (elsewhere !== null) {
|
|
35156
|
+
return elsewhere;
|
|
35157
|
+
}
|
|
34838
35158
|
this.drainEditorOutput(stopped);
|
|
34839
35159
|
this.drainTranscript(stopped);
|
|
34840
35160
|
const wasRunning = await this.runStillGoing(stopped);
|
|
@@ -34876,16 +35196,22 @@ class GodotServer {
|
|
|
34876
35196
|
entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
34877
35197
|
});
|
|
34878
35198
|
}
|
|
34879
|
-
async stopAnnouncedGame(pid, andChildren) {
|
|
35199
|
+
async stopAnnouncedGame(pid, andChildren, args) {
|
|
34880
35200
|
const announced = runtimesAnnounced();
|
|
34881
35201
|
const game = [
|
|
34882
35202
|
...this.allAnnouncedForOurProject(announced.running),
|
|
34883
35203
|
...this.allAnnouncedForOurProject(announced.unspoken)
|
|
34884
35204
|
].find((one) => one.pid === pid);
|
|
35205
|
+
if (game !== undefined) {
|
|
35206
|
+
const elsewhere = this.anotherProjectsRun(game.project.path, args, "signalled");
|
|
35207
|
+
if (elsewhere !== null) {
|
|
35208
|
+
return elsewhere;
|
|
35209
|
+
}
|
|
35210
|
+
}
|
|
34885
35211
|
if (game === undefined) {
|
|
34886
35212
|
return this.createErrorResponse(`pid ${pid} is not a game announced for this project, so nothing was signalled. editor_status lists this project's games under runtimes. A game that announces no runtime is ended by the server holding its run, or with andChildren by a stop of the run that started it.`);
|
|
34887
35213
|
}
|
|
34888
|
-
const read = game.announcedAt === undefined ? null : stillTheAnnouncedGame(pid, game.project.path, game.announcedAt);
|
|
35214
|
+
const read = game.announcedAt === undefined ? null : await stillTheAnnouncedGame(pid, game.project.path, game.announcedAt);
|
|
34889
35215
|
if (read === null) {
|
|
34890
35216
|
return this.createErrorResponse(`pid ${pid} announced itself as a game of this project, but the operating system does not describe it as this project's engine run started before that announcement (the engine with --path on the project and no editor flag), so nothing was signalled. If it is still the game, runtime_invoke can ask it to quit.`);
|
|
34891
35217
|
}
|
|
@@ -35083,6 +35409,8 @@ class GodotServer {
|
|
|
35083
35409
|
}
|
|
35084
35410
|
const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
|
|
35085
35411
|
const unseen = checked.unseen;
|
|
35412
|
+
const broughtIn = blind.unseen.map((one) => one.className).filter((name) => !unseen.some((still) => still.className === name));
|
|
35413
|
+
const dependents = busy || projectPath === "" || broughtIn.length === 0 ? null : await this.reloadDependents(projectPath, broughtIn);
|
|
35086
35414
|
const stillGone = lost.filter((name) => !restored.includes(name));
|
|
35087
35415
|
const notes = [];
|
|
35088
35416
|
if (busy) {
|
|
@@ -35094,13 +35422,16 @@ class GodotServer {
|
|
|
35094
35422
|
} else if (unseen.length > 0) {
|
|
35095
35423
|
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.");
|
|
35096
35424
|
}
|
|
35425
|
+
if (dependents !== null && dependents.notReloaded.length > 0) {
|
|
35426
|
+
notes.push(`The scan brought in ${broughtIn.join(", ")}, and ${dependents.notReloaded.map((one) => one.scriptPath).join(", ")} ${dependents.notReloaded.length === 1 ? "names one of them and" : "name them and"} could not be reloaded, so the editor still holds what it compiled while it could not see the class and reports that to the language server. editor_rescan with reloadScript on each, or editor_launch restart.`);
|
|
35427
|
+
}
|
|
35097
35428
|
if (stillAtMissingPaths.length > 0) {
|
|
35098
35429
|
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.`);
|
|
35099
35430
|
} else if (dropped.length > 0) {
|
|
35100
35431
|
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.`);
|
|
35101
35432
|
}
|
|
35102
35433
|
return this.jsonTextResponse({
|
|
35103
|
-
ok: !busy && unseen.length === 0 && stillGone.length === 0 && stillAtMissingPaths.length === 0 && checked.unchecked === undefined,
|
|
35434
|
+
ok: !busy && unseen.length === 0 && stillGone.length === 0 && stillAtMissingPaths.length === 0 && (dependents === null || dependents.notReloaded.length === 0) && checked.unchecked === undefined,
|
|
35104
35435
|
stillWorking: busy,
|
|
35105
35436
|
waitedMs: Date.now() - started,
|
|
35106
35437
|
waitedForEditorScanMs: waitedForEditorMs > 0 ? waitedForEditorMs : undefined,
|
|
@@ -35108,6 +35439,9 @@ class GodotServer {
|
|
|
35108
35439
|
reloadedMethods: reloaded.methods,
|
|
35109
35440
|
reloadProblem: reloaded.problem,
|
|
35110
35441
|
unseenByEditor: unseen.length > 0 ? unseen : undefined,
|
|
35442
|
+
broughtIn: broughtIn.length > 0 ? broughtIn : undefined,
|
|
35443
|
+
dependentsReloaded: dependents !== null && dependents.reloaded.length > 0 ? dependents.reloaded : undefined,
|
|
35444
|
+
dependentsNotReloaded: dependents !== null && dependents.notReloaded.length > 0 ? dependents.notReloaded : undefined,
|
|
35111
35445
|
cacheLost: lost.length > 0 ? lost : undefined,
|
|
35112
35446
|
cacheRestored: restored.length > 0 ? restored : undefined,
|
|
35113
35447
|
cacheDropped: dropped.length > 0 ? dropped : undefined,
|
|
@@ -35116,6 +35450,33 @@ class GodotServer {
|
|
|
35116
35450
|
note: notes.length > 0 ? notes.join(" ") : undefined
|
|
35117
35451
|
});
|
|
35118
35452
|
}
|
|
35453
|
+
async reloadDependents(projectPath, classes) {
|
|
35454
|
+
const reloaded = [];
|
|
35455
|
+
const notReloaded = [];
|
|
35456
|
+
for (const { script: scriptPath, tool } of scriptsNaming(projectPath, classes)) {
|
|
35457
|
+
if (tool) {
|
|
35458
|
+
notReloaded.push({
|
|
35459
|
+
scriptPath,
|
|
35460
|
+
problem: "a @tool script, which runs inside the editor, so it is not reloaded from here; editor_launch restart recompiles it"
|
|
35461
|
+
});
|
|
35462
|
+
continue;
|
|
35463
|
+
}
|
|
35464
|
+
try {
|
|
35465
|
+
const answer = asParams(await this.godotBridge.invokeTool("reload_script", { scriptPath }));
|
|
35466
|
+
if (readArray(answer, "methods") === undefined) {
|
|
35467
|
+
notReloaded.push({
|
|
35468
|
+
scriptPath,
|
|
35469
|
+
problem: "the editor would not say what it has after reloading it"
|
|
35470
|
+
});
|
|
35471
|
+
} else {
|
|
35472
|
+
reloaded.push(scriptPath);
|
|
35473
|
+
}
|
|
35474
|
+
} catch (error) {
|
|
35475
|
+
notReloaded.push({ scriptPath, problem: errorMessage(error) });
|
|
35476
|
+
}
|
|
35477
|
+
}
|
|
35478
|
+
return { reloaded, notReloaded };
|
|
35479
|
+
}
|
|
35119
35480
|
async rebuildCacheAfterLoss(projectPath, lost) {
|
|
35120
35481
|
if (projectPath === "") {
|
|
35121
35482
|
return [];
|
|
@@ -35298,9 +35659,9 @@ class GodotServer {
|
|
|
35298
35659
|
return { heldAt: undefined, heldUnknown: true, heldNote: reply.message };
|
|
35299
35660
|
}
|
|
35300
35661
|
async ownRunOnItsWay(projectPath, budgetMs) {
|
|
35301
|
-
const run = this.currentRun();
|
|
35662
|
+
const run = await this.currentRun();
|
|
35302
35663
|
const project = projectPath ?? run?.projectPath ?? null;
|
|
35303
|
-
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !
|
|
35664
|
+
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync12(join18(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
|
|
35304
35665
|
return null;
|
|
35305
35666
|
}
|
|
35306
35667
|
const already = runtimesAnnounced().running;
|
|
@@ -35326,8 +35687,8 @@ class GodotServer {
|
|
|
35326
35687
|
}
|
|
35327
35688
|
const endpoint = await announcedSince(project, new Set(already.map((one) => one.pid)), {
|
|
35328
35689
|
budgetMs: Math.min(budgetMs, remaining),
|
|
35329
|
-
accept: (one) => this.isTheGameOf(this.
|
|
35330
|
-
giveUp: () => this.
|
|
35690
|
+
accept: (one) => this.isTheGameOf(this.activeProcess, one, project),
|
|
35691
|
+
giveUp: () => this.activeProcess !== run || this.dapClient?.isStopped() === true || !stillRunning(run)
|
|
35331
35692
|
});
|
|
35332
35693
|
if (endpoint !== null) {
|
|
35333
35694
|
const tiedMeanwhile = () => run.announcedPid !== undefined;
|
|
@@ -35336,7 +35697,7 @@ class GodotServer {
|
|
|
35336
35697
|
}
|
|
35337
35698
|
return null;
|
|
35338
35699
|
}
|
|
35339
|
-
if (this.currentRun() !== run || !await this.runStillGoing(run)) {
|
|
35700
|
+
if (await this.currentRun() !== run || !await this.runStillGoing(run)) {
|
|
35340
35701
|
return null;
|
|
35341
35702
|
}
|
|
35342
35703
|
return `The game this server started (${which}) is still coming up and has not announced its runtime after a wait of ${Math.min(budgetMs, remaining)}ms, so there is nothing to talk to yet. It is still running: ask again in a moment rather than starting another.`;
|
|
@@ -35352,7 +35713,7 @@ class GodotServer {
|
|
|
35352
35713
|
}
|
|
35353
35714
|
const announced = runtimesAnnounced();
|
|
35354
35715
|
const wanted = typeof projectPath === "string" ? projectPath : this.ownProject ?? undefined;
|
|
35355
|
-
const own = typeof pid === "number" ? undefined : this.ownGameAmong(announced.running, wanted) ?? await this.ownGameThroughTheTree(announced.running, wanted);
|
|
35716
|
+
const own = typeof pid === "number" ? undefined : await this.ownGameAmong(announced.running, wanted) ?? await this.ownGameThroughTheTree(announced.running, wanted);
|
|
35356
35717
|
const choice = chooseRuntime(announced.running, wanted, announced.unspoken, typeof pid === "number" ? pid : own);
|
|
35357
35718
|
if ("problem" in choice) {
|
|
35358
35719
|
return this.createErrorResponse(choice.problem);
|
|
@@ -35379,8 +35740,8 @@ class GodotServer {
|
|
|
35379
35740
|
return this.createErrorResponse(refusal);
|
|
35380
35741
|
}
|
|
35381
35742
|
}
|
|
35382
|
-
const screenshotDir = expectsScreenshot ?
|
|
35383
|
-
const screenshotPath = screenshotDir ?
|
|
35743
|
+
const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
|
|
35744
|
+
const screenshotPath = screenshotDir ? join18(screenshotDir, "capture.png") : null;
|
|
35384
35745
|
try {
|
|
35385
35746
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs, !expectsScreenshot);
|
|
35386
35747
|
if (!reply.ok && reply.requestId !== undefined) {
|