gdharness 1.0.28 → 1.0.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js CHANGED
@@ -16309,7 +16309,7 @@ var init_tool_definitions = __esm(() => {
16309
16309
  },
16310
16310
  {
16311
16311
  name: "editor_run",
16312
- 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. 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.",
16312
+ 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.",
16313
16313
  parameters: {
16314
16314
  projectPath: { ...PROJECT_PATH, ops: ["start", "check"] },
16315
16315
  scene: {
@@ -16426,7 +16426,7 @@ var init_tool_definitions = __esm(() => {
16426
16426
  },
16427
16427
  {
16428
16428
  name: "editor_status",
16429
- description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait.",
16429
+ description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait.",
16430
16430
  parameters: {},
16431
16431
  requires: []
16432
16432
  },
@@ -37730,6 +37730,49 @@ var init_lsp_client = __esm(() => {
37730
37730
  init_ports();
37731
37731
  });
37732
37732
 
37733
+ // src/outside.ts
37734
+ import { spawn } from "node:child_process";
37735
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
37736
+ function encodeSpec(spec) {
37737
+ return Buffer.from(JSON.stringify(spec), "utf8").toString("base64url");
37738
+ }
37739
+ function readLaunched(printed) {
37740
+ const line = printed.split(`
37741
+ `).find((one) => one.trim() !== "")?.trim() ?? "";
37742
+ const pid = /^pid (\d+)$/.exec(line);
37743
+ if (pid?.[1] !== undefined) {
37744
+ return { pid: Number(pid[1]) };
37745
+ }
37746
+ return { error: line.replace(/^error /, "") || "the launcher said nothing" };
37747
+ }
37748
+ async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = KEEPER_SCRIPT) {
37749
+ return await new Promise((resolve) => {
37750
+ const launcher = spawn(runtime, [keeper, "launch", encodeSpec(spec)], {
37751
+ stdio: ["ignore", "pipe", "pipe"],
37752
+ windowsHide: true
37753
+ });
37754
+ let printed = "";
37755
+ let complained = "";
37756
+ launcher.stdout.on("data", (chunk) => {
37757
+ printed += chunk.toString();
37758
+ });
37759
+ launcher.stderr.on("data", (chunk) => {
37760
+ complained += chunk.toString();
37761
+ });
37762
+ launcher.once("error", (error) => {
37763
+ resolve({ error: `the launcher did not start: ${error.message}` });
37764
+ });
37765
+ launcher.once("close", () => {
37766
+ const launched = readLaunched(printed);
37767
+ resolve("error" in launched && complained.trim() !== "" ? { error: `${launched.error}: ${complained.trim()}` } : launched);
37768
+ });
37769
+ });
37770
+ }
37771
+ var KEEPER_SCRIPT;
37772
+ var init_outside = __esm(() => {
37773
+ KEEPER_SCRIPT = fileURLToPath5(new URL("./keeper.js", import.meta.url));
37774
+ });
37775
+
37733
37776
  // src/process-time.ts
37734
37777
  import { execFile as execFile6 } from "node:child_process";
37735
37778
  import { promisify as promisify6 } from "node:util";
@@ -37949,9 +37992,6 @@ import { basename as basename3, join as join22 } from "node:path";
37949
37992
  function runsDirectory() {
37950
37993
  return join22(runtimeDirectory(), "runs");
37951
37994
  }
37952
- function recordPath() {
37953
- return join22(runsDirectory(), "run.json");
37954
- }
37955
37995
  function recordPaths() {
37956
37996
  return runtimeDirectories().map((directory) => join22(directory, "runs", "run.json"));
37957
37997
  }
@@ -37960,10 +38000,6 @@ function openTranscript(startedAt) {
37960
38000
  const path = join22(runsDirectory(), `run-${startedAt}.log`);
37961
38001
  return { path, fd: openSync2(path, "a") };
37962
38002
  }
37963
- function writeRunRecord(record) {
37964
- mkdirSync13(runsDirectory(), { recursive: true });
37965
- writeFileSync15(recordPath(), JSON.stringify(record, null, 2), "utf8");
37966
- }
37967
38003
  function editorNotePath() {
37968
38004
  return join22(runsDirectory(), "editor-run.json");
37969
38005
  }
@@ -37999,21 +38035,6 @@ function editorNoteAt(path) {
37999
38035
  }
38000
38036
  return { projectPath, transcript, startedAt };
38001
38037
  }
38002
- function recordRunEnded(pid, ending) {
38003
- const path = recordPath();
38004
- const record = recordAt(path);
38005
- if (record === null || record.pid !== pid) {
38006
- return;
38007
- }
38008
- const written = {
38009
- ...record,
38010
- ...ending.exitCode === null ? {} : { exitCode: ending.exitCode },
38011
- ...ending.exitSignal === null ? {} : { exitSignal: ending.exitSignal }
38012
- };
38013
- try {
38014
- writeFileSync15(path, JSON.stringify(written, null, 2), "utf8");
38015
- } catch {}
38016
- }
38017
38038
  function readRunRecord() {
38018
38039
  for (const path of recordPaths()) {
38019
38040
  const record = recordAt(path);
@@ -38240,7 +38261,7 @@ var init_run_record = __esm(() => {
38240
38261
  });
38241
38262
 
38242
38263
  // src/server.ts
38243
- import { execFile as execFile7, spawn } from "node:child_process";
38264
+ import { execFile as execFile7, spawn as spawn2 } from "node:child_process";
38244
38265
  import { randomUUID as randomUUID2 } from "node:crypto";
38245
38266
  import {
38246
38267
  appendFileSync,
@@ -38261,7 +38282,7 @@ import {
38261
38282
  import { tmpdir as tmpdir5 } from "node:os";
38262
38283
  import { basename as basename4, dirname as dirname16, extname as extname2, isAbsolute as isAbsolute3, join as join23, normalize as normalize3, relative as relative3 } from "node:path";
38263
38284
  import { setTimeout as delay3 } from "node:timers/promises";
38264
- import { fileURLToPath as fileURLToPath5 } from "node:url";
38285
+ import { fileURLToPath as fileURLToPath6 } from "node:url";
38265
38286
  import { promisify as promisify7 } from "node:util";
38266
38287
  function captureDestinationRefusal(outputPath, projects) {
38267
38288
  if (/^(res|user):\/\//.test(outputPath)) {
@@ -38330,7 +38351,7 @@ function stillRunning(run) {
38330
38351
  if (typeof run.endedHere === "string") {
38331
38352
  return false;
38332
38353
  }
38333
- if (!run.throughEditor && run.process === null && run.pid !== null) {
38354
+ if (!run.throughEditor && run.pid !== null) {
38334
38355
  return alive(run.pid);
38335
38356
  }
38336
38357
  if (run.announcedPid !== undefined && !alive(run.announcedPid)) {
@@ -38354,10 +38375,10 @@ function runIsUp(run, editorSays, now = Date.now()) {
38354
38375
  return editorSays ?? stillRunning(run);
38355
38376
  }
38356
38377
  function noCodeWillCome(run) {
38357
- return !exited(run) && run.process === null;
38378
+ return !exited(run);
38358
38379
  }
38359
- async function untilGone(handle, pids, withinMs = STOP_WAIT_MS) {
38360
- const gone = () => (handle?.exitCode !== null || handle.signalCode !== null) && pids.every((pid) => !alive(pid));
38380
+ async function untilGone(pids, withinMs = STOP_WAIT_MS) {
38381
+ const gone = () => pids.every((pid) => !alive(pid));
38361
38382
  const deadline = Date.now() + withinMs;
38362
38383
  while (!gone() && Date.now() < deadline) {
38363
38384
  await delay3(50);
@@ -38365,6 +38386,9 @@ async function untilGone(handle, pids, withinMs = STOP_WAIT_MS) {
38365
38386
  return gone();
38366
38387
  }
38367
38388
  function endedWithoutACode(run) {
38389
+ if (!run.throughEditor && !run.pickedUp) {
38390
+ return "This run was started here, and the keeper holding it wrote no exit code into its note: the keeper was ended before the game, or another run replaced the note. Everything it printed is below, read back from its transcript.";
38391
+ }
38368
38392
  if (!run.throughEditor) {
38369
38393
  return run.endedHere ? "This run was started by another server, so nothing here held it and its exit code was never collected. Everything it printed is below, read back from its transcript." : "This run was started by another server, the one this server replaced or one running beside it, and it ended with nothing here waiting on it, so its exit code was never collected. Everything it printed is below, read back from its transcript.";
38370
38394
  }
@@ -38557,6 +38581,33 @@ function outsideTheirRanges(spec, args) {
38557
38581
  }
38558
38582
  return complaints;
38559
38583
  }
38584
+ function scanWaitIn(payload) {
38585
+ const waited = readNumber(payload, "waitedForEditorScanMs");
38586
+ const note = readString(payload, "scanNote");
38587
+ return {
38588
+ ...waited === undefined ? {} : { waitedForEditorScanMs: waited },
38589
+ ...note === undefined ? {} : { scanNote: note }
38590
+ };
38591
+ }
38592
+ function withScanWait(outcome, scanned) {
38593
+ if (scanned.waitedMs === 0) {
38594
+ return outcome;
38595
+ }
38596
+ const said = scanWaitAnswer(scanned);
38597
+ if (outcome.ok) {
38598
+ return { ...outcome, payload: { ...outcome.payload, ...said } };
38599
+ }
38600
+ return said.scanNote === undefined ? outcome : { ...outcome, message: `${outcome.message} ${said.scanNote}` };
38601
+ }
38602
+ function scanWaitAnswer(scanned) {
38603
+ if (scanned.waitedMs === 0) {
38604
+ return {};
38605
+ }
38606
+ return {
38607
+ waitedForEditorScanMs: scanned.waitedMs,
38608
+ scanNote: scanned.stillScanning ? `The editor was still scanning the project after ${SCAN_WAIT_MS / 1000} seconds, so this ` + "game started during the scan and may have read a class cache being rewritten. If " + 'editor_output shows "Could not find type" for a class that is declared, editor_rescan ' + "waits for the scan to finish, and a start after it reads the finished cache." : undefined
38609
+ };
38610
+ }
38560
38611
  function escapingPropertyValue(properties) {
38561
38612
  if (typeof properties !== "object" || properties === null) {
38562
38613
  return;
@@ -39491,6 +39542,47 @@ class GodotServer {
39491
39542
  }
39492
39543
  return { ok: true, value: stale };
39493
39544
  }
39545
+ async untilTheEditorsScanIsWritten(projectPath) {
39546
+ const open = this.godotBridge.getStatus().projectPath;
39547
+ if (!this.godotBridge.isConnected() || open === undefined || !isSameDirectory(open, projectPath)) {
39548
+ return { waitedMs: 0, stillScanning: false };
39549
+ }
39550
+ const started = Date.now();
39551
+ let sawAScan = false;
39552
+ let scanEndedAt = null;
39553
+ for (;; ) {
39554
+ const asked = Date.now();
39555
+ let status;
39556
+ try {
39557
+ status = asParams(await this.godotBridge.invokeTool("scan_status", {}));
39558
+ } catch {
39559
+ return { waitedMs: Date.now() - started, stillScanning: false };
39560
+ }
39561
+ if (status["scanning"] === true || status["importing"] === true || status["pending"] === true) {
39562
+ sawAScan = true;
39563
+ if (Date.now() - started >= SCAN_WAIT_MS) {
39564
+ return { waitedMs: Date.now() - started, stillScanning: true };
39565
+ }
39566
+ await new Promise((settle) => setTimeout(settle, 100));
39567
+ continue;
39568
+ }
39569
+ const since = readNumber(status, "sinceScanFinishedMs");
39570
+ scanEndedAt = since !== undefined && since >= 0 ? asked - since : sawAScan ? asked : null;
39571
+ break;
39572
+ }
39573
+ let waitedForTheWrite = false;
39574
+ if (scanEndedAt !== null) {
39575
+ const writeDue = scanEndedAt + CACHE_WRITE_MS;
39576
+ while (Date.now() < writeDue && (cacheWrittenAt(projectPath) ?? 0) < scanEndedAt) {
39577
+ waitedForTheWrite = true;
39578
+ await new Promise((settle) => setTimeout(settle, 50));
39579
+ }
39580
+ }
39581
+ return {
39582
+ waitedMs: sawAScan || waitedForTheWrite ? Date.now() - started : 0,
39583
+ stillScanning: false
39584
+ };
39585
+ }
39494
39586
  containProjectFiles(args) {
39495
39587
  const projectPath = readString(args, "projectPath");
39496
39588
  if (!projectPath) {
@@ -39551,14 +39643,15 @@ class GodotServer {
39551
39643
  return engine.response;
39552
39644
  }
39553
39645
  const projectPath = project.value.path;
39646
+ const scanned = await this.untilTheEditorsScanIsWritten(projectPath);
39554
39647
  const before = scriptsWithoutUid(projectPath);
39555
39648
  const imported = await runImport(engine.value, projectPath);
39556
39649
  if (!imported.ok) {
39557
- return this.answer(imported);
39650
+ return this.answer(withScanWait(imported, scanned));
39558
39651
  }
39559
39652
  const after = scriptsWithoutUid(projectPath);
39560
39653
  const given = before.filter((script) => !after.includes(script));
39561
- return this.answer({
39654
+ return this.answer(withScanWait({
39562
39655
  ok: true,
39563
39656
  messages: imported.messages,
39564
39657
  payload: {
@@ -39566,7 +39659,7 @@ class GodotServer {
39566
39659
  stillWithoutUid: after,
39567
39660
  note: uidsLeftNote(after.length)
39568
39661
  }
39569
- });
39662
+ }, scanned));
39570
39663
  }
39571
39664
  async operation(operation, params, projectPath) {
39572
39665
  const engine = await this.engine();
@@ -39577,8 +39670,10 @@ class GodotServer {
39577
39670
  messages: []
39578
39671
  };
39579
39672
  }
39673
+ const scanned = await this.untilTheEditorsScanIsWritten(projectPath);
39580
39674
  this.logDebug(`Running ${operation} in ${projectPath}: ${JSON.stringify(params)}`);
39581
- return await runOperation2({ godotPath: engine.value, script: this.operationsScript, debug: GODOT_DEBUG_MODE_DEFAULT }, operation, params, projectPath);
39675
+ const outcome = await runOperation2({ godotPath: engine.value, script: this.operationsScript, debug: GODOT_DEBUG_MODE_DEFAULT }, operation, params, projectPath);
39676
+ return withScanWait(outcome, scanned);
39582
39677
  }
39583
39678
  answer(outcome) {
39584
39679
  if (!outcome.ok) {
@@ -39760,6 +39855,7 @@ class GodotServer {
39760
39855
  if (!classes.ok) {
39761
39856
  return this.answer(classes);
39762
39857
  }
39858
+ const scanned = scanWaitIn(classes.payload);
39763
39859
  const ours = `gdharness-reports/${randomUUID2()}`;
39764
39860
  const reports = `res://.godot/${ours}`;
39765
39861
  const ignored = readStringArray(args, "ignore") ?? [];
@@ -39850,7 +39946,8 @@ class GodotServer {
39850
39946
  hung,
39851
39947
  arguments: cmdArgs,
39852
39948
  entries: forAnswer(printed.slice(0, 60)),
39853
- savesNote
39949
+ savesNote,
39950
+ ...scanned
39854
39951
  }, null, 2)
39855
39952
  }
39856
39953
  ],
@@ -39885,6 +39982,7 @@ class GodotServer {
39885
39982
  orphans: orphans.total > 0 ? orphans.total : undefined,
39886
39983
  notRun: notRun > 0 ? notRun : undefined,
39887
39984
  savesNote,
39985
+ ...scanned,
39888
39986
  note: verdict.startsWith("warnings") && warnings.length === 0 ? "gdUnit4 exits 101 for orphan nodes when nothing failed, and this run printed no count of them: orphan reporting may be off in the project settings." : notRun > 0 ? `This run stopped at the first failure in each suite it failed in, so ${notRun} case${notRun === 1 ? "" : "s"} never ran and count as neither passed nor failed. Leave failFast out to run every case.` : undefined,
39889
39987
  suites: unclean.map((suite) => ({
39890
39988
  name: suite.name,
@@ -40042,7 +40140,12 @@ class GodotServer {
40042
40140
  return anEditorIsStillComing(listeningSince, this.launchedEditorIsUp());
40043
40141
  }
40044
40142
  launchedEditorIsUp() {
40045
- return this.launchedEditor?.exitCode === null && this.launchedEditor.signalCode === null;
40143
+ const launched = this.launchedEditor;
40144
+ if (launched === null || !alive(launched.pid)) {
40145
+ return false;
40146
+ }
40147
+ const now = startTimesOf([launched.pid]).get(launched.pid);
40148
+ return launched.startedAt === null || now === undefined || now === launched.startedAt;
40046
40149
  }
40047
40150
  dap() {
40048
40151
  const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
@@ -40183,6 +40286,28 @@ class GodotServer {
40183
40286
  return null;
40184
40287
  }
40185
40288
  }
40289
+ async editorScanState() {
40290
+ if (!this.godotBridge.getStatus().connected) {
40291
+ return null;
40292
+ }
40293
+ try {
40294
+ const answer = await Promise.race([
40295
+ this.godotBridge.invokeTool("scan_status", {}),
40296
+ delay3(PLAYING_STATUS_MS).then(() => null)
40297
+ ]);
40298
+ if (answer === null) {
40299
+ return null;
40300
+ }
40301
+ const since = readNumber(asParams(answer), "sinceScanFinishedMs");
40302
+ return {
40303
+ scanning: readBoolean(asParams(answer), "scanning") ?? false,
40304
+ importing: readBoolean(asParams(answer), "importing") ?? false,
40305
+ finishedMsAgo: since === undefined || since < 0 ? null : since
40306
+ };
40307
+ } catch {
40308
+ return null;
40309
+ }
40310
+ }
40186
40311
  async handleEditorStatus() {
40187
40312
  const located = await this.locator.find();
40188
40313
  const announced = runtimesAnnounced();
@@ -40213,11 +40338,16 @@ class GodotServer {
40213
40338
  hold: current === null ? {} : await this.holdOf(current, active)
40214
40339
  };
40215
40340
  })();
40216
- const [games, { playing, active, hold }] = await Promise.all([pinged, asked]);
40341
+ const [games, { playing, active, hold }, scan] = await Promise.all([
40342
+ pinged,
40343
+ asked,
40344
+ this.editorScanState()
40345
+ ]);
40217
40346
  return this.jsonTextResponse({
40218
40347
  editor: {
40219
40348
  ...this.getEditorStatusPayload(),
40220
- debugPort: playing?.debugPort ?? this.godotBridge.getStatus().debugPort
40349
+ debugPort: playing?.debugPort ?? this.godotBridge.getStatus().debugPort,
40350
+ scan
40221
40351
  },
40222
40352
  godot: {
40223
40353
  path: located.ok ? located.path : null,
@@ -40376,9 +40506,9 @@ class GodotServer {
40376
40506
  }
40377
40507
  async openAnEditor(engine, projectPath, ports) {
40378
40508
  clearEditorLog(projectPath);
40379
- const editor = spawn(engine, editorArguments(projectPath, ports, editorLogPath(projectPath)), {
40380
- stdio: "ignore",
40381
- detached: true,
40509
+ const launched = await launchOutsideTheTree({
40510
+ command: engine,
40511
+ args: editorArguments(projectPath, ports, editorLogPath(projectPath)),
40382
40512
  env: {
40383
40513
  ...process.env,
40384
40514
  GDHARNESS_RUNTIME_DIR: runtimeDirectory(),
@@ -40387,23 +40517,15 @@ class GodotServer {
40387
40517
  [OPENED_BY_A_SERVER]: "1"
40388
40518
  }
40389
40519
  });
40390
- if (editor.pid !== undefined) {
40391
- writeEditorLogNote(projectPath, editor.pid);
40520
+ if ("error" in launched) {
40521
+ return { pid: null, error: launched.error };
40392
40522
  }
40393
- const started = await new Promise((resolve) => {
40394
- editor.once("spawn", () => {
40395
- resolve(null);
40396
- });
40397
- editor.once("error", (error) => {
40398
- resolve(error.message);
40399
- });
40400
- });
40401
- if (started !== null) {
40402
- return { pid: null, error: started };
40403
- }
40404
- editor.unref();
40405
- this.launchedEditor = editor.pid === undefined ? null : editor;
40406
- return { pid: editor.pid ?? null, error: null };
40523
+ writeEditorLogNote(projectPath, launched.pid);
40524
+ this.launchedEditor = {
40525
+ pid: launched.pid,
40526
+ startedAt: startTimesOf([launched.pid]).get(launched.pid) ?? null
40527
+ };
40528
+ return { pid: launched.pid, error: null };
40407
40529
  }
40408
40530
  async portsForAnEditor() {
40409
40531
  const lsp = portFromEnvOrNull("GDHARNESS_LSP_PORT") ?? await freePort(DEFAULT_LSP_PORT);
@@ -40438,13 +40560,14 @@ class GodotServer {
40438
40560
  if (!engine.ok) {
40439
40561
  return engine.response;
40440
40562
  }
40563
+ const scanned = await this.untilTheEditorsScanIsWritten(project.value.path);
40441
40564
  const refreshed = await this.refreshStaleClasses(project.value.path);
40442
40565
  if (!refreshed.ok) {
40443
40566
  return refreshed.response;
40444
40567
  }
40445
40568
  const sceneArgument = sceneToRun?.ok ? sceneToRun.relativePath : null;
40446
40569
  if (op === "check") {
40447
- return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value, asked.value);
40570
+ return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value, scanned, asked.value);
40448
40571
  }
40449
40572
  await this.pickUpWhatTheEditorIsPlaying();
40450
40573
  let ended = null;
@@ -40463,7 +40586,7 @@ class GodotServer {
40463
40586
  const runtimeWaitMs = this.announceBudget(args, project.value.path);
40464
40587
  const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
40465
40588
  if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && !needsItsOwnProcess(asked.value)) {
40466
- return await this.playThroughEditor(sceneArgument, refreshed.value, project.value.path, alreadyPlaying, runtimeWaitMs, ended);
40589
+ return await this.playThroughEditor(sceneArgument, refreshed.value, scanned, project.value.path, alreadyPlaying, runtimeWaitMs, ended);
40467
40590
  }
40468
40591
  const cmdArgs = runArguments({
40469
40592
  projectPath: project.value.path,
@@ -40472,7 +40595,12 @@ class GodotServer {
40472
40595
  userArgs: given.value
40473
40596
  });
40474
40597
  this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
40475
- const started = this.spawnKeptGame(engine.value, cmdArgs, project.value.path, needsItsOwnProcess(asked.value) ? environmentFor({ ...asked.value, runtimeDirectory: runtimeDirectory() }) : undefined);
40598
+ const started = await this.launchKeptGame(engine.value, cmdArgs, project.value.path, needsItsOwnProcess(asked.value) ? environmentFor({ ...asked.value, runtimeDirectory: runtimeDirectory() }) : undefined);
40599
+ if ("error" in started) {
40600
+ return this.createErrorResponse(`The game could not be started: ${started.error}`, [
40601
+ "editor_status says which Godot this server runs and whether it answers"
40602
+ ]);
40603
+ }
40476
40604
  started.announcedBefore = alreadyPlaying;
40477
40605
  this.activeProcess = started;
40478
40606
  const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && given.value.length > 0 ? " The editor cannot be handed arguments for a game it plays, so this one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && needsItsOwnProcess(asked.value) ? " The editor plays a game with its own environment, so a run given one was started " + "here: the debug_* tools answer only for a game the editor is playing." : "";
@@ -40484,10 +40612,11 @@ class GodotServer {
40484
40612
  started: true,
40485
40613
  through: "gdharness",
40486
40614
  alsoRunning: stranded.length === 0 ? undefined : stranded.map((one) => one.pid),
40487
- pid: started.process.pid ?? null,
40615
+ pid: started.pid,
40488
40616
  arguments: cmdArgs,
40489
40617
  transcript: started.transcript,
40490
40618
  refreshedClasses: refreshed.value,
40619
+ ...scanWaitAnswer(scanned),
40491
40620
  savesNote: asked.value.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
40492
40621
  endedPreviousRun: endedPreviousRun(ended),
40493
40622
  runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
@@ -40569,7 +40698,7 @@ class GodotServer {
40569
40698
  writeBootNote(run.projectPath, took);
40570
40699
  }
40571
40700
  }
40572
- async playThroughEditor(scene, refreshedClasses, projectPath, alreadyPlaying, runtimeWaitMs, ended) {
40701
+ async playThroughEditor(scene, refreshedClasses, scanned, projectPath, alreadyPlaying, runtimeWaitMs, ended) {
40573
40702
  const log = new GameLog;
40574
40703
  try {
40575
40704
  await this.dap().connect();
@@ -40598,7 +40727,6 @@ class GodotServer {
40598
40727
  closeSync2(transcript.fd);
40599
40728
  const playAnswer = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
40600
40729
  const played = {
40601
- process: null,
40602
40730
  pid: null,
40603
40731
  log,
40604
40732
  transcript: transcript.path,
@@ -40620,6 +40748,7 @@ class GodotServer {
40620
40748
  through: "editor",
40621
40749
  scene: scene === null ? "the main scene" : `res://${scene}`,
40622
40750
  refreshedClasses,
40751
+ ...scanWaitAnswer(scanned),
40623
40752
  debugPort: readNumber(playAnswer, "debugPort"),
40624
40753
  endedPreviousRun: endedPreviousRun(ended),
40625
40754
  breakpoints: breakpoints.applied.length === 0 ? undefined : this.breakpointsAsProjectSpells(projectPath, breakpoints.applied),
@@ -40696,8 +40825,14 @@ class GodotServer {
40696
40825
  }
40697
40826
  async endActiveGame(reason, going) {
40698
40827
  const running = this.activeProcess;
40699
- const recorded = running?.process === null ? readRunRecord() : null;
40700
- clearRunRecord((record) => this.couldBeOurs(record.projectPath));
40828
+ const recorded = running !== null && !running.throughEditor ? readRunRecord() : null;
40829
+ const clearTheRecord = () => {
40830
+ clearRunRecord((record) => this.couldBeOurs(record.projectPath));
40831
+ };
40832
+ const aKeeperHoldsIt = running !== null && going && !running.throughEditor && running.pid !== null;
40833
+ if (!aKeeperHoldsIt) {
40834
+ clearTheRecord();
40835
+ }
40701
40836
  if (!running || !going) {
40702
40837
  return true;
40703
40838
  }
@@ -40705,16 +40840,13 @@ class GodotServer {
40705
40840
  const announced = this.announcedPidOf(running);
40706
40841
  if (running.throughEditor) {
40707
40842
  await this.handleViaBridge("stop_playing", {});
40708
- return await untilGone(null, announced === undefined ? [] : [announced]);
40709
- }
40710
- if (running.process !== null) {
40711
- running.process.kill();
40712
- return await untilGone(running.process, announced === undefined ? [] : [announced]);
40843
+ return await untilGone(announced === undefined ? [] : [announced]);
40713
40844
  }
40714
40845
  if (running.pid === null) {
40715
40846
  return true;
40716
40847
  }
40717
40848
  if (recorded === null || recorded.pid !== running.pid || !stillTheRecordedRun(recorded)) {
40849
+ clearTheRecord();
40718
40850
  running.endedHere = null;
40719
40851
  running.log.record("warning", `This run was not ended here: pid ${running.pid} no longer answers as the run that was recorded, so nothing was signalled. If that process is still the game, end it yourself; if it is not, it belongs to something else.`);
40720
40852
  return true;
@@ -40727,12 +40859,20 @@ class GodotServer {
40727
40859
  } catch {
40728
40860
  landed = false;
40729
40861
  }
40730
- const gone = await untilGone(null, [running.pid]);
40862
+ const gone = await untilGone([running.pid]);
40863
+ if (gone) {
40864
+ await this.untilTheExitIsRecorded(running);
40865
+ }
40866
+ if (landed && gone) {
40867
+ running.exitCode = null;
40868
+ running.exitSignal = "SIGTERM";
40869
+ }
40870
+ clearTheRecord();
40731
40871
  running.log.record("info", landed ? `gdharness ended pid ${running.pid}, which the operating system described as ${described}.` : `gdharness signalled pid ${running.pid} and it was already gone; the operating system had described it as ${described}.`);
40732
40872
  return gone;
40733
40873
  }
40734
40874
  spawnGame(godotPath, cmdArgs, env) {
40735
- const child = spawn(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
40875
+ const child = spawn2(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
40736
40876
  const cancelled = callSignal();
40737
40877
  if (cancelled !== undefined) {
40738
40878
  const end = () => {
@@ -40784,25 +40924,23 @@ class GodotServer {
40784
40924
  });
40785
40925
  return started;
40786
40926
  }
40787
- spawnKeptGame(godotPath, cmdArgs, projectPath, env) {
40927
+ async launchKeptGame(godotPath, cmdArgs, projectPath, env) {
40788
40928
  const startedAt = Date.now();
40789
40929
  const transcript = openTranscript(startedAt);
40790
- let child;
40791
- try {
40792
- child = spawn(godotPath, cmdArgs, {
40793
- stdio: ["ignore", transcript.fd, transcript.fd],
40794
- detached: true,
40795
- ...env ? { env } : {}
40796
- });
40797
- } finally {
40798
- closeSync2(transcript.fd);
40930
+ closeSync2(transcript.fd);
40931
+ const launched = await launchOutsideTheTree({
40932
+ command: godotPath,
40933
+ args: cmdArgs,
40934
+ ...env === undefined ? {} : { env },
40935
+ run: { transcript: transcript.path, startedAt, projectPath }
40936
+ });
40937
+ if ("error" in launched) {
40938
+ return launched;
40799
40939
  }
40800
- child.unref();
40801
- const log = new GameLog;
40802
- const started = {
40803
- process: child,
40804
- pid: child.pid ?? null,
40805
- log,
40940
+ sweepTranscripts();
40941
+ return {
40942
+ pid: launched.pid,
40943
+ log: new GameLog,
40806
40944
  transcript: transcript.path,
40807
40945
  readOffset: 0,
40808
40946
  projectPath,
@@ -40812,34 +40950,6 @@ class GodotServer {
40812
40950
  throughEditor: false,
40813
40951
  brokeOn: null
40814
40952
  };
40815
- child.on("exit", (code, signal) => {
40816
- this.logDebug(`Godot process exited with code ${code ?? "none"}, signal ${signal ?? "none"}`);
40817
- this.drainTranscript(started);
40818
- log.finish();
40819
- started.exitCode = code;
40820
- started.exitSignal = code === null ? signal : null;
40821
- if (started.pid !== null) {
40822
- recordRunEnded(started.pid, { exitCode: started.exitCode, exitSignal: started.exitSignal });
40823
- }
40824
- });
40825
- child.on("error", (err) => {
40826
- console.error("Failed to start Godot process:", err);
40827
- log.append("stderr", `${err.message}
40828
- `);
40829
- started.exitCode = -1;
40830
- });
40831
- if (started.pid !== null) {
40832
- writeRunRecord({
40833
- pid: started.pid,
40834
- transcript: transcript.path,
40835
- startedAt,
40836
- projectPath,
40837
- arguments: cmdArgs,
40838
- command: godotPath
40839
- });
40840
- }
40841
- sweepTranscripts();
40842
- return started;
40843
40953
  }
40844
40954
  drainTranscript(game) {
40845
40955
  if (game.transcript === null) {
@@ -40895,7 +41005,6 @@ class GodotServer {
40895
41005
  const left = readEditorRunNote();
40896
41006
  const note = left !== null && project !== null && isSameDirectory(left.projectPath, project) ? left : null;
40897
41007
  const picked = {
40898
- process: null,
40899
41008
  pid: null,
40900
41009
  log: new GameLog,
40901
41010
  transcript: note?.transcript ?? null,
@@ -41032,7 +41141,6 @@ class GodotServer {
41032
41141
  return null;
41033
41142
  }
41034
41143
  const adopted = {
41035
- process: null,
41036
41144
  pid: record.pid,
41037
41145
  log: new GameLog,
41038
41146
  transcript: record.transcript,
@@ -41042,7 +41150,8 @@ class GodotServer {
41042
41150
  exitCode: null,
41043
41151
  exitSignal: null,
41044
41152
  throughEditor: false,
41045
- brokeOn: null
41153
+ brokeOn: null,
41154
+ pickedUp: true
41046
41155
  };
41047
41156
  this.drainTranscript(adopted);
41048
41157
  if (!alive(record.pid) || !couldStillBeTheRecordedRun(record)) {
@@ -41123,7 +41232,7 @@ class GodotServer {
41123
41232
  currentRun() {
41124
41233
  return this.activeProcess ?? this.adoptRecordedRun();
41125
41234
  }
41126
- async checkBoot(godotPath, projectPath, scene, args, userArgs, asked = {}) {
41235
+ async checkBoot(godotPath, projectPath, scene, args, userArgs, scanned, asked = {}) {
41127
41236
  const frames = readPositiveNumber(args, "frames") ?? 3;
41128
41237
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 60000;
41129
41238
  const cmdArgs = runArguments({ projectPath, headless: true, scene, quitAfter: frames, userArgs });
@@ -41153,6 +41262,7 @@ class GodotServer {
41153
41262
  frames,
41154
41263
  errors,
41155
41264
  warnings,
41265
+ ...scanWaitAnswer(scanned),
41156
41266
  savesNote: asked.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
41157
41267
  entries: forAnswer(boot.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
41158
41268
  });
@@ -41176,8 +41286,8 @@ class GodotServer {
41176
41286
  async runStillGoing(run, editorSays) {
41177
41287
  if (!run.throughEditor) {
41178
41288
  const going = stillRunning(run);
41179
- if (!going && run.process !== null && !exited(run)) {
41180
- await untilGone(run.process, [], EXIT_REPORTED_WITHIN_MS);
41289
+ if (!going && run.pid !== null && !exited(run) && run.endedUnwatched !== true && typeof run.endedHere !== "string" && !await this.untilTheExitIsRecorded(run)) {
41290
+ run.endedUnwatched = true;
41181
41291
  }
41182
41292
  return going;
41183
41293
  }
@@ -41194,6 +41304,21 @@ class GodotServer {
41194
41304
  }
41195
41305
  return runIsUp(run, said);
41196
41306
  }
41307
+ async untilTheExitIsRecorded(run) {
41308
+ const until = Date.now() + EXIT_REPORTED_WITHIN_MS;
41309
+ for (;; ) {
41310
+ const record = readRunRecord();
41311
+ if (record !== null && record.pid === run.pid && (record.exitCode !== undefined || record.exitSignal !== undefined)) {
41312
+ run.exitCode = record.exitCode ?? null;
41313
+ run.exitSignal = record.exitSignal ?? null;
41314
+ return true;
41315
+ }
41316
+ if (Date.now() >= until) {
41317
+ return false;
41318
+ }
41319
+ await delay3(50);
41320
+ }
41321
+ }
41197
41322
  handleEditorConsole(args) {
41198
41323
  const status = this.godotBridge.getStatus();
41199
41324
  const projectPath = status.projectPath ?? this.ownProject ?? undefined;
@@ -41295,7 +41420,8 @@ class GodotServer {
41295
41420
  throughEditor: run.throughEditor,
41296
41421
  announced: announced !== undefined || run.announcedPid !== undefined,
41297
41422
  couldAnnounce: run.projectPath !== null && existsSync15(join23(run.projectPath, RUNTIME_AUTOLOAD2.path)),
41298
- endedHere: typeof run.endedHere === "string"
41423
+ endedHere: typeof run.endedHere === "string",
41424
+ pickedUp: run.pickedUp === true
41299
41425
  }));
41300
41426
  }
41301
41427
  if (run.pickedUpPlaying === true) {
@@ -41321,7 +41447,7 @@ class GodotServer {
41321
41447
  ...hold,
41322
41448
  endedUnwatched: run.endedUnwatched === true ? true : undefined,
41323
41449
  endedBy: stillRunning(run) ? undefined : run.endedHere ?? null,
41324
- project: !run.throughEditor && run.process === null ? run.projectPath ?? undefined : undefined,
41450
+ project: run.throughEditor ? undefined : run.projectPath ?? undefined,
41325
41451
  transcript: run.transcript ?? undefined,
41326
41452
  startedAt: new Date(run.startedAt).toISOString(),
41327
41453
  elapsedMs: Date.now() - run.startedAt,
@@ -41536,6 +41662,10 @@ class GodotServer {
41536
41662
  return now === null ? [] : lost.filter((name) => now.has(name));
41537
41663
  }
41538
41664
  async rebuildClassCache(projectPath) {
41665
+ const scanned = await this.untilTheEditorsScanIsWritten(projectPath);
41666
+ return withScanWait(await this.rebuildClassCacheFromDisk(projectPath), scanned);
41667
+ }
41668
+ async rebuildClassCacheFromDisk(projectPath) {
41539
41669
  const before = cachedClasses(projectPath);
41540
41670
  const writtenBefore = cacheWrittenAt(projectPath);
41541
41671
  const noted = readClassNote(projectPath);
@@ -41924,7 +42054,7 @@ async function runGodotServer() {
41924
42054
  const server = new GodotServer;
41925
42055
  await server.run();
41926
42056
  }
41927
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run7, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
42057
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run7, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, SCAN_WAIT_MS = 30000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
41928
42058
  var init_server3 = __esm(() => {
41929
42059
  init_mcp();
41930
42060
  init_stdio2();
@@ -41947,6 +42077,7 @@ var init_server3 = __esm(() => {
41947
42077
  init_issues();
41948
42078
  init_junit();
41949
42079
  init_lsp_client();
42080
+ init_outside();
41950
42081
  init_paths();
41951
42082
  init_ports();
41952
42083
  init_process_children();
@@ -41962,7 +42093,7 @@ var init_server3 = __esm(() => {
41962
42093
  init_tool_definitions();
41963
42094
  init_update_check();
41964
42095
  run7 = promisify7(execFile7);
41965
- __dirname2 = dirname16(fileURLToPath5(import.meta.url));
42096
+ __dirname2 = dirname16(fileURLToPath6(import.meta.url));
41966
42097
  CONSOLE_WRAPPER = /_console(\.exe)?$/i;
41967
42098
  LONGEST_BOOT_NOTED_MS = 5 * 60000;
41968
42099
  VALUE_NAMES = {
@@ -43697,6 +43828,13 @@ a class it cannot resolve would go out of that file with it. The answer names th
43697
43828
  \`cacheRestored\`, so no fresh engine, CI run or clone inherits the short file. That is the same
43698
43829
  class \`unseenByEditor\` names, and the loss is not a reason to refuse the scan.
43699
43830
 
43831
+ The editor also scans on its own, when files change under it, and it rewrites that file just after
43832
+ the scan ends. Every engine this server starts on the project (a run, a boot check, a test run, a
43833
+ headless operation) waits until a scan in progress and the write after it are done, and says how
43834
+ long under \`waitedForEditorScanMs\`. A \`scanNote\` means a scan was still going after thirty
43835
+ seconds and the engine started anyway: a "Could not find type" from that engine is the scan, not
43836
+ the script, and \`editor_rescan\` followed by the same call again is the cure.
43837
+
43700
43838
  A diagnostic about a member rather than a type, \`Static function "x()" not found in base "Y"\`
43701
43839
  about something the engine compiles, is a different thing and is no longer stale here: each ask
43702
43840
  gives the document back when its answer arrives, so the next one is read off disk. If one turns