gdharness 1.1.13 → 1.1.15
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 +742 -361
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +19 -1
- 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 +623 -245
- 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";
|
|
@@ -24812,8 +24825,18 @@ function resolveHeadless(requested, host) {
|
|
|
24812
24825
|
}
|
|
24813
24826
|
return envValue("DISPLAY", host.variables) === undefined && envValue("WAYLAND_DISPLAY", host.variables) === undefined;
|
|
24814
24827
|
}
|
|
24828
|
+
function resolveSilent(requested, variables) {
|
|
24829
|
+
if (typeof requested === "boolean") {
|
|
24830
|
+
return requested;
|
|
24831
|
+
}
|
|
24832
|
+
const said = envValue("GDHARNESS_SILENT", variables)?.toLowerCase();
|
|
24833
|
+
return said !== undefined && said !== "0" && said !== "false";
|
|
24834
|
+
}
|
|
24815
24835
|
function runArguments(options) {
|
|
24816
24836
|
const args = options.headless ? ["--headless", "--path", options.projectPath] : ["--path", options.projectPath];
|
|
24837
|
+
if (options.silent === true && !options.headless) {
|
|
24838
|
+
args.push("--audio-driver", "Dummy");
|
|
24839
|
+
}
|
|
24817
24840
|
if (options.quitAfter !== undefined && options.quitAfter !== null) {
|
|
24818
24841
|
args.push("--quit-after", String(options.quitAfter));
|
|
24819
24842
|
}
|
|
@@ -24923,12 +24946,161 @@ function resolveWithinProject(projectPath, candidatePath) {
|
|
|
24923
24946
|
}
|
|
24924
24947
|
|
|
24925
24948
|
// src/process-children.ts
|
|
24926
|
-
import { execFile
|
|
24949
|
+
import { execFile } from "node:child_process";
|
|
24927
24950
|
import { basename } from "node:path";
|
|
24928
24951
|
import { promisify } from "node:util";
|
|
24952
|
+
|
|
24953
|
+
// src/windows-ask.ts
|
|
24954
|
+
import { spawn } from "node:child_process";
|
|
24955
|
+
import process6 from "node:process";
|
|
24956
|
+
var SCRIPT = [
|
|
24957
|
+
"[Console]::OutputEncoding = [Text.Encoding]::UTF8",
|
|
24958
|
+
"$in = [Console]::In",
|
|
24959
|
+
"while ($null -ne ($line = $in.ReadLine())) {",
|
|
24960
|
+
' $parts = $line.Split(" ", 2)',
|
|
24961
|
+
" try {",
|
|
24962
|
+
" $asked = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($parts[1]))",
|
|
24963
|
+
" $said = (& ([ScriptBlock]::Create($asked)) | Out-String -Width 1000000)",
|
|
24964
|
+
' $tag = "OK"',
|
|
24965
|
+
" } catch {",
|
|
24966
|
+
" $said = $_.Exception.Message",
|
|
24967
|
+
' $tag = "ERR"',
|
|
24968
|
+
" }",
|
|
24969
|
+
' [Console]::Out.Write($parts[0] + " " + $tag + " " + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($said)) + "`n")',
|
|
24970
|
+
" [Console]::Out.Flush()",
|
|
24971
|
+
"}"
|
|
24972
|
+
].join(`
|
|
24973
|
+
`);
|
|
24974
|
+
|
|
24975
|
+
class WindowsAskFailure extends Error {
|
|
24976
|
+
constructor(how) {
|
|
24977
|
+
super(`PowerShell ${how}`);
|
|
24978
|
+
}
|
|
24979
|
+
}
|
|
24980
|
+
var helper = null;
|
|
24981
|
+
var buffered = "";
|
|
24982
|
+
var next = 1;
|
|
24983
|
+
var out = null;
|
|
24984
|
+
var queued = [];
|
|
24985
|
+
var held = false;
|
|
24986
|
+
function holding(child, wanted) {
|
|
24987
|
+
if (held === wanted) {
|
|
24988
|
+
return;
|
|
24989
|
+
}
|
|
24990
|
+
held = wanted;
|
|
24991
|
+
const stdout = child.stdout;
|
|
24992
|
+
if (wanted) {
|
|
24993
|
+
stdout.ref?.();
|
|
24994
|
+
} else {
|
|
24995
|
+
stdout.unref?.();
|
|
24996
|
+
}
|
|
24997
|
+
}
|
|
24998
|
+
function running() {
|
|
24999
|
+
if (helper?.exitCode === null && helper.signalCode === null) {
|
|
25000
|
+
return helper;
|
|
25001
|
+
}
|
|
25002
|
+
const child = spawn("powershell.exe", [
|
|
25003
|
+
"-NoProfile",
|
|
25004
|
+
"-NonInteractive",
|
|
25005
|
+
"-ExecutionPolicy",
|
|
25006
|
+
"Bypass",
|
|
25007
|
+
"-EncodedCommand",
|
|
25008
|
+
Buffer.from(SCRIPT, "utf16le").toString("base64")
|
|
25009
|
+
], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
|
|
25010
|
+
buffered = "";
|
|
25011
|
+
child.stdout.setEncoding("utf8");
|
|
25012
|
+
child.stdout.on("data", (chunk) => {
|
|
25013
|
+
buffered += chunk;
|
|
25014
|
+
for (let end = buffered.indexOf(`
|
|
25015
|
+
`);end !== -1; end = buffered.indexOf(`
|
|
25016
|
+
`)) {
|
|
25017
|
+
answered(buffered.slice(0, end).trim());
|
|
25018
|
+
buffered = buffered.slice(end + 1);
|
|
25019
|
+
}
|
|
25020
|
+
});
|
|
25021
|
+
child.stderr.resume();
|
|
25022
|
+
child.stdin.on("error", () => {});
|
|
25023
|
+
const gone = () => {
|
|
25024
|
+
if (helper !== child) {
|
|
25025
|
+
return;
|
|
25026
|
+
}
|
|
25027
|
+
helper = null;
|
|
25028
|
+
if (out !== null) {
|
|
25029
|
+
clearTimeout(out.timer);
|
|
25030
|
+
out.question.reject(new WindowsAskFailure("exited before it answered"));
|
|
25031
|
+
out = null;
|
|
25032
|
+
}
|
|
25033
|
+
sendNext();
|
|
25034
|
+
};
|
|
25035
|
+
child.once("exit", gone);
|
|
25036
|
+
child.once("error", gone);
|
|
25037
|
+
child.unref();
|
|
25038
|
+
child.stdin.unref?.();
|
|
25039
|
+
child.stderr.unref?.();
|
|
25040
|
+
held = true;
|
|
25041
|
+
holding(child, false);
|
|
25042
|
+
helper = child;
|
|
25043
|
+
return child;
|
|
25044
|
+
}
|
|
25045
|
+
function answered(line) {
|
|
25046
|
+
const [id, tag, body] = line.split(" ");
|
|
25047
|
+
if (out === null || out.id !== Number(id)) {
|
|
25048
|
+
return;
|
|
25049
|
+
}
|
|
25050
|
+
const { question, timer } = out;
|
|
25051
|
+
clearTimeout(timer);
|
|
25052
|
+
out = null;
|
|
25053
|
+
const said = Buffer.from(body ?? "", "base64").toString("utf8");
|
|
25054
|
+
if (tag === "OK") {
|
|
25055
|
+
question.resolve(said);
|
|
25056
|
+
} else {
|
|
25057
|
+
question.reject(new WindowsAskFailure(`refused the question: ${said.trim() || "no reason given"}`));
|
|
25058
|
+
}
|
|
25059
|
+
sendNext();
|
|
25060
|
+
}
|
|
25061
|
+
function sendNext() {
|
|
25062
|
+
const question = queued.shift();
|
|
25063
|
+
if (question === undefined) {
|
|
25064
|
+
if (helper !== null) {
|
|
25065
|
+
holding(helper, false);
|
|
25066
|
+
}
|
|
25067
|
+
return;
|
|
25068
|
+
}
|
|
25069
|
+
const child = running();
|
|
25070
|
+
holding(child, true);
|
|
25071
|
+
const id = next;
|
|
25072
|
+
next += 1;
|
|
25073
|
+
const timer = setTimeout(() => {
|
|
25074
|
+
if (out?.id !== id) {
|
|
25075
|
+
return;
|
|
25076
|
+
}
|
|
25077
|
+
out = null;
|
|
25078
|
+
question.reject(new WindowsAskFailure(`did not answer within ${question.withinMs}ms`));
|
|
25079
|
+
if (helper === child) {
|
|
25080
|
+
helper = null;
|
|
25081
|
+
}
|
|
25082
|
+
child.kill();
|
|
25083
|
+
sendNext();
|
|
25084
|
+
}, question.withinMs);
|
|
25085
|
+
out = { id, question, timer };
|
|
25086
|
+
child.stdin.write(`${id} ${Buffer.from(question.script, "utf8").toString("base64")}
|
|
25087
|
+
`);
|
|
25088
|
+
}
|
|
25089
|
+
function askWindows(script, withinMs) {
|
|
25090
|
+
if (process6.platform !== "win32") {
|
|
25091
|
+
return Promise.reject(new WindowsAskFailure("is asked only on Windows"));
|
|
25092
|
+
}
|
|
25093
|
+
return new Promise((resolve, reject) => {
|
|
25094
|
+
queued.push({ script, withinMs, resolve, reject });
|
|
25095
|
+
if (out === null) {
|
|
25096
|
+
sendNext();
|
|
25097
|
+
}
|
|
25098
|
+
});
|
|
25099
|
+
}
|
|
25100
|
+
|
|
25101
|
+
// src/process-children.ts
|
|
24929
25102
|
var run = promisify(execFile);
|
|
24930
|
-
|
|
24931
|
-
function startTimesOf(pids) {
|
|
25103
|
+
async function startTimesOf(pids) {
|
|
24932
25104
|
const began = new Map;
|
|
24933
25105
|
if (pids.length === 0) {
|
|
24934
25106
|
return began;
|
|
@@ -24936,12 +25108,7 @@ function startTimesOf(pids) {
|
|
|
24936
25108
|
try {
|
|
24937
25109
|
if (process.platform === "win32") {
|
|
24938
25110
|
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 });
|
|
25111
|
+
const said = await askWindows(`Get-CimInstance Win32_Process -Filter "${filter}" | ForEach-Object { "$($_.ProcessId) $(([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds())" }`, ASK_TIMEOUT_MS);
|
|
24945
25112
|
for (const line of said.split(`
|
|
24946
25113
|
`)) {
|
|
24947
25114
|
const found = /^\s*(\d+)\s+(\d+)\s*$/.exec(line);
|
|
@@ -24951,11 +25118,10 @@ function startTimesOf(pids) {
|
|
|
24951
25118
|
}
|
|
24952
25119
|
return began;
|
|
24953
25120
|
}
|
|
24954
|
-
const said =
|
|
24955
|
-
|
|
24956
|
-
|
|
24957
|
-
|
|
24958
|
-
}).stdout;
|
|
25121
|
+
const said = await printedBy("ps", ["-p", pids.join(","), "-o", "pid=,lstart="], {
|
|
25122
|
+
...process.env,
|
|
25123
|
+
LC_ALL: "C"
|
|
25124
|
+
});
|
|
24959
25125
|
for (const line of said.split(`
|
|
24960
25126
|
`)) {
|
|
24961
25127
|
const found = /^\s*(\d+)\s+(.+)$/.exec(line);
|
|
@@ -24967,18 +25133,41 @@ function startTimesOf(pids) {
|
|
|
24967
25133
|
} catch {}
|
|
24968
25134
|
return began;
|
|
24969
25135
|
}
|
|
25136
|
+
function printedBy(command, args, env) {
|
|
25137
|
+
return new Promise((resolve) => {
|
|
25138
|
+
execFile(command, [...args], { encoding: "utf8", timeout: ASK_TIMEOUT_MS, env }, (_error, stdout) => {
|
|
25139
|
+
resolve(stdout);
|
|
25140
|
+
});
|
|
25141
|
+
});
|
|
25142
|
+
}
|
|
24970
25143
|
var ASK_TIMEOUT_MS = 15000;
|
|
24971
25144
|
async function processTree() {
|
|
24972
25145
|
try {
|
|
24973
|
-
const tree = process.platform === "win32" ? parseProcessTable(await
|
|
25146
|
+
const tree = process.platform === "win32" ? parseProcessTable(await listWindows(), true) : parseProcessTable(await askPosix());
|
|
24974
25147
|
lastListingFailure = undefined;
|
|
24975
25148
|
return tree;
|
|
24976
25149
|
} catch (error) {
|
|
24977
|
-
lastListingFailure =
|
|
25150
|
+
lastListingFailure = listingFailure(error);
|
|
24978
25151
|
return;
|
|
24979
25152
|
}
|
|
24980
25153
|
}
|
|
24981
25154
|
var lastListingFailure;
|
|
25155
|
+
function listingFailure(error) {
|
|
25156
|
+
if (error instanceof WindowsAskFailure) {
|
|
25157
|
+
return error.message;
|
|
25158
|
+
}
|
|
25159
|
+
if (!(error instanceof Error)) {
|
|
25160
|
+
return String(error);
|
|
25161
|
+
}
|
|
25162
|
+
const failed = error;
|
|
25163
|
+
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";
|
|
25164
|
+
const said = typeof failed.stderr === "string" ? failed.stderr.trim() : "";
|
|
25165
|
+
const asked = basename((failed.cmd ?? "").split(" ")[0] ?? "") || "the listing";
|
|
25166
|
+
return `${asked} ${how}${said === "" ? ", with nothing on stderr" : `; stderr: ${said.slice(0, 400)}`}`;
|
|
25167
|
+
}
|
|
25168
|
+
function whyTheProcessTreeFailed() {
|
|
25169
|
+
return lastListingFailure;
|
|
25170
|
+
}
|
|
24982
25171
|
function parseProcessTable(printed, started = false) {
|
|
24983
25172
|
const tree = new Map;
|
|
24984
25173
|
for (const line of printed.split(/\r?\n/)) {
|
|
@@ -25108,14 +25297,8 @@ function commandWords(command) {
|
|
|
25108
25297
|
}
|
|
25109
25298
|
return words;
|
|
25110
25299
|
}
|
|
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;
|
|
25300
|
+
function listWindows() {
|
|
25301
|
+
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
25302
|
}
|
|
25120
25303
|
async function askPosix() {
|
|
25121
25304
|
const { stdout } = await run("ps", ["-e", "-o", "pid=,ppid=,args="], {
|
|
@@ -25234,14 +25417,6 @@ function reportAlreadyInTranscript(transcript, report) {
|
|
|
25234
25417
|
return end <= report.length && report.subarray(boundary, end).equals(copied) ? end : report.length;
|
|
25235
25418
|
}
|
|
25236
25419
|
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
25420
|
function parseAnnouncement(file, pid) {
|
|
25246
25421
|
let fields;
|
|
25247
25422
|
try {
|
|
@@ -25328,6 +25503,7 @@ var STARTED_AFTER_ANNOUNCING_MS = 2000;
|
|
|
25328
25503
|
var CONFIRMED_FOR_MS = 60000;
|
|
25329
25504
|
var JUDGED_AFTER_MS = 60000;
|
|
25330
25505
|
var verdicts = new Map;
|
|
25506
|
+
var judging = null;
|
|
25331
25507
|
function strangersAmong(candidates, now, startTimes = startTimesOf) {
|
|
25332
25508
|
const toJudge = candidates.filter((one) => {
|
|
25333
25509
|
if (now - one.writtenAt < JUDGED_AFTER_MS) {
|
|
@@ -25336,15 +25512,21 @@ function strangersAmong(candidates, now, startTimes = startTimesOf) {
|
|
|
25336
25512
|
const known = verdicts.get(one.file);
|
|
25337
25513
|
return known === undefined || known.writtenAt !== one.writtenAt || !known.stranger && now - known.judgedAt > CONFIRMED_FOR_MS;
|
|
25338
25514
|
});
|
|
25339
|
-
if (toJudge.length > 0) {
|
|
25340
|
-
|
|
25341
|
-
|
|
25342
|
-
|
|
25343
|
-
|
|
25344
|
-
|
|
25345
|
-
|
|
25515
|
+
if (toJudge.length > 0 && judging === null) {
|
|
25516
|
+
judging = startTimes(toJudge.map((one) => one.pid)).then((began) => {
|
|
25517
|
+
for (const one of toJudge) {
|
|
25518
|
+
const at = began.get(one.pid);
|
|
25519
|
+
const stranger = at !== undefined && at - one.writtenAt > STARTED_AFTER_ANNOUNCING_MS;
|
|
25520
|
+
verdicts.set(one.file, { writtenAt: one.writtenAt, judgedAt: now, stranger });
|
|
25521
|
+
}
|
|
25522
|
+
}).catch(() => {}).finally(() => {
|
|
25523
|
+
judging = null;
|
|
25524
|
+
});
|
|
25346
25525
|
}
|
|
25347
|
-
return new Set(candidates.filter((one) =>
|
|
25526
|
+
return new Set(candidates.filter((one) => {
|
|
25527
|
+
const known = verdicts.get(one.file);
|
|
25528
|
+
return known?.stranger === true && known.writtenAt === one.writtenAt;
|
|
25529
|
+
}).map((one) => one.file));
|
|
25348
25530
|
}
|
|
25349
25531
|
function announcedIn(directory) {
|
|
25350
25532
|
if (!existsSync2(directory)) {
|
|
@@ -25365,17 +25547,17 @@ function announcedIn(directory) {
|
|
|
25365
25547
|
}
|
|
25366
25548
|
const file = join2(directory, entry);
|
|
25367
25549
|
const pid = Number.parseInt(match[1] ?? "", 10);
|
|
25368
|
-
const
|
|
25369
|
-
entries.push({ file, pid,
|
|
25370
|
-
if (
|
|
25550
|
+
const running = alive(pid);
|
|
25551
|
+
entries.push({ file, pid, running });
|
|
25552
|
+
if (running) {
|
|
25371
25553
|
try {
|
|
25372
25554
|
candidates.push({ file, pid, writtenAt: statSync(file).mtimeMs });
|
|
25373
25555
|
} catch {}
|
|
25374
25556
|
}
|
|
25375
25557
|
}
|
|
25376
25558
|
const strangers = strangersAmong(candidates, Date.now());
|
|
25377
|
-
for (const { file, pid,
|
|
25378
|
-
const gone = !
|
|
25559
|
+
for (const { file, pid, running } of entries) {
|
|
25560
|
+
const gone = !running || strangers.has(file);
|
|
25379
25561
|
const announced = gone ? { kind: "rubbish" } : parseAnnouncement(file, pid);
|
|
25380
25562
|
if (announced.kind === "rubbish") {
|
|
25381
25563
|
if (!gone) {
|
|
@@ -25407,7 +25589,7 @@ async function announcementEnded(pid, directories = runtimeDirectories()) {
|
|
|
25407
25589
|
return;
|
|
25408
25590
|
}
|
|
25409
25591
|
const until = Date.now() + ENDED_WITHIN_MS;
|
|
25410
|
-
while (
|
|
25592
|
+
while (alive(pid)) {
|
|
25411
25593
|
const left = until - Date.now();
|
|
25412
25594
|
if (left <= 0) {
|
|
25413
25595
|
return;
|
|
@@ -25423,7 +25605,7 @@ async function announcementEnded(pid, directories = runtimeDirectories()) {
|
|
|
25423
25605
|
}
|
|
25424
25606
|
}
|
|
25425
25607
|
function sweepErrorReport(path, pid, now = Date.now()) {
|
|
25426
|
-
if (
|
|
25608
|
+
if (alive(pid)) {
|
|
25427
25609
|
return;
|
|
25428
25610
|
}
|
|
25429
25611
|
try {
|
|
@@ -25853,6 +26035,15 @@ import { join as join6 } from "node:path";
|
|
|
25853
26035
|
var DECLARATION = /^(?:@[a-z_]+(?:\([^)\n]*\))?\s+)*class_name\s+([A-Za-z_][A-Za-z0-9_]*)/m;
|
|
25854
26036
|
function declaredClasses(projectPath) {
|
|
25855
26037
|
const declared = new Map;
|
|
26038
|
+
eachScript(projectPath, (script, source) => {
|
|
26039
|
+
const found = DECLARATION.exec(source);
|
|
26040
|
+
if (found?.[1]) {
|
|
26041
|
+
declared.set(found[1], script);
|
|
26042
|
+
}
|
|
26043
|
+
});
|
|
26044
|
+
return declared;
|
|
26045
|
+
}
|
|
26046
|
+
function eachScript(projectPath, each) {
|
|
25856
26047
|
const visit = (directory, prefix) => {
|
|
25857
26048
|
if (existsSync3(join6(directory, ".gdignore"))) {
|
|
25858
26049
|
return;
|
|
@@ -25865,15 +26056,53 @@ function declaredClasses(projectPath) {
|
|
|
25865
26056
|
if (entry.isDirectory()) {
|
|
25866
26057
|
visit(path, `${prefix}${entry.name}/`);
|
|
25867
26058
|
} else if (entry.isFile() && entry.name.endsWith(".gd")) {
|
|
25868
|
-
|
|
25869
|
-
if (found?.[1]) {
|
|
25870
|
-
declared.set(found[1], `res://${prefix}${entry.name}`);
|
|
25871
|
-
}
|
|
26059
|
+
each(`res://${prefix}${entry.name}`, readFileSync7(path, "utf8"));
|
|
25872
26060
|
}
|
|
25873
26061
|
}
|
|
25874
26062
|
};
|
|
25875
26063
|
visit(projectPath, "");
|
|
25876
|
-
|
|
26064
|
+
}
|
|
26065
|
+
function scriptsNaming(projectPath, classes) {
|
|
26066
|
+
const named = new Map(classes.map((name) => [name, ""]));
|
|
26067
|
+
const naming = [];
|
|
26068
|
+
eachScript(projectPath, (script, source) => {
|
|
26069
|
+
const declares = DECLARATION.exec(source)?.[1];
|
|
26070
|
+
if (declares !== undefined && named.has(declares)) {
|
|
26071
|
+
return;
|
|
26072
|
+
}
|
|
26073
|
+
const code = codeOf(source);
|
|
26074
|
+
if (classesNamedIn(code, named).length > 0) {
|
|
26075
|
+
naming.push({ script, tool: /^\s*@tool\b/m.test(code) });
|
|
26076
|
+
}
|
|
26077
|
+
});
|
|
26078
|
+
return naming.sort((a, b) => a.script.localeCompare(b.script));
|
|
26079
|
+
}
|
|
26080
|
+
function codeOf(source) {
|
|
26081
|
+
let code = "";
|
|
26082
|
+
let at = 0;
|
|
26083
|
+
while (at < source.length) {
|
|
26084
|
+
const here = source[at] ?? "";
|
|
26085
|
+
if (here === "#") {
|
|
26086
|
+
const end = source.indexOf(`
|
|
26087
|
+
`, at);
|
|
26088
|
+
at = end === -1 ? source.length : end;
|
|
26089
|
+
continue;
|
|
26090
|
+
}
|
|
26091
|
+
if (here === '"' || here === "'") {
|
|
26092
|
+
const fence = source.startsWith(here.repeat(3), at) ? here.repeat(3) : here;
|
|
26093
|
+
let end = at + fence.length;
|
|
26094
|
+
while (end < source.length && !source.startsWith(fence, end)) {
|
|
26095
|
+
end += source[end] === "\\" ? 2 : 1;
|
|
26096
|
+
}
|
|
26097
|
+
const literal = source.slice(at, Math.min(end + fence.length, source.length));
|
|
26098
|
+
code += literal.replace(/[^\n]/g, " ");
|
|
26099
|
+
at = end + fence.length;
|
|
26100
|
+
continue;
|
|
26101
|
+
}
|
|
26102
|
+
code += here;
|
|
26103
|
+
at += 1;
|
|
26104
|
+
}
|
|
26105
|
+
return code;
|
|
25877
26106
|
}
|
|
25878
26107
|
function cachedClasses(projectPath) {
|
|
25879
26108
|
const cache = join6(projectPath, ".godot", "global_script_class_cache.cfg");
|
|
@@ -26697,6 +26926,9 @@ class GodotDAPClient {
|
|
|
26697
26926
|
isConnected() {
|
|
26698
26927
|
return this.connected;
|
|
26699
26928
|
}
|
|
26929
|
+
connection() {
|
|
26930
|
+
return this.connected ? this.socket : null;
|
|
26931
|
+
}
|
|
26700
26932
|
isStopped() {
|
|
26701
26933
|
return this.halt !== null;
|
|
26702
26934
|
}
|
|
@@ -27837,14 +28069,48 @@ class GodotLocator {
|
|
|
27837
28069
|
|
|
27838
28070
|
// src/headless.ts
|
|
27839
28071
|
import { execFile as execFile3 } from "node:child_process";
|
|
27840
|
-
import {
|
|
27841
|
-
import {
|
|
27842
|
-
import { join as join11 } from "node:path";
|
|
28072
|
+
import { writeFileSync as writeFileSync7 } from "node:fs";
|
|
28073
|
+
import { join as join12 } from "node:path";
|
|
27843
28074
|
import { promisify as promisify3 } from "node:util";
|
|
27844
28075
|
|
|
27845
28076
|
// src/scratch.ts
|
|
27846
|
-
import { rmSync as rmSync4 } from "node:fs";
|
|
27847
|
-
|
|
28077
|
+
import { existsSync as existsSync8, mkdtempSync, readdirSync as readdirSync4, rmSync as rmSync4 } from "node:fs";
|
|
28078
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
28079
|
+
import { join as join11 } from "node:path";
|
|
28080
|
+
import process7 from "node:process";
|
|
28081
|
+
var SWEPT = { recursive: true, force: true };
|
|
28082
|
+
var AGAIN_AFTER_MS = [250, 500, 1000, 2000, 4000, 8000, 16000];
|
|
28083
|
+
var waiting = new Set;
|
|
28084
|
+
var atExit = new Set;
|
|
28085
|
+
var KINDS = [
|
|
28086
|
+
"desktop",
|
|
28087
|
+
"export",
|
|
28088
|
+
"import",
|
|
28089
|
+
"params",
|
|
28090
|
+
"runtime-screenshot",
|
|
28091
|
+
"tests"
|
|
28092
|
+
];
|
|
28093
|
+
function scratchDirectory(kind) {
|
|
28094
|
+
return mkdtempSync(join11(tmpdir3(), `gdharness-${kind}-${process7.pid}-`));
|
|
28095
|
+
}
|
|
28096
|
+
function sweepAbandonedScratch(directory = tmpdir3()) {
|
|
28097
|
+
let entries;
|
|
28098
|
+
try {
|
|
28099
|
+
entries = readdirSync4(directory);
|
|
28100
|
+
} catch {
|
|
28101
|
+
return;
|
|
28102
|
+
}
|
|
28103
|
+
const named = new RegExp(`^gdharness-(${KINDS.join("|")})-(\\d+)-[A-Za-z0-9]{6}$`);
|
|
28104
|
+
for (const entry of entries) {
|
|
28105
|
+
const owner = named.exec(entry)?.[2];
|
|
28106
|
+
if (owner === undefined || alive(Number(owner))) {
|
|
28107
|
+
continue;
|
|
28108
|
+
}
|
|
28109
|
+
try {
|
|
28110
|
+
remove(join11(directory, entry));
|
|
28111
|
+
} catch {}
|
|
28112
|
+
}
|
|
28113
|
+
}
|
|
27848
28114
|
function discardWith(remove, paths) {
|
|
27849
28115
|
const left = [];
|
|
27850
28116
|
for (const path of paths) {
|
|
@@ -27860,9 +28126,54 @@ function discardWith(remove, paths) {
|
|
|
27860
28126
|
return left;
|
|
27861
28127
|
}
|
|
27862
28128
|
function discard(...paths) {
|
|
27863
|
-
|
|
27864
|
-
|
|
27865
|
-
|
|
28129
|
+
const left = discardWith(remove, paths);
|
|
28130
|
+
for (const { path } of left) {
|
|
28131
|
+
tryAgain(path, 0);
|
|
28132
|
+
}
|
|
28133
|
+
return left;
|
|
28134
|
+
}
|
|
28135
|
+
function remove(path) {
|
|
28136
|
+
rmSync4(path, SWEPT);
|
|
28137
|
+
}
|
|
28138
|
+
function tryAgain(path, attempt) {
|
|
28139
|
+
if (attempt === 0) {
|
|
28140
|
+
if (waiting.has(path)) {
|
|
28141
|
+
return;
|
|
28142
|
+
}
|
|
28143
|
+
waiting.add(path);
|
|
28144
|
+
atExit.delete(path);
|
|
28145
|
+
lastTryOnExit();
|
|
28146
|
+
}
|
|
28147
|
+
const after = AGAIN_AFTER_MS[attempt];
|
|
28148
|
+
if (after === undefined || !existsSync8(path)) {
|
|
28149
|
+
waiting.delete(path);
|
|
28150
|
+
if (after === undefined) {
|
|
28151
|
+
atExit.add(path);
|
|
28152
|
+
}
|
|
28153
|
+
return;
|
|
28154
|
+
}
|
|
28155
|
+
setTimeout(() => {
|
|
28156
|
+
try {
|
|
28157
|
+
remove(path);
|
|
28158
|
+
waiting.delete(path);
|
|
28159
|
+
} catch {
|
|
28160
|
+
tryAgain(path, attempt + 1);
|
|
28161
|
+
}
|
|
28162
|
+
}, after).unref();
|
|
28163
|
+
}
|
|
28164
|
+
var lastTryWatched = false;
|
|
28165
|
+
function lastTryOnExit() {
|
|
28166
|
+
if (lastTryWatched) {
|
|
28167
|
+
return;
|
|
28168
|
+
}
|
|
28169
|
+
lastTryWatched = true;
|
|
28170
|
+
process7.once("exit", () => {
|
|
28171
|
+
for (const path of [...waiting, ...atExit]) {
|
|
28172
|
+
try {
|
|
28173
|
+
remove(path);
|
|
28174
|
+
} catch {}
|
|
28175
|
+
}
|
|
28176
|
+
});
|
|
27866
28177
|
}
|
|
27867
28178
|
|
|
27868
28179
|
// src/headless.ts
|
|
@@ -27905,9 +28216,9 @@ function reason(stdout, stderr) {
|
|
|
27905
28216
|
return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
|
|
27906
28217
|
}
|
|
27907
28218
|
async function runImport(godotPath, projectPath) {
|
|
27908
|
-
const logDir =
|
|
28219
|
+
const logDir = scratchDirectory("import");
|
|
27909
28220
|
try {
|
|
27910
|
-
const { stderr } = await run3(godotPath, ["--headless", "--log-file",
|
|
28221
|
+
const { stderr } = await run3(godotPath, ["--headless", "--log-file", join12(logDir, "engine.log"), "--path", projectPath, "--import"], { signal: callSignal() });
|
|
27911
28222
|
return { ok: true, messages: problems(stderr) };
|
|
27912
28223
|
} catch (error) {
|
|
27913
28224
|
if (error instanceof Error && "stdout" in error && "stderr" in error) {
|
|
@@ -27928,13 +28239,13 @@ async function runImport(godotPath, projectPath) {
|
|
|
27928
28239
|
}
|
|
27929
28240
|
}
|
|
27930
28241
|
async function runOperation(engine, operation, params, projectPath) {
|
|
27931
|
-
const paramsDir =
|
|
27932
|
-
const paramsFile =
|
|
28242
|
+
const paramsDir = scratchDirectory("params");
|
|
28243
|
+
const paramsFile = join12(paramsDir, `${operation}.json`);
|
|
27933
28244
|
writeFileSync7(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
|
|
27934
28245
|
const args = [
|
|
27935
28246
|
"--headless",
|
|
27936
28247
|
"--log-file",
|
|
27937
|
-
|
|
28248
|
+
join12(paramsDir, "engine.log"),
|
|
27938
28249
|
"--path",
|
|
27939
28250
|
projectPath,
|
|
27940
28251
|
"--script",
|
|
@@ -28888,7 +29199,7 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
28888
29199
|
}
|
|
28889
29200
|
|
|
28890
29201
|
// src/outside.ts
|
|
28891
|
-
import { spawn } from "node:child_process";
|
|
29202
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
28892
29203
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
28893
29204
|
function changesFrom(env, base) {
|
|
28894
29205
|
const changes = {};
|
|
@@ -28922,7 +29233,7 @@ function readLaunched(printed) {
|
|
|
28922
29233
|
}
|
|
28923
29234
|
async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = KEEPER_SCRIPT) {
|
|
28924
29235
|
return await new Promise((resolve) => {
|
|
28925
|
-
const launcher =
|
|
29236
|
+
const launcher = spawn2(runtime, [keeper, "launch"], {
|
|
28926
29237
|
stdio: ["pipe", "pipe", "pipe"],
|
|
28927
29238
|
windowsHide: true
|
|
28928
29239
|
});
|
|
@@ -28959,14 +29270,14 @@ var run4 = promisify4(execFile4);
|
|
|
28959
29270
|
var ASK_TIMEOUT_MS2 = 6000;
|
|
28960
29271
|
async function cpuSecondsOf(pid) {
|
|
28961
29272
|
try {
|
|
28962
|
-
return process.platform === "win32" ? await
|
|
29273
|
+
return process.platform === "win32" ? await secondsOnWindows(pid) : await askPosix2(pid);
|
|
28963
29274
|
} catch {
|
|
28964
29275
|
return;
|
|
28965
29276
|
}
|
|
28966
29277
|
}
|
|
28967
|
-
async function
|
|
28968
|
-
const
|
|
28969
|
-
const seconds = Number(
|
|
29278
|
+
async function secondsOnWindows(pid) {
|
|
29279
|
+
const said = await askWindows(`(Get-Process -Id ${pid}).CPU`, ASK_TIMEOUT_MS2);
|
|
29280
|
+
const seconds = Number(said.trim().replace(",", "."));
|
|
28970
29281
|
return Number.isFinite(seconds) ? seconds : undefined;
|
|
28971
29282
|
}
|
|
28972
29283
|
async function askPosix2(pid) {
|
|
@@ -28990,14 +29301,14 @@ function secondsFromClock(printed) {
|
|
|
28990
29301
|
}
|
|
28991
29302
|
|
|
28992
29303
|
// src/project-scan.ts
|
|
28993
|
-
import { existsSync as
|
|
28994
|
-
import { join as
|
|
29304
|
+
import { existsSync as existsSync9, readdirSync as readdirSync5, readFileSync as readFileSync11 } from "node:fs";
|
|
29305
|
+
import { join as join13 } from "node:path";
|
|
28995
29306
|
var SKIPPED = new Set(["node_modules"]);
|
|
28996
29307
|
function skipped(name) {
|
|
28997
29308
|
return name.startsWith(".") || SKIPPED.has(name);
|
|
28998
29309
|
}
|
|
28999
29310
|
function steppedOver(directory) {
|
|
29000
|
-
return
|
|
29311
|
+
return existsSync9(join13(directory, ".gdignore"));
|
|
29001
29312
|
}
|
|
29002
29313
|
var ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
|
|
29003
29314
|
function projectStructure(projectPath) {
|
|
@@ -29006,12 +29317,12 @@ function projectStructure(projectPath) {
|
|
|
29006
29317
|
if (steppedOver(directory)) {
|
|
29007
29318
|
return;
|
|
29008
29319
|
}
|
|
29009
|
-
for (const entry of
|
|
29320
|
+
for (const entry of readdirSync5(directory, { withFileTypes: true })) {
|
|
29010
29321
|
if (skipped(entry.name)) {
|
|
29011
29322
|
continue;
|
|
29012
29323
|
}
|
|
29013
29324
|
if (entry.isDirectory()) {
|
|
29014
|
-
visit(
|
|
29325
|
+
visit(join13(directory, entry.name));
|
|
29015
29326
|
} else if (entry.isFile()) {
|
|
29016
29327
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
29017
29328
|
if (extension === "tscn") {
|
|
@@ -29036,20 +29347,20 @@ function scriptsWithoutUid(projectPath) {
|
|
|
29036
29347
|
if (steppedOver(directory)) {
|
|
29037
29348
|
return;
|
|
29038
29349
|
}
|
|
29039
|
-
for (const entry of
|
|
29350
|
+
for (const entry of readdirSync5(directory, { withFileTypes: true })) {
|
|
29040
29351
|
if (skipped(entry.name)) {
|
|
29041
29352
|
continue;
|
|
29042
29353
|
}
|
|
29043
29354
|
const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
29044
29355
|
if (entry.isDirectory()) {
|
|
29045
|
-
visit(
|
|
29356
|
+
visit(join13(directory, entry.name), spelled);
|
|
29046
29357
|
continue;
|
|
29047
29358
|
}
|
|
29048
29359
|
if (!entry.isFile()) {
|
|
29049
29360
|
continue;
|
|
29050
29361
|
}
|
|
29051
29362
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
29052
|
-
if (CARRIES_A_UID.has(extension) && !
|
|
29363
|
+
if (CARRIES_A_UID.has(extension) && !existsSync9(join13(directory, `${entry.name}.uid`))) {
|
|
29053
29364
|
found.push(spelled);
|
|
29054
29365
|
}
|
|
29055
29366
|
}
|
|
@@ -29077,14 +29388,14 @@ function searchProject(projectPath, options) {
|
|
|
29077
29388
|
if (steppedOver(directory)) {
|
|
29078
29389
|
return;
|
|
29079
29390
|
}
|
|
29080
|
-
for (const entry of
|
|
29391
|
+
for (const entry of readdirSync5(directory, { withFileTypes: true })) {
|
|
29081
29392
|
if (full()) {
|
|
29082
29393
|
return;
|
|
29083
29394
|
}
|
|
29084
29395
|
if (skipped(entry.name)) {
|
|
29085
29396
|
continue;
|
|
29086
29397
|
}
|
|
29087
|
-
const entryPath =
|
|
29398
|
+
const entryPath = join13(directory, entry.name);
|
|
29088
29399
|
if (entry.isDirectory()) {
|
|
29089
29400
|
visit(entryPath);
|
|
29090
29401
|
continue;
|
|
@@ -29119,12 +29430,12 @@ function searchProject(projectPath, options) {
|
|
|
29119
29430
|
|
|
29120
29431
|
// src/reimport.ts
|
|
29121
29432
|
import { createHash as createHash2 } from "node:crypto";
|
|
29122
|
-
import { existsSync as
|
|
29123
|
-
import { basename as basename2, join as
|
|
29433
|
+
import { existsSync as existsSync10, readFileSync as readFileSync12, rmSync as rmSync5, statSync as statSync5, utimesSync, writeFileSync as writeFileSync8 } from "node:fs";
|
|
29434
|
+
import { basename as basename2, join as join14 } from "node:path";
|
|
29124
29435
|
function forceNextImport(projectPath, resourcePath) {
|
|
29125
29436
|
const relative = resourcePath.replace(/^res:\/\//, "");
|
|
29126
|
-
const sidecar =
|
|
29127
|
-
if (!
|
|
29437
|
+
const sidecar = join14(projectPath, `${relative}.import`);
|
|
29438
|
+
if (!existsSync10(sidecar)) {
|
|
29128
29439
|
return;
|
|
29129
29440
|
}
|
|
29130
29441
|
const { atime, mtimeMs } = statSync5(sidecar);
|
|
@@ -29134,7 +29445,7 @@ function forceNextImport(projectPath, resourcePath) {
|
|
|
29134
29445
|
}
|
|
29135
29446
|
function importRecord(projectPath, resourcePath) {
|
|
29136
29447
|
const digest = createHash2("md5").update(resourcePath).digest("hex");
|
|
29137
|
-
return
|
|
29448
|
+
return join14(projectPath, ".godot", "imported", `${basename2(resourcePath)}-${digest}.md5`);
|
|
29138
29449
|
}
|
|
29139
29450
|
|
|
29140
29451
|
// src/resources.ts
|
|
@@ -29380,9 +29691,9 @@ function setupResourceHandlers(mcp, getProjectPath) {
|
|
|
29380
29691
|
|
|
29381
29692
|
// src/restart-note.ts
|
|
29382
29693
|
import { mkdirSync as mkdirSync7, readFileSync as readFileSync14, rmSync as rmSync6, writeFileSync as writeFileSync9 } from "node:fs";
|
|
29383
|
-
import { dirname as dirname8, join as
|
|
29694
|
+
import { dirname as dirname8, join as join15 } from "node:path";
|
|
29384
29695
|
function restartNotePath(projectPath) {
|
|
29385
|
-
return
|
|
29696
|
+
return join15(projectPath, ".godot", "gdharness-restart.json");
|
|
29386
29697
|
}
|
|
29387
29698
|
function noteRestartBegun(note) {
|
|
29388
29699
|
const path = restartNotePath(note.projectPath);
|
|
@@ -29418,28 +29729,30 @@ function restartSettled(projectPath) {
|
|
|
29418
29729
|
}
|
|
29419
29730
|
|
|
29420
29731
|
// src/run-record.ts
|
|
29421
|
-
import {
|
|
29732
|
+
import { execFile as execFile5 } from "node:child_process";
|
|
29422
29733
|
import { createHash as createHash3 } from "node:crypto";
|
|
29423
29734
|
import {
|
|
29424
29735
|
mkdirSync as mkdirSync8,
|
|
29425
29736
|
openSync as openSync2,
|
|
29426
|
-
readdirSync as
|
|
29737
|
+
readdirSync as readdirSync6,
|
|
29427
29738
|
readFileSync as readFileSync15,
|
|
29428
29739
|
realpathSync as realpathSync3,
|
|
29429
29740
|
rmSync as rmSync7,
|
|
29430
29741
|
statSync as statSync6,
|
|
29431
29742
|
writeFileSync as writeFileSync10
|
|
29432
29743
|
} from "node:fs";
|
|
29433
|
-
import { basename as basename3, dirname as dirname9, join as
|
|
29744
|
+
import { basename as basename3, dirname as dirname9, join as join16, resolve as resolve5 } from "node:path";
|
|
29745
|
+
import { promisify as promisify5 } from "node:util";
|
|
29746
|
+
var execFileAsync = promisify5(execFile5);
|
|
29434
29747
|
function runsDirectory() {
|
|
29435
|
-
return
|
|
29748
|
+
return join16(runtimeDirectory(), "runs");
|
|
29436
29749
|
}
|
|
29437
29750
|
function realAsFarAsItExists(path) {
|
|
29438
29751
|
const missing = [];
|
|
29439
29752
|
let at = path;
|
|
29440
29753
|
for (;; ) {
|
|
29441
29754
|
try {
|
|
29442
|
-
return
|
|
29755
|
+
return join16(realpathSync3.native(at), ...missing);
|
|
29443
29756
|
} catch {
|
|
29444
29757
|
const parent = dirname9(at);
|
|
29445
29758
|
if (parent === at) {
|
|
@@ -29456,16 +29769,16 @@ function projectKey(projectPath) {
|
|
|
29456
29769
|
return createHash3("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
|
|
29457
29770
|
}
|
|
29458
29771
|
function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
29459
|
-
return
|
|
29772
|
+
return join16(directory, "runs", `note-${projectKey(projectPath)}.json`);
|
|
29460
29773
|
}
|
|
29461
29774
|
var SHARED_NOTE = "run.json";
|
|
29462
29775
|
function openTranscript(startedAt) {
|
|
29463
29776
|
mkdirSync8(runsDirectory(), { recursive: true });
|
|
29464
|
-
const path =
|
|
29777
|
+
const path = join16(runsDirectory(), `run-${startedAt}.log`);
|
|
29465
29778
|
return { path, fd: openSync2(path, "a") };
|
|
29466
29779
|
}
|
|
29467
29780
|
function editorNotePath(projectPath, directory = runtimeDirectory()) {
|
|
29468
|
-
return
|
|
29781
|
+
return join16(directory, "runs", `editor-${projectKey(projectPath)}.json`);
|
|
29469
29782
|
}
|
|
29470
29783
|
function writeEditorRunNote(note) {
|
|
29471
29784
|
mkdirSync8(runsDirectory(), { recursive: true });
|
|
@@ -29473,7 +29786,7 @@ function writeEditorRunNote(note) {
|
|
|
29473
29786
|
}
|
|
29474
29787
|
function readEditorRunNote(projectPath) {
|
|
29475
29788
|
for (const directory of runtimeDirectories()) {
|
|
29476
|
-
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(
|
|
29789
|
+
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join16(directory, "runs", "editor-run.json"));
|
|
29477
29790
|
if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
|
|
29478
29791
|
return note;
|
|
29479
29792
|
}
|
|
@@ -29505,7 +29818,7 @@ function readRunRecord(projectPath) {
|
|
|
29505
29818
|
if (own !== null) {
|
|
29506
29819
|
return own;
|
|
29507
29820
|
}
|
|
29508
|
-
const shared = recordAt(
|
|
29821
|
+
const shared = recordAt(join16(directory, "runs", SHARED_NOTE));
|
|
29509
29822
|
if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
|
|
29510
29823
|
return shared;
|
|
29511
29824
|
}
|
|
@@ -29517,13 +29830,13 @@ function everyRunRecord() {
|
|
|
29517
29830
|
for (const directory of runtimeDirectories()) {
|
|
29518
29831
|
let entries;
|
|
29519
29832
|
try {
|
|
29520
|
-
entries =
|
|
29833
|
+
entries = readdirSync6(join16(directory, "runs"));
|
|
29521
29834
|
} catch {
|
|
29522
29835
|
continue;
|
|
29523
29836
|
}
|
|
29524
29837
|
for (const entry of entries) {
|
|
29525
29838
|
if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
|
|
29526
|
-
const record = recordAt(
|
|
29839
|
+
const record = recordAt(join16(directory, "runs", entry));
|
|
29527
29840
|
if (record !== null) {
|
|
29528
29841
|
found.push(record);
|
|
29529
29842
|
}
|
|
@@ -29561,49 +29874,47 @@ function recordAt(path) {
|
|
|
29561
29874
|
...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {}
|
|
29562
29875
|
};
|
|
29563
29876
|
}
|
|
29564
|
-
function runningAs(pid) {
|
|
29877
|
+
async function runningAs(pid) {
|
|
29565
29878
|
if (process.platform === "win32") {
|
|
29566
|
-
const answer = windowsCommandLine(pid);
|
|
29879
|
+
const answer = await windowsCommandLine(pid);
|
|
29567
29880
|
if (answer !== null) {
|
|
29568
29881
|
const [, ...rest] = answer.split(`
|
|
29569
29882
|
`);
|
|
29570
29883
|
const line = rest.join(`
|
|
29571
29884
|
`).trim();
|
|
29572
|
-
const began = startedAt(pid, answer);
|
|
29885
|
+
const began = await startedAt(pid, answer);
|
|
29573
29886
|
if (line !== "") {
|
|
29574
29887
|
return { kind: "commandLine", text: line, ...began === null ? {} : { startedAt: began } };
|
|
29575
29888
|
}
|
|
29576
29889
|
}
|
|
29577
|
-
const image = windowsImage(pid);
|
|
29890
|
+
const image = await windowsImage(pid);
|
|
29578
29891
|
return image === null ? null : { kind: "image", text: image };
|
|
29579
29892
|
}
|
|
29580
29893
|
try {
|
|
29581
|
-
const began = startedAt(pid, null);
|
|
29894
|
+
const began = await startedAt(pid, null);
|
|
29582
29895
|
const when = began === null ? {} : { startedAt: began };
|
|
29583
29896
|
if (process.platform === "linux") {
|
|
29584
29897
|
const raw = readFileSync15(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
29585
29898
|
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
29586
29899
|
}
|
|
29587
|
-
const args =
|
|
29588
|
-
encoding: "utf8",
|
|
29589
|
-
timeout: 15000
|
|
29590
|
-
}).trim();
|
|
29900
|
+
const args = (await execFileAsync("ps", ["-p", String(pid), "-o", "args="], { encoding: "utf8", timeout: 15000 })).stdout.trim();
|
|
29591
29901
|
return args === "" ? null : { kind: "commandLine", text: args, ...when };
|
|
29592
29902
|
} catch {
|
|
29593
29903
|
return null;
|
|
29594
29904
|
}
|
|
29595
29905
|
}
|
|
29596
|
-
function listeningPid(port) {
|
|
29906
|
+
async function listeningPid(port) {
|
|
29597
29907
|
try {
|
|
29598
29908
|
if (process.platform === "win32") {
|
|
29599
|
-
|
|
29909
|
+
const { stdout } = await execFileAsync("netstat", ["-ano"], { encoding: "utf8", timeout: 15000 });
|
|
29910
|
+
return listeningPidInNetstat(stdout, port);
|
|
29600
29911
|
}
|
|
29601
29912
|
for (const [command, args, pattern] of [
|
|
29602
29913
|
["lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], /^(\d+)/],
|
|
29603
29914
|
["ss", ["-ltnpH", `sport = :${port}`], /pid=(\d+)/]
|
|
29604
29915
|
]) {
|
|
29605
29916
|
try {
|
|
29606
|
-
const said =
|
|
29917
|
+
const said = (await execFileAsync(command, [...args], { encoding: "utf8", timeout: 15000 })).stdout.trim();
|
|
29607
29918
|
const found = pattern.exec(said);
|
|
29608
29919
|
if (found?.[1] !== undefined) {
|
|
29609
29920
|
return Number(found[1]);
|
|
@@ -29628,39 +29939,31 @@ function listeningPidInNetstat(printed, port) {
|
|
|
29628
29939
|
}
|
|
29629
29940
|
return null;
|
|
29630
29941
|
}
|
|
29631
|
-
function windowsCommandLine(pid) {
|
|
29942
|
+
async function windowsCommandLine(pid) {
|
|
29632
29943
|
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();
|
|
29944
|
+
const answer = (await askWindows(`$p = Get-CimInstance Win32_Process -Filter "ProcessId=${pid}"; if ($p) { $p.CreationDate.ToUniversalTime().ToString("o"); $p.CommandLine }`, 15000)).trim();
|
|
29639
29945
|
return answer === "" ? null : answer;
|
|
29640
29946
|
} catch {
|
|
29641
29947
|
return null;
|
|
29642
29948
|
}
|
|
29643
29949
|
}
|
|
29644
|
-
function startedAt(pid, windowsAnswer) {
|
|
29950
|
+
async function startedAt(pid, windowsAnswer) {
|
|
29645
29951
|
if (process.platform === "win32") {
|
|
29646
29952
|
const when = Date.parse(windowsAnswer?.split(`
|
|
29647
29953
|
`)[0]?.trim() ?? "");
|
|
29648
29954
|
return Number.isNaN(when) ? null : when;
|
|
29649
29955
|
}
|
|
29650
29956
|
try {
|
|
29651
|
-
const said =
|
|
29652
|
-
encoding: "utf8",
|
|
29653
|
-
timeout: 15000
|
|
29654
|
-
}).trim();
|
|
29957
|
+
const said = (await execFileAsync("ps", ["-p", String(pid), "-o", "lstart="], { encoding: "utf8", timeout: 15000 })).stdout.trim();
|
|
29655
29958
|
const when = Date.parse(said);
|
|
29656
29959
|
return said === "" || Number.isNaN(when) ? null : when;
|
|
29657
29960
|
} catch {
|
|
29658
29961
|
return null;
|
|
29659
29962
|
}
|
|
29660
29963
|
}
|
|
29661
|
-
function windowsImage(pid) {
|
|
29964
|
+
async function windowsImage(pid) {
|
|
29662
29965
|
try {
|
|
29663
|
-
const csv =
|
|
29966
|
+
const { stdout: csv } = await execFileAsync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
|
|
29664
29967
|
encoding: "utf8",
|
|
29665
29968
|
timeout: 15000,
|
|
29666
29969
|
windowsHide: true
|
|
@@ -29670,11 +29973,11 @@ function windowsImage(pid) {
|
|
|
29670
29973
|
return null;
|
|
29671
29974
|
}
|
|
29672
29975
|
}
|
|
29673
|
-
function stillTheRecordedRun(record) {
|
|
29674
|
-
return judgeRun(record, runningAs(record.pid), "confirmed");
|
|
29976
|
+
async function stillTheRecordedRun(record) {
|
|
29977
|
+
return judgeRun(record, await runningAs(record.pid), "confirmed");
|
|
29675
29978
|
}
|
|
29676
|
-
function couldStillBeTheRecordedRun(record) {
|
|
29677
|
-
return judgeRun(record, runningAs(record.pid), "possible");
|
|
29979
|
+
async function couldStillBeTheRecordedRun(record) {
|
|
29980
|
+
return judgeRun(record, await runningAs(record.pid), "possible");
|
|
29678
29981
|
}
|
|
29679
29982
|
var SAME_RUN_WINDOW_MS = 1e4;
|
|
29680
29983
|
function judgeRun(record, running, needed) {
|
|
@@ -29702,8 +30005,8 @@ function judgeRun(record, running, needed) {
|
|
|
29702
30005
|
const project = process.platform === "win32" ? record.projectPath.toLowerCase() : record.projectPath;
|
|
29703
30006
|
return said.includes(project);
|
|
29704
30007
|
}
|
|
29705
|
-
function stillTheAnnouncedGame(pid, projectPath, announcedAt) {
|
|
29706
|
-
return judgeAnnouncedGame(runningAs(pid), projectPath, announcedAt);
|
|
30008
|
+
async function stillTheAnnouncedGame(pid, projectPath, announcedAt) {
|
|
30009
|
+
return judgeAnnouncedGame(await runningAs(pid), projectPath, announcedAt);
|
|
29707
30010
|
}
|
|
29708
30011
|
function judgeAnnouncedGame(running, projectPath, announcedAt) {
|
|
29709
30012
|
if (running?.kind !== "commandLine" || running.startedAt === undefined) {
|
|
@@ -29720,7 +30023,7 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
|
|
|
29720
30023
|
}
|
|
29721
30024
|
function clearRunRecord(projectPath) {
|
|
29722
30025
|
for (const directory of runtimeDirectories()) {
|
|
29723
|
-
const shared =
|
|
30026
|
+
const shared = join16(directory, "runs", SHARED_NOTE);
|
|
29724
30027
|
const sharedRecord = recordAt(shared);
|
|
29725
30028
|
const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
|
|
29726
30029
|
for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
|
|
@@ -29742,7 +30045,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
29742
30045
|
const kept = new Set(everyRunRecord().map((record) => record.transcript));
|
|
29743
30046
|
let entries;
|
|
29744
30047
|
try {
|
|
29745
|
-
entries =
|
|
30048
|
+
entries = readdirSync6(directory);
|
|
29746
30049
|
} catch {
|
|
29747
30050
|
return;
|
|
29748
30051
|
}
|
|
@@ -29750,7 +30053,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
29750
30053
|
if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
|
|
29751
30054
|
continue;
|
|
29752
30055
|
}
|
|
29753
|
-
const path =
|
|
30056
|
+
const path = join16(directory, entry);
|
|
29754
30057
|
if (kept.has(path) || ageMs(path, now) < keepMs) {
|
|
29755
30058
|
continue;
|
|
29756
30059
|
}
|
|
@@ -29764,16 +30067,16 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
29764
30067
|
import { createHash as createHash4 } from "node:crypto";
|
|
29765
30068
|
import {
|
|
29766
30069
|
cpSync,
|
|
29767
|
-
existsSync as
|
|
30070
|
+
existsSync as existsSync11,
|
|
29768
30071
|
mkdirSync as mkdirSync9,
|
|
29769
|
-
readdirSync as
|
|
30072
|
+
readdirSync as readdirSync7,
|
|
29770
30073
|
readFileSync as readFileSync16,
|
|
29771
30074
|
rmdirSync,
|
|
29772
30075
|
rmSync as rmSync8,
|
|
29773
30076
|
statSync as statSync7,
|
|
29774
30077
|
writeFileSync as writeFileSync11
|
|
29775
30078
|
} from "node:fs";
|
|
29776
|
-
import { dirname as dirname10, join as
|
|
30079
|
+
import { dirname as dirname10, join as join17 } from "node:path";
|
|
29777
30080
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
29778
30081
|
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
29779
30082
|
var EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
|
|
@@ -29786,11 +30089,11 @@ var DIGEST_MARKER = ".gdharness-digest";
|
|
|
29786
30089
|
function editorAddonDigest(from = shippedAddonsDirectory()) {
|
|
29787
30090
|
const hash = createHash4("sha256");
|
|
29788
30091
|
const walk = (directory, relative) => {
|
|
29789
|
-
for (const name of
|
|
30092
|
+
for (const name of readdirSync7(directory).sort()) {
|
|
29790
30093
|
if (name === VERSION_MARKER || name === DIGEST_MARKER) {
|
|
29791
30094
|
continue;
|
|
29792
30095
|
}
|
|
29793
|
-
const path =
|
|
30096
|
+
const path = join17(directory, name);
|
|
29794
30097
|
const inside = `${relative}/${name}`;
|
|
29795
30098
|
if (statSync7(path).isDirectory()) {
|
|
29796
30099
|
walk(path, inside);
|
|
@@ -29804,7 +30107,7 @@ function editorAddonDigest(from = shippedAddonsDirectory()) {
|
|
|
29804
30107
|
}
|
|
29805
30108
|
};
|
|
29806
30109
|
for (const name of EDITOR_PLUGINS) {
|
|
29807
|
-
walk(
|
|
30110
|
+
walk(join17(from, name), name);
|
|
29808
30111
|
}
|
|
29809
30112
|
return hash.digest("hex");
|
|
29810
30113
|
}
|
|
@@ -29820,11 +30123,11 @@ function shippedEditorDigest() {
|
|
|
29820
30123
|
return shippedDigest ?? undefined;
|
|
29821
30124
|
}
|
|
29822
30125
|
function shippedAddonsDirectory() {
|
|
29823
|
-
return
|
|
30126
|
+
return join17(dirname10(fileURLToPath3(import.meta.url)), "godot", "addons");
|
|
29824
30127
|
}
|
|
29825
30128
|
function installedAddonVersion(projectPath) {
|
|
29826
|
-
const marker =
|
|
29827
|
-
if (!
|
|
30129
|
+
const marker = join17(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
30130
|
+
if (!existsSync11(marker)) {
|
|
29828
30131
|
return null;
|
|
29829
30132
|
}
|
|
29830
30133
|
const said = readFileSync16(marker, "utf8").trim();
|
|
@@ -30479,7 +30782,11 @@ var TOOL_SPECS = [
|
|
|
30479
30782
|
name: "editor_run",
|
|
30480
30783
|
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
30784
|
parameters: {
|
|
30482
|
-
projectPath: {
|
|
30785
|
+
projectPath: {
|
|
30786
|
+
...PROJECT_PATH,
|
|
30787
|
+
ops: ["start", "check", "stop", "wait"],
|
|
30788
|
+
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."
|
|
30789
|
+
},
|
|
30483
30790
|
scene: {
|
|
30484
30791
|
type: "string",
|
|
30485
30792
|
ops: ["start", "check"],
|
|
@@ -30497,6 +30804,11 @@ var TOOL_SPECS = [
|
|
|
30497
30804
|
ops: ["start"],
|
|
30498
30805
|
description: "start: on Windows, show the window on the desktop in use. By default a windowed run this server starts goes on a desktop of its own, where it renders and runtime_capture works but the window never shows and never takes the keyboard. A game a visible editor plays opens on the desktop in use, and one a hidden editor (editor_launch hidden: true) plays goes on its desktop. Elsewhere than Windows the window shows either way. Default false."
|
|
30499
30806
|
},
|
|
30807
|
+
silent: {
|
|
30808
|
+
type: "boolean",
|
|
30809
|
+
ops: ["start"],
|
|
30810
|
+
description: "start: run the game with no sound out of the machine, by starting the engine with --audio-driver Dummy: every stream still plays and every playback position still advances, so what is playing can still be asked. The editor takes no engine flags for a game it plays, so a silent windowed run is started by this server, and the debug_* tools do not answer for it. A headless run, check's included, is silent already. Default false, or true when GDHARNESS_SILENT is set in the server's environment to anything but 0 or false; silent: false then plays through the editor as before."
|
|
30811
|
+
},
|
|
30500
30812
|
savesIn: {
|
|
30501
30813
|
type: "string",
|
|
30502
30814
|
ops: ["start", "check"],
|
|
@@ -30611,7 +30923,7 @@ var TOOL_SPECS = [
|
|
|
30611
30923
|
},
|
|
30612
30924
|
{
|
|
30613
30925
|
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.",
|
|
30926
|
+
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
30927
|
parameters: {
|
|
30616
30928
|
projectPath: PROJECT_PATH,
|
|
30617
30929
|
timeoutMs: {
|
|
@@ -30717,7 +31029,7 @@ var TOOL_SPECS = [
|
|
|
30717
31029
|
says: {
|
|
30718
31030
|
type: "string",
|
|
30719
31031
|
ops: ["find"],
|
|
30720
|
-
description:
|
|
31032
|
+
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
31033
|
},
|
|
30722
31034
|
limit: {
|
|
30723
31035
|
type: "integer",
|
|
@@ -30872,7 +31184,7 @@ var TOOL_SPECS = [
|
|
|
30872
31184
|
says: {
|
|
30873
31185
|
type: "string",
|
|
30874
31186
|
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."
|
|
31187
|
+
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
31188
|
},
|
|
30877
31189
|
action: {
|
|
30878
31190
|
type: "string",
|
|
@@ -31003,7 +31315,7 @@ var TOOL_SPECS = [
|
|
|
31003
31315
|
says: {
|
|
31004
31316
|
type: "string",
|
|
31005
31317
|
ops: ["until"],
|
|
31006
|
-
description:
|
|
31318
|
+
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
31319
|
},
|
|
31008
31320
|
includeHidden: {
|
|
31009
31321
|
type: "boolean",
|
|
@@ -31124,7 +31436,7 @@ function buildToolDefinitions() {
|
|
|
31124
31436
|
// src/server.ts
|
|
31125
31437
|
var UPDATE_NOTICE_EVERY = 500;
|
|
31126
31438
|
var FEEDBACK_NOTICE_EVERY = 250;
|
|
31127
|
-
var run5 =
|
|
31439
|
+
var run5 = promisify6(execFile6);
|
|
31128
31440
|
var __dirname2 = dirname11(fileURLToPath4(import.meta.url));
|
|
31129
31441
|
var EDITOR_RESTART_TIMEOUT_MS = 90000;
|
|
31130
31442
|
var HOLD_PING_MS = 2000;
|
|
@@ -31140,15 +31452,15 @@ function captureDestinationRefusal(outputPath, projects) {
|
|
|
31140
31452
|
return `outputPath ${outputPath} does not end in .png, which is what a capture is.`;
|
|
31141
31453
|
}
|
|
31142
31454
|
const directory = realPathOr(dirname11(outputPath));
|
|
31143
|
-
if (!
|
|
31455
|
+
if (!existsSync12(directory)) {
|
|
31144
31456
|
return `outputPath's directory ${dirname11(outputPath)} does not exist; a capture creates no directories.`;
|
|
31145
31457
|
}
|
|
31146
|
-
const where =
|
|
31458
|
+
const where = join18(directory, basename4(outputPath));
|
|
31147
31459
|
const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
31148
31460
|
if (inside !== undefined) {
|
|
31149
31461
|
return `outputPath ${outputPath} is inside the project at ${inside}; a capture is saved outside it, so it never becomes part of the game.`;
|
|
31150
31462
|
}
|
|
31151
|
-
if (
|
|
31463
|
+
if (existsSync12(where)) {
|
|
31152
31464
|
return `outputPath ${outputPath} already exists; a capture is not written over a file. Name a new one.`;
|
|
31153
31465
|
}
|
|
31154
31466
|
return null;
|
|
@@ -31308,7 +31620,8 @@ function aboutTheChildren(ended, throughEditor) {
|
|
|
31308
31620
|
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
31621
|
}
|
|
31310
31622
|
if (ended.unknown === "platform") {
|
|
31311
|
-
|
|
31623
|
+
const why = whyTheProcessTreeFailed();
|
|
31624
|
+
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
31625
|
}
|
|
31313
31626
|
if (ended.unknown === "no process") {
|
|
31314
31627
|
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 +31704,6 @@ function runtimeVerdict(endpoint, after) {
|
|
|
31391
31704
|
heldAt: null
|
|
31392
31705
|
};
|
|
31393
31706
|
}
|
|
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
31707
|
function isOfType(value, kind) {
|
|
31406
31708
|
switch (kind) {
|
|
31407
31709
|
case "string":
|
|
@@ -31649,13 +31951,13 @@ function aboveTheRunner(entry) {
|
|
|
31649
31951
|
}
|
|
31650
31952
|
function elsewhereIn(projectPath, asked) {
|
|
31651
31953
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
31652
|
-
if (wanted === "" ||
|
|
31954
|
+
if (wanted === "" || existsSync12(join18(projectPath, wanted))) {
|
|
31653
31955
|
return "";
|
|
31654
31956
|
}
|
|
31655
31957
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
31656
31958
|
let near = [];
|
|
31657
31959
|
try {
|
|
31658
|
-
near =
|
|
31960
|
+
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
31961
|
} catch {
|
|
31660
31962
|
return "";
|
|
31661
31963
|
}
|
|
@@ -31671,7 +31973,7 @@ function runtimeAddonAnnouncesEditors(projectPath) {
|
|
|
31671
31973
|
return false;
|
|
31672
31974
|
}
|
|
31673
31975
|
try {
|
|
31674
|
-
return readFileSync17(
|
|
31976
|
+
return readFileSync17(join18(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
|
|
31675
31977
|
} catch {
|
|
31676
31978
|
return false;
|
|
31677
31979
|
}
|
|
@@ -31696,7 +31998,7 @@ function needsItsOwnProcess(asked) {
|
|
|
31696
31998
|
class GodotServer {
|
|
31697
31999
|
mcp;
|
|
31698
32000
|
locator = new GodotLocator;
|
|
31699
|
-
operationsScript =
|
|
32001
|
+
operationsScript = join18(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
31700
32002
|
godotBridge;
|
|
31701
32003
|
tools = buildToolDefinitions();
|
|
31702
32004
|
activeProcess = null;
|
|
@@ -31708,6 +32010,7 @@ class GodotServer {
|
|
|
31708
32010
|
lastGodotVersion = null;
|
|
31709
32011
|
lspClient = null;
|
|
31710
32012
|
dapClient = null;
|
|
32013
|
+
adapterPassed = null;
|
|
31711
32014
|
breakpointsSeededOn = null;
|
|
31712
32015
|
bridgeStartupError = null;
|
|
31713
32016
|
bridgeRetry = null;
|
|
@@ -31766,6 +32069,7 @@ class GodotServer {
|
|
|
31766
32069
|
this.handleShutdown("stdin:end", 0);
|
|
31767
32070
|
});
|
|
31768
32071
|
console.error("gdharness running on stdio");
|
|
32072
|
+
sweepAbandonedScratch();
|
|
31769
32073
|
try {
|
|
31770
32074
|
await this.takeOverFromAPredecessor();
|
|
31771
32075
|
await this.godotBridge.start();
|
|
@@ -32092,7 +32396,7 @@ class GodotServer {
|
|
|
32092
32396
|
this.callsSinceFeedback = 0;
|
|
32093
32397
|
return {
|
|
32094
32398
|
...answer,
|
|
32095
|
-
content: [...answer.content, { type: "text", text: feedbackNotice() }]
|
|
32399
|
+
content: [...answer.content, { type: "text", text: feedbackNotice(FEEDBACK_NOTICE_EVERY) }]
|
|
32096
32400
|
};
|
|
32097
32401
|
}
|
|
32098
32402
|
validateArguments(spec, args) {
|
|
@@ -32436,8 +32740,8 @@ class GodotServer {
|
|
|
32436
32740
|
if (path === undefined) {
|
|
32437
32741
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
32438
32742
|
}
|
|
32439
|
-
const file =
|
|
32440
|
-
if (!
|
|
32743
|
+
const file = join18(path, "project.godot");
|
|
32744
|
+
if (!existsSync12(file)) {
|
|
32441
32745
|
return {
|
|
32442
32746
|
ok: false,
|
|
32443
32747
|
response: this.createErrorResponse(`Not a Godot project: ${path}`, [
|
|
@@ -32598,13 +32902,13 @@ class GodotServer {
|
|
|
32598
32902
|
let sawAScan = false;
|
|
32599
32903
|
let scanEndedAt = null;
|
|
32600
32904
|
for (;; ) {
|
|
32601
|
-
const asked = Date.now();
|
|
32602
32905
|
let status;
|
|
32603
32906
|
try {
|
|
32604
32907
|
status = asParams(await this.godotBridge.invokeTool("scan_status", {}));
|
|
32605
32908
|
} catch {
|
|
32606
32909
|
return { waitedMs: Date.now() - started, stillScanning: false };
|
|
32607
32910
|
}
|
|
32911
|
+
const answered = Date.now();
|
|
32608
32912
|
if (status["scanning"] === true || status["importing"] === true || status["pending"] === true) {
|
|
32609
32913
|
sawAScan = true;
|
|
32610
32914
|
if (Date.now() - started >= SCAN_WAIT_MS) {
|
|
@@ -32614,7 +32918,7 @@ class GodotServer {
|
|
|
32614
32918
|
continue;
|
|
32615
32919
|
}
|
|
32616
32920
|
const since = readNumber(status, "sinceScanFinishedMs");
|
|
32617
|
-
scanEndedAt = since !== undefined && since >= 0 ?
|
|
32921
|
+
scanEndedAt = since !== undefined && since >= 0 ? answered - since : sawAScan ? answered : null;
|
|
32618
32922
|
break;
|
|
32619
32923
|
}
|
|
32620
32924
|
let waitedForTheWrite = false;
|
|
@@ -33033,11 +33337,11 @@ class GodotServer {
|
|
|
33033
33337
|
return this.createErrorResponse(`Cannot create ${dirname11(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
33034
33338
|
}
|
|
33035
33339
|
const debug = readBoolean(args, "debug") ?? false;
|
|
33036
|
-
const exportLogs =
|
|
33340
|
+
const exportLogs = scratchDirectory("export");
|
|
33037
33341
|
const exportArgs = [
|
|
33038
33342
|
"--headless",
|
|
33039
33343
|
"--log-file",
|
|
33040
|
-
|
|
33344
|
+
join18(exportLogs, "engine.log"),
|
|
33041
33345
|
"--path",
|
|
33042
33346
|
project.value.path,
|
|
33043
33347
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -33069,7 +33373,7 @@ class GodotServer {
|
|
|
33069
33373
|
log.finish();
|
|
33070
33374
|
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
33071
33375
|
const verdict = {
|
|
33072
|
-
exported: ending.exitCode === 0 && log.count("error") === 0 &&
|
|
33376
|
+
exported: ending.exitCode === 0 && log.count("error") === 0 && existsSync12(output.absolutePath),
|
|
33073
33377
|
preset,
|
|
33074
33378
|
outputPath: output.relativePath,
|
|
33075
33379
|
debug,
|
|
@@ -33105,7 +33409,7 @@ class GodotServer {
|
|
|
33105
33409
|
return contained.response;
|
|
33106
33410
|
}
|
|
33107
33411
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
33108
|
-
if (!
|
|
33412
|
+
if (!existsSync12(join18(project.value.path, runner))) {
|
|
33109
33413
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
33110
33414
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
33111
33415
|
]);
|
|
@@ -33141,7 +33445,7 @@ class GodotServer {
|
|
|
33141
33445
|
];
|
|
33142
33446
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
33143
33447
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
33144
|
-
const userData =
|
|
33448
|
+
const userData = scratchDirectory("tests");
|
|
33145
33449
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
33146
33450
|
const console2 = [];
|
|
33147
33451
|
let lastSaid = Date.now();
|
|
@@ -33168,14 +33472,17 @@ class GodotServer {
|
|
|
33168
33472
|
resolve(false);
|
|
33169
33473
|
});
|
|
33170
33474
|
});
|
|
33171
|
-
|
|
33475
|
+
if (timedOut && run.process.pid !== undefined) {
|
|
33476
|
+
await untilGone([run.process.pid]);
|
|
33477
|
+
}
|
|
33478
|
+
const reportsDir = join18(project.value.path, ".godot", ...ours.split("/"));
|
|
33172
33479
|
let report = null;
|
|
33173
33480
|
let reportProblem = null;
|
|
33174
33481
|
try {
|
|
33175
|
-
const written =
|
|
33482
|
+
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
33483
|
const newest = written.at(-1);
|
|
33177
33484
|
if (newest !== undefined) {
|
|
33178
|
-
report = parseJUnit(readFileSync17(
|
|
33485
|
+
report = parseJUnit(readFileSync17(join18(reportsDir, newest, "results.xml"), "utf8"));
|
|
33179
33486
|
}
|
|
33180
33487
|
} catch (error) {
|
|
33181
33488
|
reportProblem = errorMessage(error);
|
|
@@ -33333,7 +33640,7 @@ class GodotServer {
|
|
|
33333
33640
|
const cached = cachedClasses(projectPath);
|
|
33334
33641
|
const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
|
|
33335
33642
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
33336
|
-
if (!contained.ok || !
|
|
33643
|
+
if (!contained.ok || !existsSync12(contained.absolutePath)) {
|
|
33337
33644
|
return null;
|
|
33338
33645
|
}
|
|
33339
33646
|
try {
|
|
@@ -33387,7 +33694,7 @@ class GodotServer {
|
|
|
33387
33694
|
}
|
|
33388
33695
|
sourceOfClass(projectPath, resourcePath) {
|
|
33389
33696
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
33390
|
-
if (!contained.ok || !
|
|
33697
|
+
if (!contained.ok || !existsSync12(contained.absolutePath)) {
|
|
33391
33698
|
return null;
|
|
33392
33699
|
}
|
|
33393
33700
|
try {
|
|
@@ -33408,28 +33715,32 @@ class GodotServer {
|
|
|
33408
33715
|
async handleDAP(toolName, args) {
|
|
33409
33716
|
return handleDAPTool(this.dap(), toolName, args);
|
|
33410
33717
|
}
|
|
33411
|
-
adapterBelongsElsewhere() {
|
|
33718
|
+
async adapterBelongsElsewhere() {
|
|
33412
33719
|
const status = this.godotBridge.getStatus();
|
|
33413
33720
|
const ours = status.editorPid;
|
|
33414
33721
|
if (ours === undefined) {
|
|
33415
33722
|
return null;
|
|
33416
33723
|
}
|
|
33724
|
+
const connection = this.dapClient?.connection() ?? null;
|
|
33725
|
+
if (connection !== null && this.adapterPassed?.connection === connection && this.adapterPassed.editorPid === ours) {
|
|
33726
|
+
return null;
|
|
33727
|
+
}
|
|
33417
33728
|
const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
|
|
33418
|
-
const
|
|
33729
|
+
const askedAt = Date.now();
|
|
33730
|
+
const holder = await listeningPid(port);
|
|
33731
|
+
this.logDebug(`Asked who holds debug adapter port ${port} in ${Date.now() - askedAt}ms: ${holder ?? "not said"}`);
|
|
33419
33732
|
if (holder === null || holder === ours) {
|
|
33733
|
+
this.adapterPassed = connection === null ? null : { connection, editorPid: ours };
|
|
33420
33734
|
return null;
|
|
33421
33735
|
}
|
|
33422
33736
|
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
33737
|
}
|
|
33424
|
-
|
|
33425
|
-
return anEditorIsStillComing(listeningSince, this.launchedEditorIsUp());
|
|
33426
|
-
}
|
|
33427
|
-
launchedEditorIsUp() {
|
|
33738
|
+
async launchedEditorIsUp() {
|
|
33428
33739
|
const launched = this.launchedEditor;
|
|
33429
33740
|
if (launched === null || !alive(launched.pid)) {
|
|
33430
33741
|
return false;
|
|
33431
33742
|
}
|
|
33432
|
-
const now = startTimesOf([launched.pid]).get(launched.pid);
|
|
33743
|
+
const now = (await startTimesOf([launched.pid])).get(launched.pid);
|
|
33433
33744
|
return launched.startedAt === null || now === undefined || now === launched.startedAt;
|
|
33434
33745
|
}
|
|
33435
33746
|
dap() {
|
|
@@ -33518,8 +33829,9 @@ class GodotServer {
|
|
|
33518
33829
|
}
|
|
33519
33830
|
return owed;
|
|
33520
33831
|
}
|
|
33521
|
-
getEditorStatusPayload() {
|
|
33832
|
+
async getEditorStatusPayload() {
|
|
33522
33833
|
const status = this.godotBridge.getStatus();
|
|
33834
|
+
const launchedIsUp = !status.connected && await this.launchedEditorIsUp();
|
|
33523
33835
|
const isPortConflict = this.bridgeStartupError?.includes("EADDRINUSE") ?? false;
|
|
33524
33836
|
const stale = status.connected && editorIsStale(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest());
|
|
33525
33837
|
const unfinished = this.restartLeftUnfinished(status.connected);
|
|
@@ -33528,14 +33840,14 @@ class GodotServer {
|
|
|
33528
33840
|
serverVersion: SERVER_VERSION,
|
|
33529
33841
|
addonIsStale: status.connected ? stale : undefined,
|
|
33530
33842
|
bridgeAvailable: this.bridgeStartupError === null,
|
|
33531
|
-
mayYetConnect: status.connected ? undefined : unfinished !== null ? false :
|
|
33843
|
+
mayYetConnect: status.connected ? undefined : unfinished !== null ? false : anEditorIsStillComing(status.listeningSince, launchedIsUp),
|
|
33532
33844
|
restartInterrupted: unfinished === null ? undefined : {
|
|
33533
33845
|
quitEditorPid: unfinished.editorPid,
|
|
33534
33846
|
quitAt: unfinished.quitAt,
|
|
33535
33847
|
byServerPid: unfinished.byPid,
|
|
33536
33848
|
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
33849
|
},
|
|
33538
|
-
awaitingLaunchedEditor: status.connected || !
|
|
33850
|
+
awaitingLaunchedEditor: status.connected || !launchedIsUp ? undefined : this.launchedEditor?.pid ?? undefined,
|
|
33539
33851
|
...status.connected ? this.whatTheLaunchedEditorDropped() : {},
|
|
33540
33852
|
startupError: this.bridgeStartupError,
|
|
33541
33853
|
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 +33936,7 @@ class GodotServer {
|
|
|
33624
33936
|
const asked = (async () => {
|
|
33625
33937
|
const playing = await this.editorPlayingState();
|
|
33626
33938
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
33627
|
-
const current = this.currentRun();
|
|
33939
|
+
const current = await this.currentRun();
|
|
33628
33940
|
const active = current === null ? false : await this.runStillGoing(current, playing?.playing ?? null);
|
|
33629
33941
|
return {
|
|
33630
33942
|
playing,
|
|
@@ -33632,14 +33944,15 @@ class GodotServer {
|
|
|
33632
33944
|
hold: current === null ? {} : await this.holdOf(current, active)
|
|
33633
33945
|
};
|
|
33634
33946
|
})();
|
|
33635
|
-
const [games, { playing, active, hold }, scan] = await Promise.all([
|
|
33947
|
+
const [games, { playing, active, hold }, scan, editorStatus] = await Promise.all([
|
|
33636
33948
|
pinged,
|
|
33637
33949
|
asked,
|
|
33638
|
-
this.editorScanState()
|
|
33950
|
+
this.editorScanState(),
|
|
33951
|
+
this.getEditorStatusPayload()
|
|
33639
33952
|
]);
|
|
33640
33953
|
return this.jsonTextResponse({
|
|
33641
33954
|
editor: {
|
|
33642
|
-
...
|
|
33955
|
+
...editorStatus,
|
|
33643
33956
|
debugPort: playing?.debugPort ?? this.godotBridge.getStatus().debugPort,
|
|
33644
33957
|
scan
|
|
33645
33958
|
},
|
|
@@ -33669,7 +33982,7 @@ class GodotServer {
|
|
|
33669
33982
|
return null;
|
|
33670
33983
|
}
|
|
33671
33984
|
try {
|
|
33672
|
-
return settingKeys(readFileSync17(
|
|
33985
|
+
return settingKeys(readFileSync17(join18(projectPath, "project.godot"), "utf8"));
|
|
33673
33986
|
} catch {
|
|
33674
33987
|
return null;
|
|
33675
33988
|
}
|
|
@@ -33818,7 +34131,7 @@ class GodotServer {
|
|
|
33818
34131
|
writeEditorLogNote(projectPath, launched.pid);
|
|
33819
34132
|
this.launchedEditor = {
|
|
33820
34133
|
pid: launched.pid,
|
|
33821
|
-
startedAt: startTimesOf([launched.pid]).get(launched.pid) ?? null
|
|
34134
|
+
startedAt: (await startTimesOf([launched.pid])).get(launched.pid) ?? null
|
|
33822
34135
|
};
|
|
33823
34136
|
return { pid: launched.pid, error: null };
|
|
33824
34137
|
}
|
|
@@ -33866,13 +34179,15 @@ class GodotServer {
|
|
|
33866
34179
|
}
|
|
33867
34180
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
33868
34181
|
let ended = null;
|
|
33869
|
-
const before = this.currentRun();
|
|
34182
|
+
const before = await this.currentRun();
|
|
33870
34183
|
if (before !== null && await this.runStillGoing(before)) {
|
|
33871
34184
|
this.logDebug("Ending the running game before starting another");
|
|
34185
|
+
const endingFrom = Date.now();
|
|
33872
34186
|
ended = {
|
|
33873
34187
|
pid: before.pid ?? this.announcedPidOf(before) ?? null,
|
|
33874
34188
|
ending: await this.endActiveGame("editor_run start, which ends the run that was going before it starts another", true)
|
|
33875
34189
|
};
|
|
34190
|
+
this.logDebug(`The running game was ${ended.ending} after ${Date.now() - endingFrom}ms`);
|
|
33876
34191
|
}
|
|
33877
34192
|
const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
|
|
33878
34193
|
const wereHereFirst = this.everyRuntimeOfOurs();
|
|
@@ -33882,14 +34197,16 @@ class GodotServer {
|
|
|
33882
34197
|
});
|
|
33883
34198
|
const runtimeWaitMs = this.announceBudget(args, project.value.path);
|
|
33884
34199
|
const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
|
|
33885
|
-
|
|
34200
|
+
const silent = resolveSilent(args["silent"], process.env) && !headless;
|
|
34201
|
+
if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && !needsItsOwnProcess(asked.value) && !silent) {
|
|
33886
34202
|
return await this.playThroughEditor(sceneArgument, refreshed.value, scanned, project.value.path, alreadyPlaying, runtimeWaitMs, ended);
|
|
33887
34203
|
}
|
|
33888
34204
|
const cmdArgs = runArguments({
|
|
33889
34205
|
projectPath: project.value.path,
|
|
33890
34206
|
headless,
|
|
33891
34207
|
scene: sceneArgument,
|
|
33892
|
-
userArgs: given.value
|
|
34208
|
+
userArgs: given.value,
|
|
34209
|
+
silent
|
|
33893
34210
|
});
|
|
33894
34211
|
this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
33895
34212
|
const started = await this.launchKeptGame(engine.value, cmdArgs, project.value.path, needsItsOwnProcess(asked.value) ? environmentFor({ ...asked.value, runtimeDirectory: runtimeDirectory() }) : undefined, headless || args["visible"] === true ? undefined : HIDDEN_DESKTOP);
|
|
@@ -33900,7 +34217,7 @@ class GodotServer {
|
|
|
33900
34217
|
}
|
|
33901
34218
|
started.announcedBefore = alreadyPlaying;
|
|
33902
34219
|
this.activeProcess = started;
|
|
33903
|
-
const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && given.value.length > 0 ? " The editor cannot be handed arguments for a game it plays, so this one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && needsItsOwnProcess(asked.value) ? " The editor plays a game with its own environment, so a run given one was started " + "here: the debug_* tools answer only for a game the editor is playing." : "";
|
|
34220
|
+
const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && given.value.length > 0 ? " The editor cannot be handed arguments for a game it plays, so this one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && needsItsOwnProcess(asked.value) ? " The editor plays a game with its own environment, so a run given one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && silent ? " The editor cannot play a game silent, which takes an engine flag, so this one was " + `started here${args["silent"] === true ? "" : ", silent because GDHARNESS_SILENT says so"}: the debug_* tools answer only for a game the editor is playing, and silent: false plays it through the editor.` : "";
|
|
33904
34221
|
const endedForThis = endedToStartThis(ended);
|
|
33905
34222
|
const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
|
|
33906
34223
|
const alsoRunning = leftRunningNote(stranded);
|
|
@@ -33922,7 +34239,7 @@ class GodotServer {
|
|
|
33922
34239
|
}
|
|
33923
34240
|
async runtimeUp(projectPath, before, budget, withArgs) {
|
|
33924
34241
|
const budgetMs = budget.ms;
|
|
33925
|
-
if (!
|
|
34242
|
+
if (!existsSync12(join18(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
33926
34243
|
return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
|
|
33927
34244
|
}
|
|
33928
34245
|
let asking = null;
|
|
@@ -33933,12 +34250,12 @@ class GodotServer {
|
|
|
33933
34250
|
const lastSaid = () => editorSaysGoing ? "playing" : "not playing";
|
|
33934
34251
|
const endpoint = await announcedSince(projectPath, before, {
|
|
33935
34252
|
budgetMs,
|
|
33936
|
-
accept: (one) => this.isTheGameOf(this.
|
|
34253
|
+
accept: (one) => this.isTheGameOf(this.activeProcess, one, projectPath),
|
|
33937
34254
|
giveUp: () => {
|
|
33938
34255
|
if (this.dapClient?.isStopped() === true) {
|
|
33939
34256
|
return true;
|
|
33940
34257
|
}
|
|
33941
|
-
const run = this.
|
|
34258
|
+
const run = this.activeProcess;
|
|
33942
34259
|
if (run === null || !run.throughEditor) {
|
|
33943
34260
|
return !stillRunning(run);
|
|
33944
34261
|
}
|
|
@@ -33956,7 +34273,7 @@ class GodotServer {
|
|
|
33956
34273
|
});
|
|
33957
34274
|
const asked = editorAsks === 0 ? "editor not asked" : `editor asked ${editorAsks === 1 ? "once" : `${editorAsks} times`}, last said ${lastSaid()}`;
|
|
33958
34275
|
this.logDebug(`announce wait ended after ${Date.now() - waitBegan}ms: ${endpoint === null ? "nothing announced" : `pid ${endpoint.pid} announced`}, ${asked}`);
|
|
33959
|
-
const going = this.currentRun();
|
|
34276
|
+
const going = await this.currentRun();
|
|
33960
34277
|
if (endpoint !== null && going !== null && going.announcedPid === undefined) {
|
|
33961
34278
|
this.tie(going, endpoint);
|
|
33962
34279
|
}
|
|
@@ -33997,8 +34314,10 @@ class GodotServer {
|
|
|
33997
34314
|
}
|
|
33998
34315
|
async playThroughEditor(scene, refreshedClasses, scanned, projectPath, alreadyPlaying, runtimeWaitMs, ended) {
|
|
33999
34316
|
const log = new GameLog;
|
|
34317
|
+
const connectingFrom = Date.now();
|
|
34000
34318
|
try {
|
|
34001
34319
|
await this.dap().connect();
|
|
34320
|
+
this.logDebug(`The debug adapter was connected after ${Date.now() - connectingFrom}ms`);
|
|
34002
34321
|
this.dapClient?.getOutput(true);
|
|
34003
34322
|
} catch (error) {
|
|
34004
34323
|
return this.createErrorResponse(`The editor is connected but its debug adapter is not: ${errorMessage(error)}`, [
|
|
@@ -34006,7 +34325,7 @@ class GodotServer {
|
|
|
34006
34325
|
"GDHARNESS_DAP_PORT points this server at another one"
|
|
34007
34326
|
]);
|
|
34008
34327
|
}
|
|
34009
|
-
const stranger = this.adapterBelongsElsewhere();
|
|
34328
|
+
const stranger = await this.adapterBelongsElsewhere();
|
|
34010
34329
|
if (stranger !== null) {
|
|
34011
34330
|
return this.createErrorResponse(stranger, [
|
|
34012
34331
|
"editor_status names the editor on the bridge and the port it says it serves",
|
|
@@ -34014,7 +34333,9 @@ class GodotServer {
|
|
|
34014
34333
|
"Closing the other editor, or moving its debug adapter port in its editor settings, frees this one"
|
|
34015
34334
|
]);
|
|
34016
34335
|
}
|
|
34336
|
+
const sendingFrom = Date.now();
|
|
34017
34337
|
const breakpoints = await this.dapHoldingBreakpointsOf(projectPath).reapplyBreakpoints();
|
|
34338
|
+
this.logDebug(`Breakpoints were sent again after ${Date.now() - sendingFrom}ms`);
|
|
34018
34339
|
const answer = await this.handleViaBridge("play_scene", scene === null ? {} : { scenePath: `res://${scene}` });
|
|
34019
34340
|
if (answer.isError === true) {
|
|
34020
34341
|
return answer;
|
|
@@ -34200,13 +34521,13 @@ class GodotServer {
|
|
|
34200
34521
|
arguments: [],
|
|
34201
34522
|
...running.command === undefined ? {} : { command: running.command }
|
|
34202
34523
|
};
|
|
34203
|
-
if (!stillTheRecordedRun(startedAs)) {
|
|
34524
|
+
if (!await stillTheRecordedRun(startedAs)) {
|
|
34204
34525
|
clearTheRecord();
|
|
34205
34526
|
running.endedHere = null;
|
|
34206
34527
|
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
34528
|
return "refused";
|
|
34208
34529
|
}
|
|
34209
|
-
const signalled = runningAs(running.pid);
|
|
34530
|
+
const signalled = await runningAs(running.pid);
|
|
34210
34531
|
const described = signalled === null ? "nothing it would name" : `${signalled.kind}: ${signalled.text}`;
|
|
34211
34532
|
let landed = true;
|
|
34212
34533
|
try {
|
|
@@ -34227,7 +34548,7 @@ class GodotServer {
|
|
|
34227
34548
|
return gone ? "gone" : "lingering";
|
|
34228
34549
|
}
|
|
34229
34550
|
spawnGame(godotPath, cmdArgs, env) {
|
|
34230
|
-
const child =
|
|
34551
|
+
const child = spawn3(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
|
|
34231
34552
|
const cancelled = callSignal();
|
|
34232
34553
|
if (cancelled !== undefined) {
|
|
34233
34554
|
const end = () => {
|
|
@@ -34394,8 +34715,8 @@ class GodotServer {
|
|
|
34394
34715
|
}
|
|
34395
34716
|
return run.announcedPid;
|
|
34396
34717
|
}
|
|
34397
|
-
ownGameAmong(running, projectPath) {
|
|
34398
|
-
const run = this.currentRun();
|
|
34718
|
+
async ownGameAmong(running, projectPath) {
|
|
34719
|
+
const run = await this.currentRun();
|
|
34399
34720
|
if (run === null || !stillRunning(run)) {
|
|
34400
34721
|
return;
|
|
34401
34722
|
}
|
|
@@ -34406,7 +34727,7 @@ class GodotServer {
|
|
|
34406
34727
|
return its !== undefined && running.some((one) => one.pid === its) ? its : undefined;
|
|
34407
34728
|
}
|
|
34408
34729
|
async ownGameThroughTheTree(running, projectPath) {
|
|
34409
|
-
const run = this.currentRun();
|
|
34730
|
+
const run = await this.currentRun();
|
|
34410
34731
|
if (run === null || !stillRunning(run) || run.announcedPid !== undefined) {
|
|
34411
34732
|
return;
|
|
34412
34733
|
}
|
|
@@ -34492,7 +34813,7 @@ class GodotServer {
|
|
|
34492
34813
|
const editor = this.godotBridge.getStatus().editorPid;
|
|
34493
34814
|
return endpoint.editorPid === undefined || endpoint.editorPid !== editor;
|
|
34494
34815
|
}
|
|
34495
|
-
adoptRecordedRun() {
|
|
34816
|
+
async adoptRecordedRun() {
|
|
34496
34817
|
const project = this.ourProject();
|
|
34497
34818
|
const record = project === null ? null : readRunRecord(project);
|
|
34498
34819
|
if (record === null || !this.couldBeOurs(record.projectPath)) {
|
|
@@ -34513,7 +34834,7 @@ class GodotServer {
|
|
|
34513
34834
|
pickedUp: true
|
|
34514
34835
|
};
|
|
34515
34836
|
this.drainTranscript(adopted);
|
|
34516
|
-
if (!alive(record.pid) || !couldStillBeTheRecordedRun(record)) {
|
|
34837
|
+
if (!alive(record.pid) || !await couldStillBeTheRecordedRun(record)) {
|
|
34517
34838
|
adopted.log.finish();
|
|
34518
34839
|
adopted.exitCode = record.exitCode ?? null;
|
|
34519
34840
|
adopted.exitSignal = record.exitSignal ?? null;
|
|
@@ -34594,8 +34915,8 @@ class GodotServer {
|
|
|
34594
34915
|
const status = this.godotBridge.getStatus();
|
|
34595
34916
|
return this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
|
|
34596
34917
|
}
|
|
34597
|
-
currentRun() {
|
|
34598
|
-
return this.activeProcess ?? this.adoptRecordedRun();
|
|
34918
|
+
async currentRun() {
|
|
34919
|
+
return this.activeProcess ?? await this.adoptRecordedRun();
|
|
34599
34920
|
}
|
|
34600
34921
|
async checkBoot(godotPath, projectPath, scene, args, userArgs, scanned, asked = {}) {
|
|
34601
34922
|
const frames = readPositiveNumber(args, "frames") ?? 3;
|
|
@@ -34634,10 +34955,14 @@ class GodotServer {
|
|
|
34634
34955
|
}
|
|
34635
34956
|
async handleWaitForRun(args) {
|
|
34636
34957
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
34637
|
-
const run = this.currentRun();
|
|
34958
|
+
const run = await this.currentRun();
|
|
34638
34959
|
if (!run) {
|
|
34639
34960
|
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
34640
34961
|
}
|
|
34962
|
+
const elsewhere = this.anotherProjectsRun(run.projectPath, args, "waited on");
|
|
34963
|
+
if (elsewhere !== null) {
|
|
34964
|
+
return elsewhere;
|
|
34965
|
+
}
|
|
34641
34966
|
const budgetMs = readPositiveNumber(args, "timeoutMs") ?? WAIT_FOR_RUN_MS;
|
|
34642
34967
|
const until = Date.now() + budgetMs;
|
|
34643
34968
|
const cancelled = callSignal();
|
|
@@ -34684,7 +35009,7 @@ class GodotServer {
|
|
|
34684
35009
|
await delay3(50);
|
|
34685
35010
|
}
|
|
34686
35011
|
}
|
|
34687
|
-
handleEditorConsole(args) {
|
|
35012
|
+
async handleEditorConsole(args) {
|
|
34688
35013
|
const status = this.godotBridge.getStatus();
|
|
34689
35014
|
const projectPath = status.projectPath ?? this.ownProject ?? undefined;
|
|
34690
35015
|
if (projectPath === undefined) {
|
|
@@ -34721,10 +35046,11 @@ class GodotServer {
|
|
|
34721
35046
|
const filtered = severity !== "info" || contains !== undefined;
|
|
34722
35047
|
const grouped = bursts(everything, readNonNegativeNumber(args, "before") ?? 3, filtered ? (entry) => answersTo(entry, severity, contains) : undefined);
|
|
34723
35048
|
const shown = grouped.slice(0, MOST_GROUPS);
|
|
35049
|
+
const stillComing = !status.connected && await this.launchedEditorIsUp();
|
|
34724
35050
|
return this.jsonTextResponse({
|
|
34725
35051
|
editorPid: editorPid ?? undefined,
|
|
34726
35052
|
projectPath,
|
|
34727
|
-
...status.connected ? {} :
|
|
35053
|
+
...status.connected ? {} : stillComing ? { stillConnecting: true } : { exitedBeforeConnecting: true },
|
|
34728
35054
|
capturedIn: console2.path,
|
|
34729
35055
|
...console2.of === undefined ? {} : {
|
|
34730
35056
|
readFromTheStart: console2.readBytes,
|
|
@@ -34748,7 +35074,7 @@ class GodotServer {
|
|
|
34748
35074
|
return this.handleEditorConsole(args);
|
|
34749
35075
|
}
|
|
34750
35076
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
34751
|
-
const run = this.currentRun();
|
|
35077
|
+
const run = await this.currentRun();
|
|
34752
35078
|
if (!run) {
|
|
34753
35079
|
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
34754
35080
|
}
|
|
@@ -34787,7 +35113,7 @@ class GodotServer {
|
|
|
34787
35113
|
notes.push(endedWithoutACode({
|
|
34788
35114
|
throughEditor: run.throughEditor,
|
|
34789
35115
|
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
34790
|
-
couldAnnounce: run.projectPath !== null &&
|
|
35116
|
+
couldAnnounce: run.projectPath !== null && existsSync12(join18(run.projectPath, RUNTIME_AUTOLOAD.path)),
|
|
34791
35117
|
endedHere: typeof run.endedHere === "string",
|
|
34792
35118
|
pickedUp: run.pickedUp === true
|
|
34793
35119
|
}));
|
|
@@ -34825,16 +35151,27 @@ class GodotServer {
|
|
|
34825
35151
|
entries: forAnswer(selected.entries)
|
|
34826
35152
|
});
|
|
34827
35153
|
}
|
|
35154
|
+
anotherProjectsRun(runsProject, args, act) {
|
|
35155
|
+
const named = readNonEmptyString(args, "projectPath");
|
|
35156
|
+
if (named === undefined || runsProject === null || isSameDirectory(runsProject, named)) {
|
|
35157
|
+
return null;
|
|
35158
|
+
}
|
|
35159
|
+
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.`);
|
|
35160
|
+
}
|
|
34828
35161
|
async handleStopProject(args) {
|
|
34829
35162
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
34830
|
-
const stopped = this.currentRun();
|
|
35163
|
+
const stopped = await this.currentRun();
|
|
34831
35164
|
const named = readNumber(args, "pid");
|
|
34832
35165
|
if (named !== undefined && (stopped === null || named !== stopped.pid && named !== this.announcedPidOf(stopped))) {
|
|
34833
|
-
return await this.stopAnnouncedGame(named, readBoolean(args, "andChildren") === true);
|
|
35166
|
+
return await this.stopAnnouncedGame(named, readBoolean(args, "andChildren") === true, args);
|
|
34834
35167
|
}
|
|
34835
35168
|
if (!stopped) {
|
|
34836
35169
|
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
34837
35170
|
}
|
|
35171
|
+
const elsewhere = this.anotherProjectsRun(stopped.projectPath, args, "stopped");
|
|
35172
|
+
if (elsewhere !== null) {
|
|
35173
|
+
return elsewhere;
|
|
35174
|
+
}
|
|
34838
35175
|
this.drainEditorOutput(stopped);
|
|
34839
35176
|
this.drainTranscript(stopped);
|
|
34840
35177
|
const wasRunning = await this.runStillGoing(stopped);
|
|
@@ -34876,16 +35213,22 @@ class GodotServer {
|
|
|
34876
35213
|
entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
34877
35214
|
});
|
|
34878
35215
|
}
|
|
34879
|
-
async stopAnnouncedGame(pid, andChildren) {
|
|
35216
|
+
async stopAnnouncedGame(pid, andChildren, args) {
|
|
34880
35217
|
const announced = runtimesAnnounced();
|
|
34881
35218
|
const game = [
|
|
34882
35219
|
...this.allAnnouncedForOurProject(announced.running),
|
|
34883
35220
|
...this.allAnnouncedForOurProject(announced.unspoken)
|
|
34884
35221
|
].find((one) => one.pid === pid);
|
|
35222
|
+
if (game !== undefined) {
|
|
35223
|
+
const elsewhere = this.anotherProjectsRun(game.project.path, args, "signalled");
|
|
35224
|
+
if (elsewhere !== null) {
|
|
35225
|
+
return elsewhere;
|
|
35226
|
+
}
|
|
35227
|
+
}
|
|
34885
35228
|
if (game === undefined) {
|
|
34886
35229
|
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
35230
|
}
|
|
34888
|
-
const read = game.announcedAt === undefined ? null : stillTheAnnouncedGame(pid, game.project.path, game.announcedAt);
|
|
35231
|
+
const read = game.announcedAt === undefined ? null : await stillTheAnnouncedGame(pid, game.project.path, game.announcedAt);
|
|
34889
35232
|
if (read === null) {
|
|
34890
35233
|
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
35234
|
}
|
|
@@ -35083,6 +35426,8 @@ class GodotServer {
|
|
|
35083
35426
|
}
|
|
35084
35427
|
const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
|
|
35085
35428
|
const unseen = checked.unseen;
|
|
35429
|
+
const broughtIn = blind.unseen.map((one) => one.className).filter((name) => !unseen.some((still) => still.className === name));
|
|
35430
|
+
const dependents = busy || projectPath === "" || broughtIn.length === 0 ? null : await this.reloadDependents(projectPath, broughtIn);
|
|
35086
35431
|
const stillGone = lost.filter((name) => !restored.includes(name));
|
|
35087
35432
|
const notes = [];
|
|
35088
35433
|
if (busy) {
|
|
@@ -35094,13 +35439,16 @@ class GodotServer {
|
|
|
35094
35439
|
} else if (unseen.length > 0) {
|
|
35095
35440
|
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
35441
|
}
|
|
35442
|
+
if (dependents !== null && dependents.notReloaded.length > 0) {
|
|
35443
|
+
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.`);
|
|
35444
|
+
}
|
|
35097
35445
|
if (stillAtMissingPaths.length > 0) {
|
|
35098
35446
|
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
35447
|
} else if (dropped.length > 0) {
|
|
35100
35448
|
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
35449
|
}
|
|
35102
35450
|
return this.jsonTextResponse({
|
|
35103
|
-
ok: !busy && unseen.length === 0 && stillGone.length === 0 && stillAtMissingPaths.length === 0 && checked.unchecked === undefined,
|
|
35451
|
+
ok: !busy && unseen.length === 0 && stillGone.length === 0 && stillAtMissingPaths.length === 0 && (dependents === null || dependents.notReloaded.length === 0) && checked.unchecked === undefined,
|
|
35104
35452
|
stillWorking: busy,
|
|
35105
35453
|
waitedMs: Date.now() - started,
|
|
35106
35454
|
waitedForEditorScanMs: waitedForEditorMs > 0 ? waitedForEditorMs : undefined,
|
|
@@ -35108,6 +35456,9 @@ class GodotServer {
|
|
|
35108
35456
|
reloadedMethods: reloaded.methods,
|
|
35109
35457
|
reloadProblem: reloaded.problem,
|
|
35110
35458
|
unseenByEditor: unseen.length > 0 ? unseen : undefined,
|
|
35459
|
+
broughtIn: broughtIn.length > 0 ? broughtIn : undefined,
|
|
35460
|
+
dependentsReloaded: dependents !== null && dependents.reloaded.length > 0 ? dependents.reloaded : undefined,
|
|
35461
|
+
dependentsNotReloaded: dependents !== null && dependents.notReloaded.length > 0 ? dependents.notReloaded : undefined,
|
|
35111
35462
|
cacheLost: lost.length > 0 ? lost : undefined,
|
|
35112
35463
|
cacheRestored: restored.length > 0 ? restored : undefined,
|
|
35113
35464
|
cacheDropped: dropped.length > 0 ? dropped : undefined,
|
|
@@ -35116,6 +35467,33 @@ class GodotServer {
|
|
|
35116
35467
|
note: notes.length > 0 ? notes.join(" ") : undefined
|
|
35117
35468
|
});
|
|
35118
35469
|
}
|
|
35470
|
+
async reloadDependents(projectPath, classes) {
|
|
35471
|
+
const reloaded = [];
|
|
35472
|
+
const notReloaded = [];
|
|
35473
|
+
for (const { script: scriptPath, tool } of scriptsNaming(projectPath, classes)) {
|
|
35474
|
+
if (tool) {
|
|
35475
|
+
notReloaded.push({
|
|
35476
|
+
scriptPath,
|
|
35477
|
+
problem: "a @tool script, which runs inside the editor, so it is not reloaded from here; editor_launch restart recompiles it"
|
|
35478
|
+
});
|
|
35479
|
+
continue;
|
|
35480
|
+
}
|
|
35481
|
+
try {
|
|
35482
|
+
const answer = asParams(await this.godotBridge.invokeTool("reload_script", { scriptPath }));
|
|
35483
|
+
if (readArray(answer, "methods") === undefined) {
|
|
35484
|
+
notReloaded.push({
|
|
35485
|
+
scriptPath,
|
|
35486
|
+
problem: "the editor would not say what it has after reloading it"
|
|
35487
|
+
});
|
|
35488
|
+
} else {
|
|
35489
|
+
reloaded.push(scriptPath);
|
|
35490
|
+
}
|
|
35491
|
+
} catch (error) {
|
|
35492
|
+
notReloaded.push({ scriptPath, problem: errorMessage(error) });
|
|
35493
|
+
}
|
|
35494
|
+
}
|
|
35495
|
+
return { reloaded, notReloaded };
|
|
35496
|
+
}
|
|
35119
35497
|
async rebuildCacheAfterLoss(projectPath, lost) {
|
|
35120
35498
|
if (projectPath === "") {
|
|
35121
35499
|
return [];
|
|
@@ -35298,9 +35676,9 @@ class GodotServer {
|
|
|
35298
35676
|
return { heldAt: undefined, heldUnknown: true, heldNote: reply.message };
|
|
35299
35677
|
}
|
|
35300
35678
|
async ownRunOnItsWay(projectPath, budgetMs) {
|
|
35301
|
-
const run = this.currentRun();
|
|
35679
|
+
const run = await this.currentRun();
|
|
35302
35680
|
const project = projectPath ?? run?.projectPath ?? null;
|
|
35303
|
-
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !
|
|
35681
|
+
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
35682
|
return null;
|
|
35305
35683
|
}
|
|
35306
35684
|
const already = runtimesAnnounced().running;
|
|
@@ -35326,8 +35704,8 @@ class GodotServer {
|
|
|
35326
35704
|
}
|
|
35327
35705
|
const endpoint = await announcedSince(project, new Set(already.map((one) => one.pid)), {
|
|
35328
35706
|
budgetMs: Math.min(budgetMs, remaining),
|
|
35329
|
-
accept: (one) => this.isTheGameOf(this.
|
|
35330
|
-
giveUp: () => this.
|
|
35707
|
+
accept: (one) => this.isTheGameOf(this.activeProcess, one, project),
|
|
35708
|
+
giveUp: () => this.activeProcess !== run || this.dapClient?.isStopped() === true || !stillRunning(run)
|
|
35331
35709
|
});
|
|
35332
35710
|
if (endpoint !== null) {
|
|
35333
35711
|
const tiedMeanwhile = () => run.announcedPid !== undefined;
|
|
@@ -35336,7 +35714,7 @@ class GodotServer {
|
|
|
35336
35714
|
}
|
|
35337
35715
|
return null;
|
|
35338
35716
|
}
|
|
35339
|
-
if (this.currentRun() !== run || !await this.runStillGoing(run)) {
|
|
35717
|
+
if (await this.currentRun() !== run || !await this.runStillGoing(run)) {
|
|
35340
35718
|
return null;
|
|
35341
35719
|
}
|
|
35342
35720
|
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 +35730,7 @@ class GodotServer {
|
|
|
35352
35730
|
}
|
|
35353
35731
|
const announced = runtimesAnnounced();
|
|
35354
35732
|
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);
|
|
35733
|
+
const own = typeof pid === "number" ? undefined : await this.ownGameAmong(announced.running, wanted) ?? await this.ownGameThroughTheTree(announced.running, wanted);
|
|
35356
35734
|
const choice = chooseRuntime(announced.running, wanted, announced.unspoken, typeof pid === "number" ? pid : own);
|
|
35357
35735
|
if ("problem" in choice) {
|
|
35358
35736
|
return this.createErrorResponse(choice.problem);
|
|
@@ -35379,8 +35757,8 @@ class GodotServer {
|
|
|
35379
35757
|
return this.createErrorResponse(refusal);
|
|
35380
35758
|
}
|
|
35381
35759
|
}
|
|
35382
|
-
const screenshotDir = expectsScreenshot ?
|
|
35383
|
-
const screenshotPath = screenshotDir ?
|
|
35760
|
+
const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
|
|
35761
|
+
const screenshotPath = screenshotDir ? join18(screenshotDir, "capture.png") : null;
|
|
35384
35762
|
try {
|
|
35385
35763
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs, !expectsScreenshot);
|
|
35386
35764
|
if (!reply.ok && reply.requestId !== undefined) {
|