gdharness 1.1.13 → 1.1.14

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