gdharness 1.3.56 → 1.3.58

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
@@ -1763,6 +1763,9 @@ function nonEmpty(value) {
1763
1763
  function keptSlotsOf(count) {
1764
1764
  return Math.max(2, Math.floor(count / 4));
1765
1765
  }
1766
+ function reservedSlotOf(count) {
1767
+ return count - Math.max(1, count - keptSlotsOf(count)) >= 2 ? count - 1 : null;
1768
+ }
1766
1769
  function gamesKeptOf(kept) {
1767
1770
  return Math.max(0, Math.min(Math.floor(kept / 2), kept - GATE_KEPT - 1));
1768
1771
  }
@@ -1777,19 +1780,24 @@ function slotsDrawnOn(count, taker, queue) {
1777
1780
  return { numbers: every };
1778
1781
  }
1779
1782
  if (taker === "tier") {
1780
- return { numbers: [...kept].reverse(), limit: "tier" };
1783
+ const reserve = reservedSlotOf(count);
1784
+ return { numbers: [...kept].reverse().filter((slot) => slot !== reserve), limit: "tier" };
1781
1785
  }
1782
1786
  return { numbers: kept.slice(0, Math.max(1, kept.length - GATE_KEPT)), limit: "kept" };
1783
1787
  }
1784
1788
  function inAGate(env = process4.env) {
1785
1789
  return nonEmpty(env[SHARE_VARIABLE]) !== undefined && nonEmpty(env[SHARE_COUNT_VARIABLE]) !== undefined;
1786
1790
  }
1787
- function inheritsSlot(tree, pid, holder, executable) {
1791
+ function inheritsSlot(tree, pid, holder, executable, engineStarted) {
1788
1792
  const program = (name) => name.toLowerCase().replace(/\.(?:exe|cmd|bat|com)$/, "");
1789
1793
  const own = program(executable);
1790
- for (const above of ancestorsIn(tree, pid, holder)) {
1794
+ for (const above of ancestorsIn(tree, pid, holder.pid)) {
1791
1795
  if (program(readCommandLine(tree.get(above)?.command ?? "").executable) !== own) {
1792
- return above === holder;
1796
+ if (above === holder.pid) {
1797
+ return true;
1798
+ }
1799
+ const started = tree.get(above)?.startedAt ?? engineStarted;
1800
+ return above === holder.engine?.pid && started !== undefined && Math.abs(started - holder.engine.started) <= START_TOLERANCE_MS;
1793
1801
  }
1794
1802
  }
1795
1803
  return false;
@@ -1854,20 +1862,29 @@ function holderOf(content) {
1854
1862
  if (typeof parsed !== "object" || parsed === null) {
1855
1863
  return null;
1856
1864
  }
1857
- const { pid, started, by, game } = parsed;
1858
- if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || typeof started !== "number") {
1865
+ const { by, game, engine } = parsed;
1866
+ const named = namedProcess(parsed);
1867
+ if (named === null) {
1859
1868
  return null;
1860
1869
  }
1870
+ const runs = namedProcess(engine);
1861
1871
  return {
1862
- pid,
1863
- started,
1872
+ ...named,
1864
1873
  ...typeof by === "string" && by !== "" ? { by } : {},
1865
- ...game === true ? { game } : {}
1874
+ ...game === true ? { game } : {},
1875
+ ...runs === null ? {} : { engine: runs }
1866
1876
  };
1867
1877
  } catch {
1868
1878
  return null;
1869
1879
  }
1870
1880
  }
1881
+ function namedProcess(value) {
1882
+ if (typeof value !== "object" || value === null) {
1883
+ return null;
1884
+ }
1885
+ const { pid, started } = value;
1886
+ return typeof pid === "number" && Number.isInteger(pid) && pid > 0 && typeof started === "number" ? { pid, started } : null;
1887
+ }
1871
1888
  function lockIsFree(content, writtenAt, now, isAlive, startOf) {
1872
1889
  const holder = holderOf(content);
1873
1890
  if (holder === null) {
@@ -1913,7 +1930,7 @@ class EngineSlots {
1913
1930
  try {
1914
1931
  for (;; ) {
1915
1932
  signal?.throwIfAborted();
1916
- const lock = await this.takeFree(game);
1933
+ const lock = await this.takeFree(game, Date.now() - began);
1917
1934
  if (lock !== null) {
1918
1935
  return lock;
1919
1936
  }
@@ -1945,7 +1962,7 @@ class EngineSlots {
1945
1962
  }
1946
1963
  }
1947
1964
  }
1948
- async takeFree(game = false) {
1965
+ async takeFree(game = false, waitedMs = 0) {
1949
1966
  mkdirSync2(this.directory, { recursive: true });
1950
1967
  await this.sweep();
1951
1968
  const inherited = this.inherited === null ? await this.inheritedLock() : null;
@@ -1954,11 +1971,14 @@ class EngineSlots {
1954
1971
  return inherited;
1955
1972
  }
1956
1973
  const taker = this.taker();
1957
- this.drawn = slotsDrawnOn(this.count, taker, taker === "gate" || taker === "timing" ? "none" : await this.gateQueue());
1974
+ const aged = taker === "other" && !game && waitedMs >= ONE_OFF_AGED_MS;
1975
+ this.drawn = slotsDrawnOn(this.count, taker, taker === "gate" || taker === "timing" || aged ? "none" : await this.gateQueue());
1958
1976
  if (game && this.drawn.limit === "kept") {
1959
- const share = slotsDrawnOn(this.count, "tier", "queued").numbers;
1960
- const games = await this.heldByGames(share);
1961
- if (games.length >= gamesKeptOf(share.length)) {
1977
+ const reserve = reservedSlotOf(this.count);
1978
+ this.drawn = { ...this.drawn, numbers: this.drawn.numbers.filter((slot) => slot !== reserve) };
1979
+ const kept = this.count - Math.max(1, this.count - keptSlotsOf(this.count));
1980
+ const games = await this.heldByGames(Array.from({ length: kept }, (_, at) => this.count - kept + at));
1981
+ if (games.length >= gamesKeptOf(kept)) {
1962
1982
  this.drawn = { numbers: games, limit: "games" };
1963
1983
  return null;
1964
1984
  }
@@ -1981,7 +2001,9 @@ class EngineSlots {
1981
2001
  return null;
1982
2002
  }
1983
2003
  const tree = await processTree();
1984
- return tree !== undefined && inheritsSlot(tree, process4.pid, holder.pid, basename3(process4.execPath)) ? lock : null;
2004
+ const engine = holder.engine?.pid;
2005
+ const engineStarted = engine === undefined || tree?.get(engine)?.startedAt !== undefined ? undefined : (await startTimesOf([engine])).get(engine);
2006
+ return tree !== undefined && inheritsSlot(tree, process4.pid, holder, basename3(process4.execPath), engineStarted) ? lock : null;
1985
2007
  }
1986
2008
  markWaiting() {
1987
2009
  const mark = this.waitMarked();
@@ -2107,6 +2129,25 @@ class EngineSlots {
2107
2129
  await this.cleared(join5(this.directory, name), false);
2108
2130
  }
2109
2131
  }
2132
+ const marks = join5(this.directory, WAITING_FOLDER);
2133
+ let waits;
2134
+ try {
2135
+ waits = readdirSync(marks);
2136
+ } catch {
2137
+ return;
2138
+ }
2139
+ for (const name of waits) {
2140
+ let content;
2141
+ try {
2142
+ content = readWhole(join5(marks, name));
2143
+ } catch {
2144
+ continue;
2145
+ }
2146
+ const pid = content === null ? undefined : holderOf(content)?.pid;
2147
+ if (pid !== undefined && !alive(pid)) {
2148
+ removeWhole(join5(marks, name));
2149
+ }
2150
+ }
2110
2151
  }
2111
2152
  takenOver(lock, holder) {
2112
2153
  return this.cleared(lock, true, holder);
@@ -2172,6 +2213,45 @@ function handSlot(lock, from, to) {
2172
2213
  const holder = content === null ? null : holderOf(content);
2173
2214
  return holder?.pid === from && writeWhole(lock, JSON.stringify(holder.game === true ? { ...to, game: true } : to));
2174
2215
  }
2216
+ function nameSlotEngine(lock, holder, engine) {
2217
+ let content;
2218
+ try {
2219
+ content = readWhole(lock);
2220
+ } catch {
2221
+ return false;
2222
+ }
2223
+ const named = content === null ? null : holderOf(content);
2224
+ return named?.pid === holder && writeWhole(lock, JSON.stringify({ ...named, engine }));
2225
+ }
2226
+ async function nameSlotEngineOf(lock, holder, pid) {
2227
+ const started = (await startTimesOf([pid])).get(pid);
2228
+ if (started !== undefined) {
2229
+ nameSlotEngine(lock, holder, { pid, started });
2230
+ }
2231
+ }
2232
+ async function slotEngineNamed(lock, run, pidFile) {
2233
+ if (pidFile === undefined) {
2234
+ if (run.pid !== null) {
2235
+ await nameSlotEngineOf(lock, process4.pid, run.pid);
2236
+ }
2237
+ return;
2238
+ }
2239
+ const state = { settled: false };
2240
+ run.settled.finally(() => {
2241
+ state.settled = true;
2242
+ });
2243
+ while (!state.settled) {
2244
+ let written = Number.NaN;
2245
+ try {
2246
+ written = Number(readFileSync6(pidFile, "utf8").trim());
2247
+ } catch {}
2248
+ if (Number.isInteger(written) && written > 0) {
2249
+ await nameSlotEngineOf(lock, process4.pid, written);
2250
+ return;
2251
+ }
2252
+ await delay2(ENGINE_PID_LOOK_MS);
2253
+ }
2254
+ }
2175
2255
  function giveBackSlot(lock, pid) {
2176
2256
  try {
2177
2257
  if (holderOf(readFileSync6(lock, "utf8"))?.pid === pid) {
@@ -2290,7 +2370,7 @@ async function inASlot(body) {
2290
2370
  held.release();
2291
2371
  }
2292
2372
  }
2293
- var HELD_VARIABLE = "GODOT_ENGINE_HELD", SLOTS_VARIABLE = "GODOT_ENGINE_SLOTS", SLOT_COUNT_VARIABLE = "GODOT_ENGINE_SLOT_COUNT", PROCESSORS_A_SLOT = 2, START_TOLERANCE_MS = 2000, UNWRITTEN_MS = 1e4, SLOT_LOOK_MS = 1000, SLOT_SETTLE_MS = 50, WAIT_SAID_MS = 20000, COMMAND_WAIT_SAID_MS = 2000, START_READ_MS = 1e4, WAITING_FOLDER = "waiting", QUEUED_FOLDER = "queued", SHARE_VARIABLE = "GODOT_ENGINE_SHARE", SHARE_COUNT_VARIABLE = "GODOT_ENGINE_SHARE_COUNT", GATE_KEPT = 1, TIMED_VARIABLE = "GODOT_ENGINE_TIMED", TIER_VARIABLE = "GODOT_ENGINE_TIER", SEATED_SUFFIX = ".seated", waitsMarked = 0, ours, saidTo, takenBy;
2373
+ var HELD_VARIABLE = "GODOT_ENGINE_HELD", SLOTS_VARIABLE = "GODOT_ENGINE_SLOTS", SLOT_COUNT_VARIABLE = "GODOT_ENGINE_SLOT_COUNT", PROCESSORS_A_SLOT = 2, START_TOLERANCE_MS = 2000, UNWRITTEN_MS = 1e4, SLOT_LOOK_MS = 1000, SLOT_SETTLE_MS = 50, WAIT_SAID_MS = 20000, COMMAND_WAIT_SAID_MS = 2000, START_READ_MS = 1e4, WAITING_FOLDER = "waiting", QUEUED_FOLDER = "queued", SHARE_VARIABLE = "GODOT_ENGINE_SHARE", SHARE_COUNT_VARIABLE = "GODOT_ENGINE_SHARE_COUNT", GATE_KEPT = 1, ONE_OFF_AGED_MS = 120000, TIMED_VARIABLE = "GODOT_ENGINE_TIMED", TIER_VARIABLE = "GODOT_ENGINE_TIER", SEATED_SUFFIX = ".seated", waitsMarked = 0, ENGINE_PID_LOOK_MS = 100, ours, saidTo, takenBy;
2294
2374
  var init_engine_slots = __esm(() => {
2295
2375
  init_alive();
2296
2376
  init_cache_home();
@@ -13189,9 +13269,13 @@ async function watchedEngine(launch, lock, args, directory, log, silenceSeconds,
13189
13269
  return;
13190
13270
  }
13191
13271
  };
13272
+ const found = (engine) => {
13273
+ watching(engine, true);
13274
+ nameSlotEngineOf(lock, process7.pid, engine);
13275
+ };
13192
13276
  let pid = engineAt();
13193
13277
  if (pid !== undefined) {
13194
- watching(pid, true);
13278
+ found(pid);
13195
13279
  }
13196
13280
  try {
13197
13281
  let size = 0;
@@ -13206,7 +13290,7 @@ async function watchedEngine(launch, lock, args, directory, log, silenceSeconds,
13206
13290
  if (pid === undefined) {
13207
13291
  pid = engineAt();
13208
13292
  if (pid !== undefined) {
13209
- watching(pid, true);
13293
+ found(pid);
13210
13294
  }
13211
13295
  }
13212
13296
  const taken = await sample();
@@ -14023,6 +14107,9 @@ function runStarted(executable, args, lock, options, signal) {
14023
14107
  env: onTheSlot(process.env, lock),
14024
14108
  ...signal === undefined ? {} : { signal }
14025
14109
  });
14110
+ if (child.pid !== undefined) {
14111
+ nameSlotEngineOf(lock, process.pid, child.pid);
14112
+ }
14026
14113
  child.stdout.on("data", (chunk) => {
14027
14114
  log.append("stdout", chunk);
14028
14115
  });
@@ -18097,7 +18184,7 @@ var init_tool_definitions = __esm(() => {
18097
18184
  direction: {
18098
18185
  type: "string",
18099
18186
  enum: ["forward", "reverse"],
18100
- description: "forward: what this resource loads or names. Each file named by its path carries kind, as reverse words it: ext_resource, extends, preload, load, include, or path for a path only written in a string, which the file itself may never load. Each reference closing a cycle is marked circular, with the cycle under circular_references. A script's dependencies include every script whose class_name it uses, marked by_class with the name, and the script of every autoload it names, marked by_autoload with the autoload's name. An ext_resource whose uid names another file than its path is followed to that file, as the engine loads it, with the path written under stale_path; one whose uid names no file the project has is followed by its path, as the engine does after warning, with the uid under unknown_uid; and a load or preload of a uid the project does not know is listed under that uid as missing. A shader's #include is followed as the preprocessor reads it, relative to the shader. A file the depth stops at carries beyond_depth, how many files it names that were not walked. A file's dependencies are listed at its first entry, and again further down only where the depth cut that list short and more is left; every other entry for a file naming any carries dependencies_listed_above. summary.total_resources is the file asked about, so 1; total_dependencies counts each file reached, directly or through another, once however many places load it; total_references counts the entries in the tree, and missing names every file referenced that does not exist, with the files referring to it. reverse: every file that refers to it and how, for a file that is gone as well while anything still refers to it, answered with exists: false (a missing file nothing refers to is refused, naming the nearest files), a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name, and for an autoload's script every use of the autoload's name, with autoload naming it. Each reference carries a kind: ext_resource, extends, preload, load, include or path for a reference by path, with stale_path when an ext_resource reaches it by its uid under another path, and unknown_uid when one names it by path beside a uid naming no file; setting, plugin, export_preset and import for one in project.godot, a plugin.cfg, export_presets.cfg or an import sidecar; class_name for a use of the declared name, the declaration itself being the script asked about; autoload for code naming the autoload. Three kinds are not uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs; comment is one in ordinary prose, which it would only make stale; and stale_path is an ext_resource naming it by a path the engine does not load, since its uid leads to the file under loads. summary.in_code is the count with those left out, which is the number to read when the question is whether anything still uses this. Default forward."
18187
+ description: "forward: what this resource loads or names. Each file named by its path carries kind, as reverse words it: ext_resource, extends, preload, load, include, or path for a path only written in a string, which the file itself may never load. Each reference closing a cycle is marked circular, with the cycle under circular_references. A script's dependencies include every script whose class_name it uses, marked by_class with the name, and the script of every autoload it names, marked by_autoload with the autoload's name. An ext_resource whose uid names another file than its path is followed to that file, as the engine loads it, with the path written under stale_path; one whose uid names no file the project has is followed by its path, as the engine does after warning, with the uid under unknown_uid; and a load or preload of a uid the project does not know is listed under that uid as missing. A shader's #include is followed as the preprocessor reads it, relative to the shader. A file the depth stops at carries beyond_depth, how many files it names that were not walked. A file's dependencies are listed at its first entry, and again further down only where the depth cut that list short and more is left; every other entry for a file naming any carries dependencies_listed_above. summary.total_resources is the file asked about, so 1; total_dependencies counts each file reached, directly or through another, once however many places load it; total_references counts the entries in the tree, and missing names every file referenced that does not exist, with the files referring to it. reverse: every file that refers to it and how, for a file that is gone as well while anything still refers to it, answered with exists: false (a missing file nothing refers to is refused, naming the nearest files), a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name, and for an autoload's script every use of the autoload's name, with autoload naming it. Each reference carries a kind: ext_resource, extends, preload, load, include or path for a reference by path, with stale_path when an ext_resource reaches it by its uid under another path, and unknown_uid when one names it by path beside a uid naming no file; setting, plugin, export_preset and import for one in project.godot, a plugin.cfg, export_presets.cfg or an import sidecar; class_name for a use of the declared name, the declaration itself being the script asked about; autoload for code naming the autoload. Three kinds are not uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs; comment is one in ordinary prose, which it would only make stale; and stale_path is an ext_resource naming it by a path the engine does not load, since its uid leads to the file under loads. A file whose header, or an import sidecar, claims the same uid as its own is no use either: it is listed under shares_uid, since the engine maps a uid to one file and loads that one wherever the uid is named. summary.in_code is the count with those left out, which is the number to read when the question is whether anything still uses this. Default forward."
18101
18188
  },
18102
18189
  depth: {
18103
18190
  type: "integer",
@@ -19119,7 +19206,7 @@ var init_tool_definitions = __esm(() => {
19119
19206
  },
19120
19207
  wait: {
19121
19208
  effect: "read",
19122
- summary: "wait for the run to end, then answer as editor_output does",
19209
+ summary: "wait for the run to end, or to be held by its debugger at a breakpoint, a pause or a step, then answer as editor_output does",
19123
19210
  requires: []
19124
19211
  },
19125
19212
  profile: {
@@ -56713,6 +56800,7 @@ ${error.text}`;
56713
56800
  try {
56714
56801
  const run = this.spawnHeld(godotPath, cmdArgs, onTheSlot(env ?? process.env, held.lock), onDesktop);
56715
56802
  run.settled.finally(held.release);
56803
+ slotEngineNamed(held.lock, run, onDesktop?.files.pid);
56716
56804
  return run;
56717
56805
  } catch (error) {
56718
56806
  held.release();
@@ -57455,14 +57543,33 @@ ${error.text}`;
57455
57543
  return elsewhere;
57456
57544
  }
57457
57545
  const budgetMs = readPositiveNumber(args, "timeoutMs") ?? WAIT_FOR_RUN_MS;
57458
- const until = Date.now() + budgetMs;
57546
+ const began = Date.now();
57547
+ const until = began + budgetMs;
57459
57548
  const cancelled = callSignal();
57549
+ let held = false;
57550
+ let holdAsked = 0;
57460
57551
  while (cancelled?.aborted !== true && await this.runStillGoing(run) && Date.now() < until) {
57461
57552
  this.drainEditorOutput(run);
57462
57553
  this.drainTranscript(run);
57554
+ if (Date.now() - holdAsked >= HOLD_ASKED_EVERY_MS) {
57555
+ holdAsked = Date.now();
57556
+ if (await this.heldByItsDebugger(run)) {
57557
+ held = true;
57558
+ break;
57559
+ }
57560
+ }
57463
57561
  await delay8(RUN_POLL_MS);
57464
57562
  }
57465
- return await this.handleGetDebugOutput(args, await this.runStillGoing(run) ? budgetMs : undefined);
57563
+ if (!await this.runStillGoing(run)) {
57564
+ return await this.handleGetDebugOutput(args);
57565
+ }
57566
+ return await this.handleGetDebugOutput(args, held ? { heldAfterMs: Date.now() - began } : { ranOutMs: budgetMs });
57567
+ }
57568
+ async heldByItsDebugger(run) {
57569
+ if (!run.throughEditor) {
57570
+ return run.keeper !== undefined && await this.keptHold(run.keeper) !== null;
57571
+ }
57572
+ return this.dapClient?.holdIsKnown() === true && this.dapClient.whereItStopped() !== null;
57466
57573
  }
57467
57574
  async runStillGoing(run, editorSays) {
57468
57575
  if (!run.throughEditor) {
@@ -57611,7 +57718,7 @@ ${error.text}`;
57611
57718
  "editor_status names every game running on the machine under runtimes"
57612
57719
  ]);
57613
57720
  }
57614
- async handleGetDebugOutput(args, waitedMs) {
57721
+ async handleGetDebugOutput(args, waited) {
57615
57722
  if (readString(args, "op") === "editor") {
57616
57723
  return this.handleEditorConsole(args);
57617
57724
  }
@@ -57648,8 +57755,11 @@ ${error.text}`;
57648
57755
  if (wanted && cpuSeconds === undefined && stillRunning(run)) {
57649
57756
  notes.push(processToAsk === null ? "cpu was asked for and there is no process to ask: the editor is playing this run and holds it, and the game announced no runtime, so nothing here has its process id. A game with the runtime addon announces one; editor_run start without a connected editor, or with args, starts the game here instead and that run answers cpuSeconds either way." : "cpu was asked for and this platform would not say what the run has used.");
57650
57757
  }
57651
- if (waitedMs !== undefined) {
57652
- notes.push(`The wait of ${waitedMs}ms ran out and the run is still going, so this is what it had printed by then rather than everything it will print. editor_run wait again to keep waiting.`);
57758
+ if (waited !== undefined && "ranOutMs" in waited) {
57759
+ notes.push(`The wait of ${waited.ranOutMs}ms ran out and the run is still going, so this is what it had printed by then rather than everything it will print. editor_run wait again to keep waiting.`);
57760
+ }
57761
+ if (waited !== undefined && "heldAfterMs" in waited) {
57762
+ notes.push(`The wait answered after ${waited.heldAfterMs}ms because the game is held by its debugger, where heldAt says, and a held game does not end: debug_control continue lets it go, and editor_run wait then waits on.`);
57653
57763
  }
57654
57764
  const restarts = run.restartedFrom ?? [];
57655
57765
  if (restarts.length > 0) {
@@ -59507,7 +59617,7 @@ async function runGodotServer() {
59507
59617
  const server = new GodotServer;
59508
59618
  await server.run();
59509
59619
  }
59510
- var TYPE_ARGUMENTS, UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, FAILED_BUDGET = 1e6, ANSWER_BUDGET = 4000000, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, SUCCESSOR_SOUGHT_WITHIN_MS = 20000, CLOSE_ASK_MS = 3000, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_SETTLED_MS = 500, SLOW_ANSWER_MS = 1000, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_TIMEOUT_MS = 600000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", LOOPING_AFTER = 100, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, DESTINATION_ARGUMENTS, OUTPUT_PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, MOST_PRINTED_LINES = 50, MOST_UNDECLARED_ERRORS = 20, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", RAISED_WARNING = " (Warning treated as error.)", PARSE_ERROR2 = "Parse Error: ", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
59620
+ var TYPE_ARGUMENTS, UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, FAILED_BUDGET = 1e6, ANSWER_BUDGET = 4000000, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, SUCCESSOR_SOUGHT_WITHIN_MS = 20000, CLOSE_ASK_MS = 3000, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_SETTLED_MS = 500, SLOW_ANSWER_MS = 1000, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_TIMEOUT_MS = 600000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", LOOPING_AFTER = 100, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, HOLD_ASKED_EVERY_MS = 1000, PATH_SOLUTIONS, DESTINATION_ARGUMENTS, OUTPUT_PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, MOST_PRINTED_LINES = 50, MOST_UNDECLARED_ERRORS = 20, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", RAISED_WARNING = " (Warning treated as error.)", PARSE_ERROR2 = "Parse Error: ", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
59511
59621
  var init_server = __esm(() => {
59512
59622
  init_dist2();
59513
59623
  init_stdio();
@@ -16,8 +16,9 @@ const SETTLE_MS: int = 8000
16
16
  ## How long a window with nothing in it is walked before the client stops: a game building its tree
17
17
  ## fills the window within a few frames of being asked, and one building none never does.
18
18
  const EMPTY_MS: int = 4000
19
- ## How long starting PowerShell and loading UI Automation may take before the client is stopped. It
20
- ## waits on the machine alone: a warm start takes under a second, and a cold one on a loaded Windows
19
+ ## How long starting PowerShell, loading UI Automation and the client's other cold loads may take
20
+ ## before the client is stopped. It waits on the machine alone: a warm start takes under a second,
21
+ ## and a cold one on a loaded Windows
21
22
  ## runner took the whole of a single 23 s budget meant for the walk as well.
22
23
  const START_MS: int = 45000
23
24
  ## How long the walks may take beyond [constant SETTLE_MS] a window once UI Automation is loaded,
@@ -39,6 +40,7 @@ const NODE_ID: String = "_node_instance"
39
40
  const WALK: String = r"""
40
41
  $ErrorActionPreference = 'Stop'
41
42
  function Answer($value) {
43
+ Mark 'answering'
42
44
  [System.IO.File]::WriteAllText($Out, (ConvertTo-Json -InputObject $value -Depth 100 -Compress))
43
45
  Mark 'answered'
44
46
  }
@@ -48,7 +50,6 @@ function Mark($stage) {
48
50
  try {
49
51
  Mark 'began'
50
52
  Add-Type -AssemblyName UIAutomationClient, UIAutomationTypes
51
- Mark 'loaded'
52
53
  $walker = [System.Windows.Automation.TreeWalker]::RawViewWalker
53
54
  function Patterned($element, $name) {
54
55
  $pattern = ("System.Windows.Automation.$($name)Pattern" -as [type])::Pattern
@@ -70,6 +71,10 @@ try {
70
71
  } catch {
71
72
  $script:unread = "$_"
72
73
  }
74
+ # Serialised once here, since the first ConvertTo-Json loads its serialiser, and on a loaded
75
+ # machine that cold load belongs to the start's budget, not to the walks'.
76
+ $null = ConvertTo-Json -InputObject @{ warm = @(1) } -Depth 100 -Compress
77
+ Mark 'loaded'
73
78
  function FullDescription($element) {
74
79
  if ($null -ne $script:unread) { return '' }
75
80
  try {
@@ -281,7 +286,9 @@ func get_accessibility(params: Dictionary) -> Dictionary:
281
286
  var looked_at: int = began_at
282
287
  while OS.is_process_running(pid):
283
288
  var now: int = Time.get_ticks_msec()
284
- if now - looked_at >= LOOK_MS:
289
+ # Read again at the deadline too, so an answer marked since the last look is taken rather
290
+ # than stopped.
291
+ if now - looked_at >= LOOK_MS or now > deadline:
285
292
  looked_at = now
286
293
  var marked: String = FileAccess.get_file_as_string(progress)
287
294
  if not walking and marked.contains("loaded "):
@@ -756,8 +763,8 @@ static func _marks(progress: String) -> Array:
756
763
 
757
764
  ## How far a client started at [param started_ms] and stopped at [param stopped_ms] got by the marks
758
765
  ## in [param progress], said for a caller whose read it stopped: the time went to starting
759
- ## PowerShell, loading UI Automation, the walks, or after the last walk, and which tells a loaded
760
- ## machine from a window that never answers.
766
+ ## PowerShell, loading UI Automation, the walks, after the last walk, or writing the answer, and
767
+ ## which tells a loaded machine from a window that never answers.
761
768
  static func _reached(progress: String, started_ms: int, stopped_ms: int) -> String:
762
769
  var marks: Array = _marks(progress)
763
770
  var at: Dictionary = {}
@@ -786,7 +793,7 @@ static func _reached(progress: String, started_ms: int, stopped_ms: int) -> Stri
786
793
  )
787
794
  % [began, loaded]
788
795
  )
789
- return (
796
+ var walked: String = (
790
797
  (
791
798
  "PowerShell began the client after %d ms and loaded UI Automation in %d ms, and the "
792
799
  + "windows were walked %d time%s, the last finding %s element%s and ending %d ms "
@@ -802,6 +809,10 @@ static func _reached(progress: String, started_ms: int, stopped_ms: int) -> Stri
802
809
  stopped_ms - last_at,
803
810
  ]
804
811
  )
812
+ if not at.has("answering"):
813
+ return walked
814
+ var answering: int = stopped_ms - at["answering"]
815
+ return walked + ", which had been writing the answer for %d ms" % answering
805
816
 
806
817
 
807
818
  ## Where the time of a client started at [param started_ms] and done at [param done_ms] went, by
@@ -281,6 +281,8 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
281
281
  var by_kind: Dictionary = {}
282
282
  var total: int = 0
283
283
  var in_code: int = 0
284
+ var shares_uid: Array[String] = []
285
+ var target_uid: String = target["uid"]
284
286
 
285
287
  for file_path: String in all_files:
286
288
  # Its own sidecar names it as the file it was imported from, which is not a use of it.
@@ -305,6 +307,11 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
305
307
  var references: Array[Dictionary] = []
306
308
  for i: int in range(lines.size()):
307
309
  var line: String = lines[i]
310
+ # A file declaring the target's uid as its own is not using it: a resource copied with
311
+ # its header claims the same uid, and its header line was answered as a use by path.
312
+ if not target_uid.is_empty() and _own_uid(line, i, file_path) == target_uid:
313
+ shares_uid.append(file_path)
314
+ continue
308
315
  var by_its_path: bool = _names_target(line, file_path, target)
309
316
  var by_its_name: bool = false
310
317
  var name_in_comment: bool = false
@@ -408,9 +415,30 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
408
415
  }
409
416
  if not autoload_named.is_empty():
410
417
  answer["autoload"] = autoload_named
418
+ if not shares_uid.is_empty():
419
+ answer["shares_uid"] = shares_uid
411
420
  return answer
412
421
 
413
422
 
423
+ # The uid [param line], line [param index] of [param file_path], declares as the file's own: a
424
+ # scene's or a resource's header, or an import sidecar's uid of the file it imports; empty for any
425
+ # other line.
426
+ static func _own_uid(line: String, index: int, file_path: String) -> String:
427
+ var trimmed: String = line.strip_edges()
428
+ var declares: bool = (
429
+ (
430
+ index == 0
431
+ and file_path.get_extension() in ["tscn", "tres"]
432
+ and (trimmed.begins_with("[gd_scene") or trimmed.begins_with("[gd_resource"))
433
+ )
434
+ or (file_path.get_extension() == "import" and trimmed.begins_with('uid="'))
435
+ )
436
+ if not declares:
437
+ return ""
438
+ var found: RegExMatch = GdhPatterns.compiled('uid="(uid://[^"]+)"').search(trimmed)
439
+ return "" if found == null else found.get_string(1)
440
+
441
+
414
442
  # The class_name a script declares, or empty for a scene, a resource or a script without one.
415
443
  func _declared_class_name(path: String) -> String:
416
444
  if not path.ends_with(".gd"):
package/build/keeper.js CHANGED
@@ -8833,20 +8833,29 @@ function holderOf(content) {
8833
8833
  if (typeof parsed !== "object" || parsed === null) {
8834
8834
  return null;
8835
8835
  }
8836
- const { pid, started, by, game } = parsed;
8837
- if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || typeof started !== "number") {
8836
+ const { by, game, engine } = parsed;
8837
+ const named = namedProcess(parsed);
8838
+ if (named === null) {
8838
8839
  return null;
8839
8840
  }
8841
+ const runs = namedProcess(engine);
8840
8842
  return {
8841
- pid,
8842
- started,
8843
+ ...named,
8843
8844
  ...typeof by === "string" && by !== "" ? { by } : {},
8844
- ...game === true ? { game } : {}
8845
+ ...game === true ? { game } : {},
8846
+ ...runs === null ? {} : { engine: runs }
8845
8847
  };
8846
8848
  } catch {
8847
8849
  return null;
8848
8850
  }
8849
8851
  }
8852
+ function namedProcess(value) {
8853
+ if (typeof value !== "object" || value === null) {
8854
+ return null;
8855
+ }
8856
+ const { pid, started } = value;
8857
+ return typeof pid === "number" && Number.isInteger(pid) && pid > 0 && typeof started === "number" ? { pid, started } : null;
8858
+ }
8850
8859
  async function ownStart() {
8851
8860
  const said = (await startTimesOf([process4.pid])).get(process4.pid);
8852
8861
  return said ?? Math.round(Date.now() - process4.uptime() * 1000);
@@ -8861,6 +8870,16 @@ function handSlot(lock, from, to) {
8861
8870
  const holder = content === null ? null : holderOf(content);
8862
8871
  return holder?.pid === from && writeWhole(lock, JSON.stringify(holder.game === true ? { ...to, game: true } : to));
8863
8872
  }
8873
+ function nameSlotEngine(lock, holder, engine) {
8874
+ let content;
8875
+ try {
8876
+ content = readWhole(lock);
8877
+ } catch {
8878
+ return false;
8879
+ }
8880
+ const named = content === null ? null : holderOf(content);
8881
+ return named?.pid === holder && writeWhole(lock, JSON.stringify({ ...named, engine }));
8882
+ }
8864
8883
  function giveBackSlot(lock, pid) {
8865
8884
  try {
8866
8885
  if (holderOf(readFileSync5(lock, "utf8"))?.pid === pid) {
@@ -11067,10 +11086,12 @@ async function keep() {
11067
11086
  });
11068
11087
  const slot = run?.slot;
11069
11088
  if (slot !== undefined && keeperStarted !== undefined) {
11089
+ const gameStarted = (await startTimesOf([pid])).get(pid);
11070
11090
  handSlot(slot.lock, slot.from, {
11071
11091
  pid: process.pid,
11072
11092
  started: await keeperStarted,
11073
- by: `${slot.by}, through the keeper process that runs it`
11093
+ by: `${slot.by}, through the keeper process that runs it`,
11094
+ ...gameStarted === undefined ? {} : { engine: { pid, started: gameStarted } }
11074
11095
  });
11075
11096
  }
11076
11097
  let { code, signal } = await exited;
@@ -11106,6 +11127,9 @@ async function keep() {
11106
11127
  restartedFrom: [...note.restartedFrom ?? [], before]
11107
11128
  };
11108
11129
  writeNoteIfItCan(note);
11130
+ if (slot !== undefined) {
11131
+ nameSlotEngine(slot.lock, process.pid, { pid: next.pid, started: next.startedAt });
11132
+ }
11109
11133
  end = () => {
11110
11134
  try {
11111
11135
  process.kill(next.pid);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.3.56",
3
+ "version": "1.3.58",
4
4
  "mcpName": "io.github.Aureliolo/gdharness",
5
5
  "description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
6
6
  "type": "module",