gdharness 1.3.8 → 1.3.10

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.
Files changed (78) hide show
  1. package/build/cli.js +4668 -3229
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +115 -4
  3. package/build/godot/addons/gdharness_editor/declared_values.gd +272 -0
  4. package/build/godot/addons/gdharness_editor/file_reads.gd +115 -18
  5. package/build/godot/addons/gdharness_editor/headless_check.gd +49 -2
  6. package/build/godot/addons/gdharness_editor/property_paths.gd +89 -16
  7. package/build/godot/addons/gdharness_editor/property_values.gd +180 -121
  8. package/build/godot/addons/gdharness_editor/reading.gd +112 -6
  9. package/build/godot/addons/gdharness_editor/scene_file.gd +117 -174
  10. package/build/godot/addons/gdharness_editor/scene_uids.gd +107 -0
  11. package/build/godot/addons/gdharness_editor/scene_uids.gd.uid +1 -0
  12. package/build/godot/addons/gdharness_editor/serialisation.gd +19 -29
  13. package/build/godot/addons/gdharness_editor/setting_reads.gd +121 -0
  14. package/build/godot/addons/gdharness_editor/setting_reads.gd.uid +1 -0
  15. package/build/godot/addons/gdharness_editor/tool_executor.gd +39 -6
  16. package/build/godot/addons/gdharness_editor/tools/animation_states.gd +263 -0
  17. package/build/godot/addons/gdharness_editor/tools/animation_states.gd.uid +1 -0
  18. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +49 -414
  19. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +464 -0
  20. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd.uid +1 -0
  21. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +37 -0
  22. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +221 -0
  23. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd.uid +1 -0
  24. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +162 -483
  25. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -0
  26. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +8 -4
  27. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +2 -1
  28. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +10 -0
  29. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +151 -8
  30. package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +7 -30
  31. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +190 -38
  32. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +485 -0
  33. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd.uid +1 -0
  34. package/build/godot/addons/gdharness_editor/value_shapes.gd +128 -1
  35. package/build/godot/addons/gdharness_runtime/call_errors.gd +8 -2
  36. package/build/godot/addons/gdharness_runtime/declared_values.gd +272 -0
  37. package/build/godot/addons/gdharness_runtime/reading.gd +112 -6
  38. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +35 -3
  39. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +5 -0
  40. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +188 -30
  41. package/build/godot/addons/gdharness_runtime/runtime_input.gd +107 -104
  42. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +17 -0
  43. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +13 -4
  44. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +320 -27
  45. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +165 -20
  46. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +49 -3
  47. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +31 -2
  48. package/build/godot/addons/gdharness_runtime/runtime_values.gd +250 -10
  49. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +279 -11
  50. package/build/godot/addons/gdharness_runtime/runtime_words.gd +27 -0
  51. package/build/godot/addons/gdharness_runtime/serialisation.gd +19 -29
  52. package/build/godot/addons/gdharness_runtime/value_shapes.gd +128 -1
  53. package/build/godot/operations/animation_reads.gd +115 -4
  54. package/build/godot/operations/audio_buses.gd +93 -7
  55. package/build/godot/operations/classdb_queries.gd +119 -7
  56. package/build/godot/operations/declared_values.gd +272 -0
  57. package/build/godot/operations/dependencies.gd +154 -41
  58. package/build/godot/operations/editor_tools.gd +29 -13
  59. package/build/godot/operations/export_presets.gd +602 -4
  60. package/build/godot/operations/file_reads.gd +115 -18
  61. package/build/godot/operations/gdscript_analysis.gd +14 -0
  62. package/build/godot/operations/gdscript_authoring.gd +12 -2
  63. package/build/godot/operations/godot_operations.gd +18 -2
  64. package/build/godot/operations/import_pipeline.gd +20 -221
  65. package/build/godot/operations/input_actions.gd +127 -51
  66. package/build/godot/operations/project_config.gd +113 -68
  67. package/build/godot/operations/project_diagnostics.gd +4 -0
  68. package/build/godot/operations/project_validation.gd +266 -0
  69. package/build/godot/operations/property_paths.gd +89 -16
  70. package/build/godot/operations/property_values.gd +180 -121
  71. package/build/godot/operations/reading.gd +112 -6
  72. package/build/godot/operations/scene_connections.gd +222 -0
  73. package/build/godot/operations/serialisation.gd +19 -29
  74. package/build/godot/operations/setting_reads.gd +121 -0
  75. package/build/godot/operations/value_shapes.gd +128 -1
  76. package/build/godot/tests/listed.gd +20 -13
  77. package/build/keeper.js +1402 -707
  78. package/package.json +1 -1
package/build/keeper.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/keeper.ts
4
- import { closeSync as closeSync2, existsSync as existsSync3, mkdtempSync as mkdtempSync4, openSync as openSync3, readFileSync as readFileSync3, rmSync as rmSync6, writeFileSync as writeFileSync3 } from "node:fs";
4
+ import { closeSync as closeSync2, existsSync as existsSync3, mkdtempSync as mkdtempSync4, openSync as openSync3, readFileSync as readFileSync6, rmSync as rmSync6, writeFileSync as writeFileSync3 } from "node:fs";
5
5
  import { tmpdir as tmpdir5 } from "node:os";
6
- import { join as join7 } from "node:path";
7
- import { setTimeout as delay2 } from "node:timers/promises";
6
+ import { join as join9 } from "node:path";
7
+ import { setTimeout as delay3 } from "node:timers/promises";
8
8
 
9
9
  // src/alive.ts
10
10
  import process2 from "node:process";
@@ -60,6 +60,18 @@ function spawnHidden(command, args, options = {}) {
60
60
  function spawnShown(command, args, options = {}) {
61
61
  return spawn(command, [...args], { ...options, windowsHide: false });
62
62
  }
63
+ function runHidden(command, args, options = {}) {
64
+ return new Promise((resolve, reject) => {
65
+ execFile(command, [...args], { ...options, encoding: "utf8", windowsHide: true }, (error, stdout, stderr) => {
66
+ if (error !== null) {
67
+ Object.assign(error, { stdout, stderr });
68
+ reject(error);
69
+ return;
70
+ }
71
+ resolve({ stdout, stderr });
72
+ });
73
+ });
74
+ }
63
75
 
64
76
  // src/desktop.ts
65
77
  var DESKTOP_VARIABLE = "GDHARNESS_DESKTOP";
@@ -163,10 +175,25 @@ public static class GdharnessDesktop {
163
175
  // it did not start itself: a handle names one process, where a pid can name the next one too. The
164
176
  // process starts in directory rather than in this one's, which is a temporary directory: Windows
165
177
  // PowerShell saves its module cache relative to where it runs when the profile does not resolve.
166
- public static int Run(string desktop, string commandLine, string pidFile, bool noActivate, string stopFile, string directory) {
178
+ // The spec is one field a line, each base64 of its UTF-8, read with no PowerShell cmdlet: a cmdlet
179
+ // has PowerShell prepare its modules first, on every start.
180
+ public static int RunSpec(string specFile) {
181
+ string[] lines = File.ReadAllLines(specFile);
182
+ Func<int, string> field = delegate(int index) { return Encoding.UTF8.GetString(Convert.FromBase64String(lines[index])); };
183
+ return Run(field(0), field(1), field(2), field(3) == "1", field(4), field(5), field(6));
184
+ }
185
+
186
+ static void Step(string stepFile, string step) {
187
+ if (stepFile.Length > 0) {
188
+ File.WriteAllText(stepFile, step);
189
+ }
190
+ }
191
+
192
+ public static int Run(string desktop, string commandLine, string pidFile, bool noActivate, string stopFile, string directory, string stepFile) {
167
193
  StartupInfo startup = new StartupInfo();
168
194
  startup.cb = Marshal.SizeOf(typeof(StartupInfo));
169
195
  if (desktop.Length > 0) {
196
+ Step(stepFile, "creating the " + desktop + " desktop");
170
197
  IntPtr held = CreateDesktopW(desktop, IntPtr.Zero, IntPtr.Zero, 0, GenericAll, IntPtr.Zero);
171
198
  if (held == IntPtr.Zero) {
172
199
  throw new Win32Exception(Marshal.GetLastWin32Error(), "CreateDesktopW " + desktop);
@@ -191,6 +218,7 @@ public static class GdharnessDesktop {
191
218
  LockSetForegroundWindow(LockForeground);
192
219
  }
193
220
  try {
221
+ Step(stepFile, "starting the process");
194
222
  if (!CreateProcessW(null, new StringBuilder(commandLine), IntPtr.Zero, IntPtr.Zero, true, DetachedProcess, IntPtr.Zero, directory, ref startup, out started)) {
195
223
  throw new Win32Exception(Marshal.GetLastWin32Error(), "CreateProcessW " + commandLine);
196
224
  }
@@ -330,24 +358,40 @@ function quoted(argument) {
330
358
  function windowsCommandLine(command, args) {
331
359
  return [command, ...args].map(quoted).join(" ");
332
360
  }
361
+ function helperFailure(file) {
362
+ try {
363
+ const said = readFileSync(file, "utf8").trim();
364
+ return said === "" ? null : said;
365
+ } catch {
366
+ return null;
367
+ }
368
+ }
369
+ function helperStep(stepFile) {
370
+ let step = null;
371
+ try {
372
+ step = readFileSync(stepFile, "utf8").trim();
373
+ } catch {}
374
+ return step === null || step === "" ? "Windows PowerShell had not begun running the helper" : `the helper was ${step}`;
375
+ }
333
376
  function throughHelper(how, command, args, files, assembly = helperAssembly()) {
334
- const spec = {
335
- desktop: how.desktop ?? "",
336
- noActivate: how.noActivate === true,
337
- commandLine: windowsCommandLine(command, args),
338
- pidFile: files.pid,
339
- stopFile: files.stop,
340
- directory: how.directory,
341
- assembly
342
- };
343
- writeFileSync(files.spec, JSON.stringify(spec), "utf8");
377
+ const fields = [
378
+ how.desktop ?? "",
379
+ windowsCommandLine(command, args),
380
+ files.pid,
381
+ how.noActivate === true ? "1" : "0",
382
+ files.stop,
383
+ how.directory,
384
+ files.step
385
+ ];
386
+ writeFileSync(files.spec, fields.map((field) => Buffer.from(field, "utf8").toString("base64")).join(`
387
+ `), "utf8");
344
388
  const literal = (path) => `'${path.replaceAll("'", "''")}'`;
345
389
  const script = [
346
390
  "$ErrorActionPreference = 'Stop'",
347
391
  "try {",
348
- ` $spec = [IO.File]::ReadAllText(${literal(files.spec)}, [Text.Encoding]::UTF8) | ConvertFrom-Json`,
349
- " Add-Type -LiteralPath $spec.assembly",
350
- " $code = [GdharnessDesktop]::Run($spec.desktop, $spec.commandLine, $spec.pidFile, [bool]$spec.noActivate, $spec.stopFile, $spec.directory)",
392
+ ` [IO.File]::WriteAllText(${literal(files.step)}, 'loading its assembly')`,
393
+ ` $null = [Reflection.Assembly]::LoadFrom(${literal(assembly)})`,
394
+ ` $code = [GdharnessDesktop]::RunSpec(${literal(files.spec)})`,
351
395
  "} catch {",
352
396
  ` [IO.File]::WriteAllText(${literal(files.error)}, $_.Exception.Message)`,
353
397
  " exit 1",
@@ -396,7 +440,9 @@ function crashesReported() {
396
440
  }
397
441
 
398
442
  // src/game-debugger.ts
443
+ import { readFileSync as readFileSync3 } from "node:fs";
399
444
  import { createServer } from "node:net";
445
+ import { join as join5 } from "node:path";
400
446
 
401
447
  // src/godot-variant.ts
402
448
  var NIL = 0;
@@ -700,185 +746,636 @@ function readMessage(body) {
700
746
  return { message, thread, data };
701
747
  }
702
748
 
703
- // src/game-debugger.ts
704
- var PAUSE_MS = 30000;
705
- var STEP_MS = 1e4;
706
- var ADDON_STEP_MS = 5000;
707
- var RUNTIME_ADDON = "res://addons/gdharness_runtime/";
708
- var READ_MS = 5000;
709
- var REPLIES = new Set([
710
- "debug_enter",
711
- "debug_exit",
712
- "stack_dump",
713
- "stack_frame_vars",
714
- "stack_frame_var"
749
+ // src/setup.ts
750
+ import { dirname as dirname2, join as join4 } from "node:path";
751
+ import { fileURLToPath } from "node:url";
752
+
753
+ // src/gdscript-source.ts
754
+ var DECLARING = new Set(["var", "const", "func", "signal", "enum", "class", "for", "class_name"]);
755
+ var NAME = "[\\p{L}_][\\p{L}\\p{N}_]*";
756
+ var ANNOTATIONS = "(?:@[\\p{L}_][\\p{L}\\p{N}_]*(?:\\([^)]*\\))?\\s+)*";
757
+ var DECLARATION_LINE = new RegExp(`^(${ANNOTATIONS})(static\\s+)?(func|var|const|signal|enum|class)\\s+(${NAME})`, "u");
758
+ var CLASS_NAME_LINE = new RegExp(`^${ANNOTATIONS}class_name\\s+(${NAME})`, "u");
759
+ var EXTENDS_LINE = new RegExp(`^${ANNOTATIONS}(?:class_name\\s+${NAME}\\s+)?extends\\s`, "u");
760
+ var FUNC_LINE = new RegExp(`^[ \\t]*${ANNOTATIONS}(?:static\\s+)?func\\s+(${NAME})\\s*\\(`, "u");
761
+ var LAMBDA_HEADER = new RegExp(`(?<![\\p{L}\\p{N}_])func(?:\\s+${NAME})?\\s*\\(`, "u");
762
+ var DOTTED = `${NAME}(?:\\.${NAME})*`;
763
+ var MEMBER_KINDS = new Set(["var", "const", "signal", "enum", "enumValue"]);
764
+ var KEYWORDS = new Set([
765
+ "and",
766
+ "as",
767
+ "assert",
768
+ "await",
769
+ "break",
770
+ "breakpoint",
771
+ "class",
772
+ "class_name",
773
+ "const",
774
+ "continue",
775
+ "elif",
776
+ "else",
777
+ "enum",
778
+ "extends",
779
+ "false",
780
+ "for",
781
+ "func",
782
+ "if",
783
+ "in",
784
+ "is",
785
+ "match",
786
+ "namespace",
787
+ "not",
788
+ "null",
789
+ "or",
790
+ "pass",
791
+ "preload",
792
+ "return",
793
+ "self",
794
+ "signal",
795
+ "static",
796
+ "super",
797
+ "trait",
798
+ "true",
799
+ "var",
800
+ "void",
801
+ "when",
802
+ "while",
803
+ "yield",
804
+ "INF",
805
+ "NAN",
806
+ "PI",
807
+ "TAU"
715
808
  ]);
716
- var INBOX_KEPT = 1e4;
717
- var OUTPUT_KEPT = 1000;
718
- var TYPE_NAMES = [
719
- "Nil",
720
- "bool",
721
- "int",
722
- "float",
723
- "String",
724
- "Vector2",
725
- "Vector2i",
726
- "Rect2",
727
- "Rect2i",
728
- "Vector3",
729
- "Vector3i",
730
- "Transform2D",
731
- "Vector4",
732
- "Vector4i",
733
- "Plane",
734
- "Quaternion",
735
- "AABB",
736
- "Basis",
737
- "Transform3D",
738
- "Projection",
739
- "Color",
740
- "StringName",
741
- "NodePath",
742
- "RID",
743
- "Object",
744
- "Callable",
745
- "Signal",
746
- "Dictionary",
747
- "Array",
748
- "PackedByteArray",
749
- "PackedInt32Array",
750
- "PackedInt64Array",
751
- "PackedFloat32Array",
752
- "PackedFloat64Array",
753
- "PackedStringArray",
754
- "PackedVector2Array",
755
- "PackedVector3Array",
756
- "PackedColorArray",
757
- "PackedVector4Array"
758
- ];
759
- var SCOPES = ["Locals", "Members", "Globals"];
760
- var ELEMENTS_SHOWN = 20;
761
- function isList(value) {
762
- return Array.isArray(value);
763
- }
764
- function shownValue(value, typeHint = "") {
765
- if (value === null) {
766
- return "null";
767
- }
768
- if (typeof value === "string") {
769
- return value;
770
- }
771
- if (typeof value === "number" || typeof value === "boolean") {
772
- return String(value);
773
- }
774
- if (isList(value)) {
775
- const shown = value.slice(0, ELEMENTS_SHOWN).map((each) => shownValue(each));
776
- const more = value.length - shown.length;
777
- return `[${shown.join(", ")}${more > 0 ? `, and ${more} more` : ""}]`;
778
- }
779
- if (value.kind === "dictionary" && "entries" in value) {
780
- const shown = value.entries.slice(0, ELEMENTS_SHOWN).map(([key, each]) => `${shownValue(key)}: ${shownValue(each)}`);
781
- const more = value.entries.length - shown.length;
782
- return `{${shown.join(", ")}${more > 0 ? `, and ${more} more` : ""}}`;
783
- }
784
- if (!("values" in value)) {
785
- return value.kind;
786
- }
787
- if (value.kind === "Object") {
788
- return `<${typeHint === "" ? "Object" : typeHint}#${String(value.values[0])}>`;
789
- }
790
- return `${value.kind}(${value.values.join(", ")})`;
791
- }
792
- function framesOf(data) {
793
- const frames = [];
794
- for (let at = 1;at + 2 < data.length; at += 3) {
795
- const [script, line, name] = [data[at], data[at + 1], data[at + 2]];
796
- if (typeof script !== "string" || typeof line !== "number" || typeof name !== "string") {
797
- break;
798
- }
799
- frames.push({ id: frames.length, name, line, script });
800
- }
801
- return frames;
809
+
810
+ // src/dictionary.ts
811
+ function emptyRecord() {
812
+ return Object.create(null);
802
813
  }
803
814
 
804
- class GameDebugger {
805
- connection = null;
806
- held = null;
807
- ended = false;
808
- waiters = [];
809
- inbox = [];
810
- onClosed = [];
811
- output = [];
812
- outputDropped = 0;
813
- queue = Promise.resolve();
814
- server;
815
- port;
816
- breakpoints;
817
- observers;
818
- constructor(server, port, breakpoints, observers) {
819
- this.server = server;
820
- this.port = port;
821
- this.breakpoints = breakpoints;
822
- this.observers = observers;
823
- }
824
- static async open(breakpoints, observers = []) {
825
- const server = createServer();
826
- await new Promise((resolve, reject) => {
827
- server.once("error", reject);
828
- server.listen(0, "127.0.0.1", () => {
829
- resolve();
830
- });
831
- });
832
- const address = server.address();
833
- if (address === null || typeof address === "string") {
834
- server.close();
835
- throw new Error("the debugger has no port");
836
- }
837
- const peer = new GameDebugger(server, address.port, breakpoints, observers);
838
- server.on("connection", (socket) => {
839
- peer.accept(socket);
840
- });
841
- return peer;
842
- }
843
- get engineArguments() {
844
- const listed = this.breakpoints.map((one) => one.replaceAll(" ", "%20")).join(",");
845
- return [
846
- "--remote-debug",
847
- `tcp://127.0.0.1:${this.port}`,
848
- "--ignore-error-breaks",
849
- ...listed === "" ? [] : ["--breakpoints", listed]
850
- ];
851
- }
852
- accept(socket) {
853
- if (this.connection !== null) {
854
- socket.destroy();
855
- return;
856
- }
857
- this.connection = socket;
858
- this.server.close();
859
- const send = (message, data) => {
860
- this.send(message, data);
861
- };
862
- for (const observer of this.observers) {
863
- observer.connected(send);
864
- }
865
- const stream = new DebuggerStream;
866
- socket.on("data", (chunk) => {
867
- let messages;
868
- try {
869
- messages = stream.push(chunk);
870
- } catch {
871
- socket.destroy();
872
- return;
873
- }
874
- for (const received of messages) {
875
- if ("message" in received) {
876
- this.take(received);
877
- for (const observer of this.observers) {
878
- observer.message(received, send);
879
- }
880
- }
881
- }
815
+ // src/call-signal.ts
816
+ import { AsyncLocalStorage } from "node:async_hooks";
817
+ var current = new AsyncLocalStorage;
818
+
819
+ // src/game-log.ts
820
+ var COLOUR_CODE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
821
+ var ADMITTED = {
822
+ error: new Set(["error"]),
823
+ warning: new Set(["error", "warning"]),
824
+ info: new Set(["error", "warning", "info"])
825
+ };
826
+
827
+ // src/engine-run.ts
828
+ var WINDOWS_STATUSES = new Map([
829
+ [3221225477, "an access violation, so it crashed"],
830
+ [3221225501, "an illegal instruction, so it crashed"],
831
+ [3221225620, "an integer division by zero, so it crashed"],
832
+ [3221225725, "a stack overflow, so it crashed"],
833
+ [3221225781, "a library it needs was not found, so it never started"],
834
+ [3221225786, "its console was closed or sent Ctrl+C"],
835
+ [3221226356, "a corrupted heap, so it crashed"],
836
+ [3221226505, "a stack buffer overrun or a fast fail, so it crashed"]
837
+ ]);
838
+
839
+ // src/engine-leaks.ts
840
+ var NAME2 = "[\\p{L}_][\\p{L}\\p{N}_]*";
841
+ var EXPORTED = new RegExp(`@export(?!_(?:group|subgroup|category)\\b)\\w*(?:\\([^)]*\\))?\\s+(?:@${NAME2}(?:\\([^)]*\\))?\\s+)*` + `(?:static\\s+)?var\\s+(${NAME2})\\s*:\\s*(?:Array|Dictionary)\\s*\\[([^\\]]*)\\]`, "gu");
842
+
843
+ // src/project-scan.ts
844
+ var SKIPPED = new Set(["node_modules"]);
845
+ var SCENE_EXTENSIONS = new Set(["tscn", "scn"]);
846
+ var SIDECAR_EXTENSIONS = new Set(["import", "uid"]);
847
+ var ASSET_EXTENSIONS = new Set([
848
+ "png",
849
+ "jpg",
850
+ "jpeg",
851
+ "webp",
852
+ "svg",
853
+ "bmp",
854
+ "tga",
855
+ "exr",
856
+ "hdr",
857
+ "ktx",
858
+ "dds",
859
+ "ttf",
860
+ "otf",
861
+ "woff",
862
+ "woff2",
863
+ "fnt",
864
+ "wav",
865
+ "mp3",
866
+ "ogg",
867
+ "glb",
868
+ "gltf",
869
+ "obj",
870
+ "fbx",
871
+ "blend",
872
+ "dae",
873
+ "ogv",
874
+ "gdshader",
875
+ "gdshaderinc",
876
+ "shader",
877
+ "glsl"
878
+ ]);
879
+ var CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
880
+ var BINARY_EXTENSIONS = new Set([
881
+ ...[...ASSET_EXTENSIONS].filter((extension) => !["svg", "gltf", "gdshader", "shader", "fnt"].includes(extension)),
882
+ "scn",
883
+ "res",
884
+ "zip",
885
+ "pck",
886
+ "exe",
887
+ "dll",
888
+ "so",
889
+ "dylib"
890
+ ]);
891
+
892
+ // src/import-freshness.ts
893
+ var IMPORTABLE_EXTENSIONS = [
894
+ "png",
895
+ "jpg",
896
+ "jpeg",
897
+ "webp",
898
+ "svg",
899
+ "bmp",
900
+ "tga",
901
+ "exr",
902
+ "hdr",
903
+ "dds",
904
+ "ktx",
905
+ "wav",
906
+ "mp3",
907
+ "ogg",
908
+ "ttf",
909
+ "otf",
910
+ "ttc",
911
+ "otc",
912
+ "woff",
913
+ "woff2",
914
+ "pfb",
915
+ "pfm",
916
+ "fnt",
917
+ "font",
918
+ "glb",
919
+ "gltf",
920
+ "fbx",
921
+ "obj",
922
+ "dae",
923
+ "csv",
924
+ "glsl"
925
+ ];
926
+ var IMPORTABLE = new Set(IMPORTABLE_EXTENSIONS);
927
+ var hashes = new Map;
928
+
929
+ // src/paths.ts
930
+ import { realpathSync, statSync as statSync2 } from "node:fs";
931
+ import { basename, dirname, isAbsolute, join as join2, relative, resolve, sep } from "node:path";
932
+ function realAsFarAsItExists(path) {
933
+ const missing = [];
934
+ let at = path;
935
+ for (;; ) {
936
+ try {
937
+ return join2(realpathSync.native(at), ...missing);
938
+ } catch {
939
+ const parent = dirname(at);
940
+ if (parent === at) {
941
+ return path;
942
+ }
943
+ missing.unshift(basename(at));
944
+ at = parent;
945
+ }
946
+ }
947
+ }
948
+
949
+ // src/scratch.ts
950
+ import { existsSync as existsSync2, mkdtempSync as mkdtempSync2, readdirSync, rmSync as rmSync2 } from "node:fs";
951
+ import { tmpdir as tmpdir2 } from "node:os";
952
+ import { join as join3 } from "node:path";
953
+ import process4 from "node:process";
954
+ var SWEPT = { recursive: true, force: true };
955
+ var AGAIN_AFTER_MS = [250, 500, 1000, 2000, 4000, 8000, 16000];
956
+ var waiting = new Set;
957
+ var atExit = new Set;
958
+ function scratchDirectory(kind) {
959
+ return mkdtempSync2(join3(tmpdir2(), `gdharness-${kind}-${process4.pid}-`));
960
+ }
961
+ function discardWith(remove, paths) {
962
+ const left = [];
963
+ for (const path of paths) {
964
+ if (!path) {
965
+ continue;
966
+ }
967
+ try {
968
+ remove(path);
969
+ } catch (error) {
970
+ left.push({ path, reason: error instanceof Error ? error.message : String(error) });
971
+ }
972
+ }
973
+ return left;
974
+ }
975
+ function discard(...paths) {
976
+ const left = discardWith(remove, paths);
977
+ for (const { path } of left) {
978
+ tryAgain(path, 0);
979
+ }
980
+ return left;
981
+ }
982
+ function remove(path) {
983
+ rmSync2(path, SWEPT);
984
+ }
985
+ function tryAgain(path, attempt) {
986
+ if (attempt === 0) {
987
+ if (waiting.has(path)) {
988
+ return;
989
+ }
990
+ waiting.add(path);
991
+ atExit.delete(path);
992
+ lastTryOnExit();
993
+ }
994
+ const after = AGAIN_AFTER_MS[attempt];
995
+ if (after === undefined || !existsSync2(path)) {
996
+ waiting.delete(path);
997
+ if (after === undefined) {
998
+ atExit.add(path);
999
+ }
1000
+ return;
1001
+ }
1002
+ setTimeout(() => {
1003
+ try {
1004
+ remove(path);
1005
+ waiting.delete(path);
1006
+ } catch {
1007
+ tryAgain(path, attempt + 1);
1008
+ }
1009
+ }, after).unref();
1010
+ }
1011
+ var lastTryWatched = false;
1012
+ function lastTryOnExit() {
1013
+ if (lastTryWatched) {
1014
+ return;
1015
+ }
1016
+ lastTryWatched = true;
1017
+ process4.once("exit", () => {
1018
+ for (const path of [...waiting, ...atExit]) {
1019
+ try {
1020
+ remove(path);
1021
+ } catch {}
1022
+ }
1023
+ });
1024
+ }
1025
+
1026
+ // src/junit.ts
1027
+ var ESCAPE = String.fromCharCode(27);
1028
+ var LOCATION = `'[^'\\n]*' in \\S+:\\d+`;
1029
+ var IGNORING_CASE = " \\(ignoring case\\)";
1030
+ var PRINTED_ACTUAL = new RegExp(` but was\\n '((?:(?!\\tat ${LOCATION})[\\s\\S])*?)'((?:${ESCAPE}\\[[0-9;]*m)*)(${IGNORING_CASE})?((?:${ESCAPE}\\[[0-9;]*m)*)\\tat (${LOCATION})`, "g");
1031
+ var QUOTED_EQUALITY = new RegExp(`^Expecting(?: error message)?:\\n '([\\s\\S]*)'\\n but was\\n '([\\s\\S]*)'(?:${IGNORING_CASE})?\\n\\tat `);
1032
+ var COLOUR = new RegExp(`${ESCAPE}\\[([0-9;]*)m`, "g");
1033
+
1034
+ // src/answer-json.ts
1035
+ var SCALAR = String.raw`(?:-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null)`;
1036
+ var SCALAR_ARRAY = new RegExp(String.raw`\[\n\s+(${SCALAR}(?:,\n\s+${SCALAR})*)\n\s*\]`, "g");
1037
+
1038
+ // src/resources.ts
1039
+ var STATIC_RESOURCES = [
1040
+ {
1041
+ uri: "godot://project/info",
1042
+ name: "Project Info",
1043
+ description: "Parsed Godot project.godot metadata as JSON.",
1044
+ mimeType: "application/json"
1045
+ }
1046
+ ];
1047
+ var RESOURCE_TEMPLATES = [
1048
+ {
1049
+ uriTemplate: "godot://scene/{path}",
1050
+ name: "Scene File",
1051
+ description: "Read a Godot scene (.tscn) file from the current project.",
1052
+ mimeType: "text/plain"
1053
+ },
1054
+ {
1055
+ uriTemplate: "godot://script/{path}",
1056
+ name: "GDScript File",
1057
+ description: "Read a GDScript (.gd) file from the current project.",
1058
+ mimeType: "text/x-gdscript"
1059
+ },
1060
+ {
1061
+ uriTemplate: "godot://resource/{path}",
1062
+ name: "Resource File",
1063
+ description: "Read a project resource file (.tres, .tscn, .gd).",
1064
+ mimeType: "text/plain"
1065
+ }
1066
+ ];
1067
+ var RESOURCE_COUNT = STATIC_RESOURCES.length + RESOURCE_TEMPLATES.length;
1068
+ function isValueComplete(value) {
1069
+ let depth = 0;
1070
+ let inString = false;
1071
+ let escaped = false;
1072
+ for (const character of value) {
1073
+ if (escaped) {
1074
+ escaped = false;
1075
+ continue;
1076
+ }
1077
+ if (character === "\\") {
1078
+ escaped = true;
1079
+ continue;
1080
+ }
1081
+ if (character === '"') {
1082
+ inString = !inString;
1083
+ continue;
1084
+ }
1085
+ if (inString) {
1086
+ continue;
1087
+ }
1088
+ if (character === "{" || character === "[" || character === "(") {
1089
+ depth += 1;
1090
+ } else if (character === "}" || character === "]" || character === ")") {
1091
+ depth -= 1;
1092
+ }
1093
+ }
1094
+ return depth <= 0 && !inString;
1095
+ }
1096
+ function parseProjectGodot(content) {
1097
+ const result = emptyRecord();
1098
+ let section = emptyRecord();
1099
+ result["root"] = section;
1100
+ const lines = content.split(/\r?\n/);
1101
+ for (let index = 0;index < lines.length; index += 1) {
1102
+ const line = (lines[index] ?? "").trim();
1103
+ if (!line || line.startsWith(";") || line.startsWith("#")) {
1104
+ continue;
1105
+ }
1106
+ if (line.startsWith("[") && line.endsWith("]")) {
1107
+ const name = line.slice(1, -1).trim();
1108
+ section = result[name] ?? emptyRecord();
1109
+ result[name] = section;
1110
+ continue;
1111
+ }
1112
+ const eqIndex = line.indexOf("=");
1113
+ if (eqIndex === -1) {
1114
+ continue;
1115
+ }
1116
+ const key = line.slice(0, eqIndex).trim();
1117
+ let rawValue = line.slice(eqIndex + 1).trim();
1118
+ while (index + 1 < lines.length && !isValueComplete(rawValue)) {
1119
+ index += 1;
1120
+ rawValue += `
1121
+ ${(lines[index] ?? "").trim()}`;
1122
+ }
1123
+ section[key] = parseIniLikeValue(rawValue);
1124
+ }
1125
+ return result;
1126
+ }
1127
+ function parseIniLikeValue(value) {
1128
+ if (value === "null") {
1129
+ return null;
1130
+ }
1131
+ if (value === "true") {
1132
+ return true;
1133
+ }
1134
+ if (value === "false") {
1135
+ return false;
1136
+ }
1137
+ if (/^-?\d+$/.test(value)) {
1138
+ const parsed = Number.parseInt(value, 10);
1139
+ return Number.isSafeInteger(parsed) ? parsed : value;
1140
+ }
1141
+ if (/^-?\d*\.\d+$/.test(value)) {
1142
+ const parsed = Number.parseFloat(value);
1143
+ return Number.isFinite(parsed) ? parsed : value;
1144
+ }
1145
+ if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
1146
+ return value.slice(1, -1);
1147
+ }
1148
+ return value;
1149
+ }
1150
+
1151
+ // src/server-version.ts
1152
+ import { readFileSync as readFileSync2 } from "node:fs";
1153
+
1154
+ // src/update-check.ts
1155
+ var CACHE_MS = 10 * 60 * 1000;
1156
+ var MAX_BODY_BYTES = 1 << 20;
1157
+ var MAX_RETRY_MS = 60 * 60 * 1000;
1158
+
1159
+ // src/server-version.ts
1160
+ var DEBUG_MODE = process.env["GDHARNESS_DEBUG"] === "1";
1161
+ var SERVER_VERSION = (() => {
1162
+ try {
1163
+ const pkg = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf8"));
1164
+ return typeof pkg.version === "string" ? pkg.version : "0.0.0";
1165
+ } catch {
1166
+ return "0.0.0";
1167
+ }
1168
+ })();
1169
+
1170
+ // src/setup.ts
1171
+ function shippedAddonsDirectory() {
1172
+ return join4(dirname2(fileURLToPath(import.meta.url)), "godot", "addons");
1173
+ }
1174
+
1175
+ // src/game-debugger.ts
1176
+ var PAUSE_MS = 30000;
1177
+ var STEP_MS = 1e4;
1178
+ var ADDON_FRAMES = 5;
1179
+ var ADDON_LINES = 200;
1180
+ var RUNTIME_ADDON = "res://addons/gdharness_runtime/";
1181
+ var RUNTIME_AUTOLOAD = `${RUNTIME_ADDON}runtime_autoload.gd`;
1182
+ var frameEnd = null;
1183
+ function frameEndLine() {
1184
+ if (frameEnd === null) {
1185
+ const lines = readFileSync3(join5(shippedAddonsDirectory(), "gdharness_runtime", "runtime_autoload.gd"), "utf8").split(`
1186
+ `);
1187
+ const declared = lines.findIndex((line) => line.startsWith("func _notification("));
1188
+ if (declared === -1) {
1189
+ throw new Error(`The shipped ${RUNTIME_AUTOLOAD} declares no _notification.`);
1190
+ }
1191
+ frameEnd = declared + 2;
1192
+ }
1193
+ return frameEnd;
1194
+ }
1195
+ var READ_MS = 5000;
1196
+ var REPLIES = new Set([
1197
+ "debug_enter",
1198
+ "debug_exit",
1199
+ "stack_dump",
1200
+ "stack_frame_vars",
1201
+ "stack_frame_var",
1202
+ "evaluation_return",
1203
+ "scene:inspect_object"
1204
+ ]);
1205
+ var INBOX_KEPT = 1e4;
1206
+ var OUTPUT_KEPT = 1000;
1207
+ var TYPE_NAMES = [
1208
+ "Nil",
1209
+ "bool",
1210
+ "int",
1211
+ "float",
1212
+ "String",
1213
+ "Vector2",
1214
+ "Vector2i",
1215
+ "Rect2",
1216
+ "Rect2i",
1217
+ "Vector3",
1218
+ "Vector3i",
1219
+ "Transform2D",
1220
+ "Vector4",
1221
+ "Vector4i",
1222
+ "Plane",
1223
+ "Quaternion",
1224
+ "AABB",
1225
+ "Basis",
1226
+ "Transform3D",
1227
+ "Projection",
1228
+ "Color",
1229
+ "StringName",
1230
+ "NodePath",
1231
+ "RID",
1232
+ "Object",
1233
+ "Callable",
1234
+ "Signal",
1235
+ "Dictionary",
1236
+ "Array",
1237
+ "PackedByteArray",
1238
+ "PackedInt32Array",
1239
+ "PackedInt64Array",
1240
+ "PackedFloat32Array",
1241
+ "PackedFloat64Array",
1242
+ "PackedStringArray",
1243
+ "PackedVector2Array",
1244
+ "PackedVector3Array",
1245
+ "PackedColorArray",
1246
+ "PackedVector4Array"
1247
+ ];
1248
+ var SCOPES = ["Locals", "Members", "Globals"];
1249
+ var ELEMENTS_SHOWN = 20;
1250
+ function isList(value) {
1251
+ return Array.isArray(value);
1252
+ }
1253
+ function shownValue(value, typeHint = "") {
1254
+ if (value === null) {
1255
+ return "null";
1256
+ }
1257
+ if (typeof value === "string") {
1258
+ return value;
1259
+ }
1260
+ if (typeof value === "number" || typeof value === "boolean") {
1261
+ return String(value);
1262
+ }
1263
+ if (isList(value)) {
1264
+ const shown = value.slice(0, ELEMENTS_SHOWN).map((each) => shownValue(each));
1265
+ const more = value.length - shown.length;
1266
+ return `[${shown.join(", ")}${more > 0 ? `, and ${more} more` : ""}]`;
1267
+ }
1268
+ if (value.kind === "dictionary" && "entries" in value) {
1269
+ const shown = value.entries.slice(0, ELEMENTS_SHOWN).map(([key, each]) => `${shownValue(key)}: ${shownValue(each)}`);
1270
+ const more = value.entries.length - shown.length;
1271
+ return `{${shown.join(", ")}${more > 0 ? `, and ${more} more` : ""}}`;
1272
+ }
1273
+ if (!("values" in value)) {
1274
+ return value.kind;
1275
+ }
1276
+ if (value.kind === "Object") {
1277
+ return `<${typeHint === "" ? "Object" : typeHint}#${String(value.values[0])}>`;
1278
+ }
1279
+ return `${value.kind}(${value.values.join(", ")})`;
1280
+ }
1281
+ function typeNamed(type, hint) {
1282
+ if (typeof hint === "string" && hint !== "") {
1283
+ return hint;
1284
+ }
1285
+ return typeof type === "number" ? TYPE_NAMES[type] ?? String(type) : "Variant";
1286
+ }
1287
+ function framesOf(data) {
1288
+ const frames = [];
1289
+ for (let at = 1;at + 2 < data.length; at += 3) {
1290
+ const [script, line, name] = [data[at], data[at + 1], data[at + 2]];
1291
+ if (typeof script !== "string" || typeof line !== "number" || typeof name !== "string") {
1292
+ break;
1293
+ }
1294
+ frames.push({ id: frames.length, name, line, script });
1295
+ }
1296
+ return frames;
1297
+ }
1298
+
1299
+ class GameDebugger {
1300
+ connection = null;
1301
+ held = null;
1302
+ ended = false;
1303
+ waiters = [];
1304
+ inbox = [];
1305
+ onClosed = [];
1306
+ output = [];
1307
+ outputDropped = 0;
1308
+ queue = Promise.resolve();
1309
+ server;
1310
+ port;
1311
+ breakpoints;
1312
+ breakpointLines;
1313
+ observers;
1314
+ constructor(server, port, breakpoints, observers) {
1315
+ this.server = server;
1316
+ this.port = port;
1317
+ this.breakpoints = breakpoints;
1318
+ this.breakpointLines = new Set(breakpoints);
1319
+ this.observers = observers;
1320
+ }
1321
+ static async open(breakpoints, observers = []) {
1322
+ const server = createServer();
1323
+ await new Promise((resolve, reject) => {
1324
+ server.once("error", reject);
1325
+ server.listen(0, "127.0.0.1", () => {
1326
+ resolve();
1327
+ });
1328
+ });
1329
+ const address = server.address();
1330
+ if (address === null || typeof address === "string") {
1331
+ server.close();
1332
+ throw new Error("the debugger has no port");
1333
+ }
1334
+ const peer = new GameDebugger(server, address.port, breakpoints, observers);
1335
+ server.on("connection", (socket) => {
1336
+ peer.accept(socket);
1337
+ });
1338
+ return peer;
1339
+ }
1340
+ get engineArguments() {
1341
+ const listed = this.breakpoints.map((one) => one.replaceAll(" ", "%20")).join(",");
1342
+ return [
1343
+ "--remote-debug",
1344
+ `tcp://127.0.0.1:${this.port}`,
1345
+ "--ignore-error-breaks",
1346
+ ...listed === "" ? [] : ["--breakpoints", listed]
1347
+ ];
1348
+ }
1349
+ accept(socket) {
1350
+ if (this.connection !== null) {
1351
+ socket.destroy();
1352
+ return;
1353
+ }
1354
+ this.connection = socket;
1355
+ this.server.close();
1356
+ const send = (message, data) => {
1357
+ this.send(message, data);
1358
+ };
1359
+ for (const observer of this.observers) {
1360
+ observer.connected(send);
1361
+ }
1362
+ const stream = new DebuggerStream;
1363
+ socket.on("data", (chunk) => {
1364
+ let messages;
1365
+ try {
1366
+ messages = stream.push(chunk);
1367
+ } catch {
1368
+ socket.destroy();
1369
+ return;
1370
+ }
1371
+ for (const received of messages) {
1372
+ if ("message" in received) {
1373
+ this.take(received);
1374
+ for (const observer of this.observers) {
1375
+ observer.message(received, send);
1376
+ }
1377
+ }
1378
+ }
882
1379
  });
883
1380
  socket.on("error", () => {
884
1381
  return;
@@ -1010,8 +1507,13 @@ class GameDebugger {
1010
1507
  }
1011
1508
  switch (request.op) {
1012
1509
  case "breakpoint":
1510
+ if (request.set) {
1511
+ this.breakpointLines.add(`${request.script}:${String(request.line)}`);
1512
+ } else {
1513
+ this.breakpointLines.delete(`${request.script}:${String(request.line)}`);
1514
+ }
1013
1515
  this.send("breakpoint", [request.script, request.line, request.set], this.held?.thread ?? MAIN_THREAD);
1014
- return { sent: true };
1516
+ return { sent: true, taken: await this.taken() };
1015
1517
  case "pause":
1016
1518
  return await this.pause();
1017
1519
  case "resume":
@@ -1031,6 +1533,8 @@ class GameDebugger {
1031
1533
  }
1032
1534
  case "variables":
1033
1535
  return await this.variables(request.frame);
1536
+ case "evaluate":
1537
+ return await this.evaluate(request.frame, request.expression);
1034
1538
  default:
1035
1539
  return { refused: `Nothing here answers ${JSON.stringify(request)}.` };
1036
1540
  }
@@ -1087,30 +1591,77 @@ class GameDebugger {
1087
1591
  };
1088
1592
  }
1089
1593
  }
1090
- let frames = await this.stack();
1091
- const stepping = Date.now();
1092
- while (frames !== null && this.held !== null && (frames[0]?.script.startsWith(RUNTIME_ADDON) ?? false) && Date.now() - stepping < ADDON_STEP_MS) {
1093
- this.forget("debug_enter");
1094
- this.send("step", [], this.held.thread);
1095
- if (await this.next(GameDebugger.named("debug_enter"), STEP_MS) === null) {
1096
- return {
1097
- paused: false,
1098
- note: `The game stopped in gdharness's runtime addon and was stepped on to reach its own script, and did not stop again within ${STEP_MS / 1000}s. It stops at the next line of script it runs; debug_state stack reads it then.`
1099
- };
1100
- }
1101
- frames = await this.stack();
1594
+ const past = await this.pastTheAddon();
1595
+ if (past === "lost") {
1596
+ return {
1597
+ paused: false,
1598
+ note: `The game stopped in gdharness's runtime addon and was let go on to reach its own script, and did not stop again within ${STEP_MS / 1000}s. It stops at the next line of script it runs; debug_state stack reads it then.`
1599
+ };
1102
1600
  }
1103
1601
  this.stoppedBy("Paused");
1104
- const stack = frames ?? [];
1602
+ const stack = past.frames ?? [];
1105
1603
  return {
1106
1604
  paused: true,
1107
1605
  held: this.heldState(),
1108
1606
  stack,
1109
- ...stack[0]?.script.startsWith(RUNTIME_ADDON) === true ? {
1110
- note: `The game is held in gdharness's runtime addon, whose _process runs first in every frame: ${ADDON_STEP_MS / 1000}s of stepping reached no line of the game's own script, so it runs none from frame to frame. A breakpoint in the game's script stops it there.`
1607
+ ...past.gaveUp === "frames" ? {
1608
+ note: `The game is held in gdharness's runtime addon, whose _process runs first in every frame: ${ADDON_FRAMES} frames went by without a line of the game's own script, so it runs none from frame to frame. A breakpoint in the game's script stops it there.`
1609
+ } : past.gaveUp === "lines" ? {
1610
+ note: `The game is held in gdharness's runtime addon, which ran ${ADDON_LINES} lines outside its _process without reaching a line of the game's own script. debug_control step_over goes on from here, and a breakpoint in the game's script stops it there.`
1111
1611
  } : {}
1112
1612
  };
1113
1613
  }
1614
+ async pastTheAddon(until) {
1615
+ let frames = await this.stack();
1616
+ let framesLeft = ADDON_FRAMES;
1617
+ let linesLeft = ADDON_LINES;
1618
+ while (frames !== null && this.held !== null && (frames[0]?.script.startsWith(RUNTIME_ADDON) ?? false)) {
1619
+ if (until !== undefined && Date.now() >= until) {
1620
+ return { frames, gaveUp: "time" };
1621
+ }
1622
+ if (frames.some((frame) => frame.script === RUNTIME_AUTOLOAD && frame.name === "_process")) {
1623
+ if (until === undefined && framesLeft === 0) {
1624
+ return { frames, gaveUp: "frames" };
1625
+ }
1626
+ framesLeft -= 1;
1627
+ const reached = await this.toTheFrameEnd();
1628
+ if (reached === "lost") {
1629
+ return "lost";
1630
+ }
1631
+ frames = reached;
1632
+ continue;
1633
+ }
1634
+ if (until === undefined && linesLeft === 0) {
1635
+ return { frames, gaveUp: "lines" };
1636
+ }
1637
+ linesLeft -= 1;
1638
+ this.forget("debug_enter");
1639
+ this.send("step", [], this.held.thread);
1640
+ if (await this.next(GameDebugger.named("debug_enter"), STEP_MS) === null) {
1641
+ return "lost";
1642
+ }
1643
+ frames = await this.stack();
1644
+ }
1645
+ return { frames };
1646
+ }
1647
+ async toTheFrameEnd() {
1648
+ const held = this.held;
1649
+ if (held === null) {
1650
+ return null;
1651
+ }
1652
+ const line = frameEndLine();
1653
+ const theirs = this.breakpointLines.has(`${RUNTIME_AUTOLOAD}:${String(line)}`);
1654
+ if (!theirs) {
1655
+ this.send("breakpoint", [RUNTIME_AUTOLOAD, line, true], held.thread);
1656
+ }
1657
+ this.forget("debug_enter");
1658
+ this.send("continue", [], held.thread);
1659
+ const stopped = await this.next(GameDebugger.named("debug_enter"), STEP_MS);
1660
+ if (!theirs) {
1661
+ this.send("breakpoint", [RUNTIME_AUTOLOAD, line, false], this.held?.thread ?? MAIN_THREAD);
1662
+ }
1663
+ return stopped === null ? "lost" : await this.stack();
1664
+ }
1114
1665
  stoppedBy(cause) {
1115
1666
  if (this.held !== null) {
1116
1667
  this.held = { ...this.held, reason: cause };
@@ -1139,8 +1690,27 @@ class GameDebugger {
1139
1690
  note: `The game had not stopped again ${STEP_MS / 1000}s after the step: the step is still running, as one out of a function that never returns does, or it went on without reaching another line. debug_state stack answers once it stops, and debug_control pause stops it wherever it is.`
1140
1691
  };
1141
1692
  }
1693
+ const past = await this.pastTheAddon(Date.now() + STEP_MS);
1694
+ if (past === "lost") {
1695
+ return {
1696
+ stepped: true,
1697
+ heldAgain: false,
1698
+ note: `The step stopped in gdharness's runtime addon and was let go on to reach the game's own script, and did not stop again within ${STEP_MS / 1000}s. It stops at the next line of script the game runs; debug_state stack reads it then.`
1699
+ };
1700
+ }
1142
1701
  this.stoppedBy("Stepped");
1143
- return { stepped: true, heldAgain: true, held: this.heldState(), stack: await this.stack() ?? [] };
1702
+ const stack = past.frames ?? [];
1703
+ const top = stack[0];
1704
+ const onBreakpoint = top !== undefined && this.breakpointLines.has(`${top.script}:${String(top.line)}`);
1705
+ return {
1706
+ stepped: true,
1707
+ heldAgain: true,
1708
+ held: { ...this.heldState(), ...onBreakpoint ? { onBreakpoint: true } : {} },
1709
+ stack,
1710
+ ...past.gaveUp === undefined ? {} : {
1711
+ note: `The step went on through gdharness's runtime addon, whose _process runs first in every frame, for ${STEP_MS / 1000}s without the game running a line of its own script, and is held in the addon. debug_control continue lets the game go, and a breakpoint in its script stops it there.`
1712
+ }
1713
+ };
1144
1714
  }
1145
1715
  async variables(frame) {
1146
1716
  const held = this.held;
@@ -1151,10 +1721,9 @@ class GameDebugger {
1151
1721
  if (frames === null) {
1152
1722
  return { refused: `The game did not send its stack within ${READ_MS / 1000}s.` };
1153
1723
  }
1154
- if (frames.find((each) => each.id === frame) === undefined) {
1155
- return {
1156
- refused: frames.length === 0 ? "The game is held where no script runs, so it has no frames: debug_control step_over takes it to the next line of script." : `The stack has no frame ${frame}; its frames are ${frames.map((each) => each.id).join(", ")}.`
1157
- };
1724
+ const missing = GameDebugger.noFrame(frames, frame);
1725
+ if (missing !== null) {
1726
+ return missing;
1158
1727
  }
1159
1728
  this.forget("stack_frame_vars");
1160
1729
  this.forget("stack_frame_var");
@@ -1178,15 +1747,66 @@ class GameDebugger {
1178
1747
  };
1179
1748
  }
1180
1749
  const [name, scope, type, value, hint] = one.data;
1181
- const into = scopes[typeof scope === "number" ? scope : 0];
1182
- into?.variables.push({
1183
- name: typeof name === "string" ? name : shownValue(name ?? null),
1750
+ const named = typeof name === "string" ? name : shownValue(name ?? null);
1751
+ if (named.startsWith("@")) {
1752
+ continue;
1753
+ }
1754
+ scopes[typeof scope === "number" ? scope : 0]?.variables.push({
1755
+ name: named,
1184
1756
  value: shownValue(value ?? null, typeof hint === "string" ? hint : ""),
1185
- type: typeof hint === "string" && hint !== "" ? hint : typeof type === "number" ? TYPE_NAMES[type] ?? String(type) : "Variant"
1757
+ type: typeNamed(type, hint)
1186
1758
  });
1187
1759
  }
1188
1760
  return { held: this.heldState(), frame, scopes };
1189
1761
  }
1762
+ async evaluate(frame, expression) {
1763
+ const held = this.held;
1764
+ if (held === null) {
1765
+ return this.notHeld();
1766
+ }
1767
+ const read = await this.variables(frame);
1768
+ if (!Array.isArray(read["scopes"])) {
1769
+ return read;
1770
+ }
1771
+ const members = read["scopes"].find((scope) => scope.name === "Members");
1772
+ if (members?.variables.some((variable) => variable.name === "self") !== true) {
1773
+ return {
1774
+ refused: `Frame ${frame} has no instance of its script to evaluate on, as a static function called on its class has none, and the engine lets the game go on rather than answer one there, so it was not asked. Evaluate in a frame below it, or read this one's locals with debug_state variables.`
1775
+ };
1776
+ }
1777
+ this.forget("evaluation_return");
1778
+ this.send("evaluate", [expression, frame], held.thread);
1779
+ const answered = await this.next(GameDebugger.named("evaluation_return"), READ_MS);
1780
+ if (answered === null || !("data" in answered)) {
1781
+ return {
1782
+ refused: `The game worked out nothing for ${expression} within ${READ_MS / 1000}s. Ask again.`
1783
+ };
1784
+ }
1785
+ const [, , type, value, hint] = answered.data;
1786
+ return {
1787
+ held: this.heldState(),
1788
+ frame,
1789
+ expression,
1790
+ value: shownValue(value ?? null, typeof hint === "string" ? hint : ""),
1791
+ type: typeNamed(type, hint),
1792
+ ...value === null || value === undefined ? {
1793
+ note: "null is also what an expression comes to when it does not parse or fails as it runs. The game prints why as an error, which editor_output reads, since its debugger hands errors over apart from the console debug_state output keeps."
1794
+ } : {}
1795
+ };
1796
+ }
1797
+ async taken() {
1798
+ this.forget("scene:inspect_object");
1799
+ this.send("scene:inspect_object", [0], this.held?.thread ?? MAIN_THREAD);
1800
+ return await this.next(GameDebugger.named("scene:inspect_object"), READ_MS) !== null;
1801
+ }
1802
+ static noFrame(frames, frame) {
1803
+ if (frames.find((each) => each.id === frame) !== undefined) {
1804
+ return null;
1805
+ }
1806
+ return {
1807
+ refused: frames.length === 0 ? "The game is held where no script runs, so it has no frames: debug_control step_over takes it to the next line of script." : `The stack has no frame ${frame}; its frames are ${frames.map((each) => each.id).join(", ")}.`
1808
+ };
1809
+ }
1190
1810
  close() {
1191
1811
  this.connection?.destroy();
1192
1812
  this.server.close();
@@ -1195,11 +1815,11 @@ class GameDebugger {
1195
1815
 
1196
1816
  // src/keeper-channel.ts
1197
1817
  import { randomUUID } from "node:crypto";
1198
- import { mkdtempSync as mkdtempSync2, rmSync as rmSync2 } from "node:fs";
1818
+ import { mkdtempSync as mkdtempSync3, rmSync as rmSync3 } from "node:fs";
1199
1819
  import { connect, createServer as createServer2 } from "node:net";
1200
- import { tmpdir as tmpdir2 } from "node:os";
1201
- import { join as join2 } from "node:path";
1202
- import process4 from "node:process";
1820
+ import { tmpdir as tmpdir3 } from "node:os";
1821
+ import { join as join6 } from "node:path";
1822
+ import process5 from "node:process";
1203
1823
  var DEBUG_PREFIX = "debug ";
1204
1824
  function endedOrWhyNot(end) {
1205
1825
  try {
@@ -1226,8 +1846,8 @@ function errorText(error) {
1226
1846
  async function listenForTheServer(end, debug) {
1227
1847
  let directory = null;
1228
1848
  try {
1229
- directory = process4.platform === "win32" ? null : mkdtempSync2(join2(tmpdir2(), "gdharness-keeper-"));
1230
- const address = directory === null ? `\\\\.\\pipe\\gdharness-keeper-${randomUUID()}` : join2(directory, "keeper.sock");
1849
+ directory = process5.platform === "win32" ? null : mkdtempSync3(join6(tmpdir3(), "gdharness-keeper-"));
1850
+ const address = directory === null ? `\\\\.\\pipe\\gdharness-keeper-${randomUUID()}` : join6(directory, "keeper.sock");
1231
1851
  const server = createServer2((socket) => {
1232
1852
  socket.setEncoding("utf8");
1233
1853
  socket.on("error", () => {});
@@ -1263,13 +1883,13 @@ async function listenForTheServer(end, debug) {
1263
1883
  close: () => {
1264
1884
  server.close();
1265
1885
  if (made !== null) {
1266
- rmSync2(made, { recursive: true, force: true });
1886
+ rmSync3(made, { recursive: true, force: true });
1267
1887
  }
1268
1888
  }
1269
1889
  };
1270
1890
  } catch {
1271
1891
  if (directory !== null) {
1272
- rmSync2(directory, { recursive: true, force: true });
1892
+ rmSync3(directory, { recursive: true, force: true });
1273
1893
  }
1274
1894
  return null;
1275
1895
  }
@@ -1285,7 +1905,7 @@ function listening(server, address) {
1285
1905
  }
1286
1906
 
1287
1907
  // src/outside.ts
1288
- import { fileURLToPath } from "node:url";
1908
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
1289
1909
  function withChanges(base, changes) {
1290
1910
  const env = { ...base };
1291
1911
  for (const [name, value] of Object.entries(changes)) {
@@ -1307,7 +1927,7 @@ async function receivedSpec() {
1307
1927
  }
1308
1928
  return JSON.parse(Buffer.concat(chunks).toString("utf8"));
1309
1929
  }
1310
- var KEEPER_SCRIPT = fileURLToPath(new URL("./keeper.js", import.meta.url));
1930
+ var KEEPER_SCRIPT = fileURLToPath2(new URL("./keeper.js", import.meta.url));
1311
1931
  function readLaunched(printed) {
1312
1932
  const line = printed.split(`
1313
1933
  `).find((one) => one.trim() !== "")?.trim() ?? "";
@@ -1320,28 +1940,12 @@ function readLaunched(printed) {
1320
1940
 
1321
1941
  // src/run-record.ts
1322
1942
  import { createHash as createHash2 } from "node:crypto";
1323
- import { mkdirSync, openSync as openSync2, readdirSync, readFileSync as readFileSync2, rmSync as rmSync4, statSync as statSync3 } from "node:fs";
1324
- import { basename as basename2, join as join5, resolve as resolve3 } from "node:path";
1943
+ import { mkdirSync, openSync as openSync2, readdirSync as readdirSync2, readFileSync as readFileSync5, rmSync as rmSync5, statSync as statSync3 } from "node:fs";
1944
+ import { basename as basename3, join as join8, resolve as resolve3 } from "node:path";
1325
1945
 
1326
- // src/paths.ts
1327
- import { realpathSync, statSync as statSync2 } from "node:fs";
1328
- import { basename, dirname, isAbsolute, join as join3, relative, resolve, sep } from "node:path";
1329
- function realAsFarAsItExists(path) {
1330
- const missing = [];
1331
- let at = path;
1332
- for (;; ) {
1333
- try {
1334
- return join3(realpathSync.native(at), ...missing);
1335
- } catch {
1336
- const parent = dirname(at);
1337
- if (parent === at) {
1338
- return path;
1339
- }
1340
- missing.unshift(basename(at));
1341
- at = parent;
1342
- }
1343
- }
1344
- }
1946
+ // src/process-children.ts
1947
+ import { readFileSync as readFileSync4 } from "node:fs";
1948
+ import { basename as basename2 } from "node:path";
1345
1949
 
1346
1950
  // src/windows-ask.ts
1347
1951
  var SCRIPT = [
@@ -1356,473 +1960,307 @@ var SCRIPT = [
1356
1960
  " } catch {",
1357
1961
  " $said = $_.Exception.Message",
1358
1962
  ' $tag = "ERR"',
1359
- " }",
1360
- ' [Console]::Out.Write($parts[0] + " " + $tag + " " + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($said)) + "`n")',
1361
- " [Console]::Out.Flush()",
1362
- "}"
1363
- ].join(`
1364
- `);
1365
-
1366
- // src/runtime-client.ts
1367
- import { tmpdir as tmpdir3 } from "node:os";
1368
- import { dirname as dirname2, join as join4, resolve as resolve2 } from "node:path";
1369
-
1370
- // src/launch.ts
1371
- function envValue(name, variables = process.env) {
1372
- const value = variables[name];
1373
- return value === undefined || value === "" ? undefined : value;
1374
- }
1375
- var ENGINE_FLAGS = Object.freeze({
1376
- "--verbose": "none",
1377
- "--quiet": "none",
1378
- "--debug-collisions": "none",
1379
- "--debug-paths": "none",
1380
- "--debug-navigation": "none",
1381
- "--debug-avoidance": "none",
1382
- "--debug-stringnames": "none",
1383
- "--debug-canvas-item-redraw": "none",
1384
- "--debug-mute-audio": "none",
1385
- "--print-fps": "none",
1386
- "--gpu-profile": "none",
1387
- "--gpu-validation": "none",
1388
- "--gpu-abort": "none",
1389
- "--disable-vsync": "none",
1390
- "--frame-delay": "number",
1391
- "--time-scale": "number",
1392
- "--fixed-fps": "number",
1393
- "--max-fps": "number",
1394
- "--resolution": "size",
1395
- "--write-movie": "file",
1396
- "--rendering-driver": "driver",
1397
- "--rendering-method": "method",
1398
- "--accessibility": "accessibility"
1399
- });
1400
- var SET_BY_ARGUMENT = Object.freeze({
1401
- "--headless": "headless",
1402
- "--audio-driver": "silent",
1403
- "--quit-after": "frames, on editor_run check"
1404
- });
1405
-
1406
- // src/runtime-client.ts
1407
- function runtimeDirectory(variables = process.env) {
1408
- const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
1409
- if (explicit) {
1410
- return explicit;
1411
- }
1412
- const perUser = envValue("XDG_RUNTIME_DIR", variables);
1413
- return join4(perUser ?? tmpdir3(), "gdharness");
1414
- }
1415
- var ERROR_REPORT_KEEP_MS = 60 * 60 * 1000;
1416
- var verdicts = new Map;
1417
- var LATE_ANSWER_MS = 30 * 60000;
1418
- var lateAnswers = new Map;
1419
- var untaken = new Map;
1420
-
1421
- // src/whole-file.ts
1422
- import { renameSync, rmSync as rmSync3, writeFileSync as writeFileSync2 } from "node:fs";
1423
- var HELD_FILE_MS = 1000;
1424
- var HELD_FILE_RETRY_MS = 10;
1425
- var HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
1426
- function writeWhole(path, text) {
1427
- const partial = `${path}.${process.pid}.partial`;
1428
- try {
1429
- writeFileSync2(partial, text, "utf8");
1430
- } catch {
1431
- return false;
1432
- }
1433
- const until = Date.now() + HELD_FILE_MS;
1434
- for (;; ) {
1435
- try {
1436
- renameSync(partial, path);
1437
- return true;
1438
- } catch (error) {
1439
- const code = error.code ?? "";
1440
- if (!HELD_BY_A_READER.has(code) || Date.now() >= until) {
1441
- try {
1442
- rmSync3(partial, { force: true });
1443
- } catch {}
1444
- return false;
1445
- }
1446
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, HELD_FILE_RETRY_MS);
1447
- }
1448
- }
1449
- }
1450
-
1451
- // src/run-record.ts
1452
- function runsDirectory() {
1453
- return join5(runtimeDirectory(), "runs");
1454
- }
1455
- function projectKey(projectPath) {
1456
- const real = realAsFarAsItExists(resolve3(projectPath)).replace(/[\\/]+$/, "");
1457
- const folded = process.platform === "win32" || process.platform === "darwin" ? real.toLowerCase() : real;
1458
- return createHash2("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
1459
- }
1460
- function runRecordPath(projectPath, directory = runtimeDirectory()) {
1461
- return join5(directory, "runs", `note-${projectKey(projectPath)}.json`);
1462
- }
1463
- function writeRunRecord(record) {
1464
- mkdirSync(runsDirectory(), { recursive: true });
1465
- const path = runRecordPath(record.projectPath);
1466
- if (!writeWhole(path, JSON.stringify(record, null, 2))) {
1467
- throw new Error(`the run note could not be written at ${path}`);
1468
- }
1469
- }
1470
- function recordRunEnded(projectPath, pid, ending) {
1471
- const path = runRecordPath(projectPath);
1472
- const record = recordAt(path);
1473
- if (record === null || record.pid !== pid) {
1474
- return;
1475
- }
1476
- const written = {
1477
- ...record,
1478
- ...ending.exitCode === null ? {} : { exitCode: ending.exitCode },
1479
- ...ending.exitSignal === null ? {} : { exitSignal: ending.exitSignal },
1480
- endedAt: Date.now()
1481
- };
1482
- writeWhole(path, JSON.stringify(written, null, 2));
1483
- }
1484
- function recordAt(path) {
1485
- let parsed;
1486
- try {
1487
- parsed = JSON.parse(readFileSync2(path, "utf8"));
1488
- } catch {
1489
- return null;
1490
- }
1491
- if (!parsed || typeof parsed !== "object") {
1492
- return null;
1493
- }
1494
- const fields = parsed;
1495
- const pid = fields["pid"];
1496
- const transcript = fields["transcript"];
1497
- const startedAt = fields["startedAt"];
1498
- if (typeof pid !== "number" || typeof transcript !== "string" || typeof startedAt !== "number") {
1499
- return null;
1500
- }
1501
- const args = fields["arguments"];
1502
- const startedBy = fields["startedBy"];
1503
- const projectPath = fields["projectPath"];
1504
- const command = fields["command"];
1505
- if (typeof startedBy !== "number" || typeof projectPath !== "string" || projectPath === "" || typeof command !== "string" || !Array.isArray(args)) {
1506
- return null;
1507
- }
1508
- return {
1509
- pid,
1510
- transcript,
1511
- startedAt,
1512
- startedBy,
1513
- projectPath,
1514
- ...typeof fields["servedBy"] === "string" && fields["servedBy"] !== "" ? { servedBy: fields["servedBy"] } : {},
1515
- ...typeof fields["keeper"] === "string" && fields["keeper"] !== "" ? { keeper: fields["keeper"] } : {},
1516
- ...typeof fields["profile"] === "string" && fields["profile"] !== "" ? { profile: fields["profile"] } : {},
1517
- arguments: args.filter((value) => typeof value === "string"),
1518
- command,
1519
- ...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
1520
- ...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {},
1521
- ...typeof fields["endedAt"] === "number" ? { endedAt: fields["endedAt"] } : {}
1522
- };
1523
- }
1963
+ " }",
1964
+ ' [Console]::Out.Write($parts[0] + " " + $tag + " " + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($said)) + "`n")',
1965
+ " [Console]::Out.Flush()",
1966
+ "}"
1967
+ ].join(`
1968
+ `);
1524
1969
 
1525
- // src/scratch.ts
1526
- import { existsSync as existsSync2, mkdtempSync as mkdtempSync3, readdirSync as readdirSync2, rmSync as rmSync5 } from "node:fs";
1527
- import { tmpdir as tmpdir4 } from "node:os";
1528
- import { join as join6 } from "node:path";
1529
- import process5 from "node:process";
1530
- var SWEPT = { recursive: true, force: true };
1531
- var AGAIN_AFTER_MS = [250, 500, 1000, 2000, 4000, 8000, 16000];
1532
- var waiting = new Set;
1533
- var atExit = new Set;
1534
- function scratchDirectory(kind) {
1535
- return mkdtempSync3(join6(tmpdir4(), `gdharness-${kind}-${process5.pid}-`));
1536
- }
1537
- function discardWith(remove, paths) {
1538
- const left = [];
1539
- for (const path of paths) {
1540
- if (!path) {
1970
+ // src/process-children.ts
1971
+ async function linuxStartTimes(pids) {
1972
+ const began = new Map;
1973
+ const hertz = await ticksPerSecond();
1974
+ let booted;
1975
+ try {
1976
+ booted = Number(/^btime\s+(\d+)$/m.exec(readFileSync4("/proc/stat", "utf8"))?.[1] ?? Number.NaN) * 1000;
1977
+ } catch {
1978
+ return began;
1979
+ }
1980
+ if (hertz === null || Number.isNaN(booted)) {
1981
+ return began;
1982
+ }
1983
+ for (const pid of pids) {
1984
+ let stat;
1985
+ try {
1986
+ stat = readFileSync4(`/proc/${String(pid)}/stat`, "utf8");
1987
+ } catch {
1541
1988
  continue;
1542
1989
  }
1543
- try {
1544
- remove(path);
1545
- } catch (error) {
1546
- left.push({ path, reason: error instanceof Error ? error.message : String(error) });
1990
+ const ticks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
1991
+ if (Number.isInteger(ticks) && ticks >= 0) {
1992
+ began.set(pid, booted + Math.round(ticks * 1000 / hertz));
1547
1993
  }
1548
1994
  }
1549
- return left;
1995
+ return began;
1550
1996
  }
1551
- function discard(...paths) {
1552
- const left = discardWith(remove, paths);
1553
- for (const { path } of left) {
1554
- tryAgain(path, 0);
1997
+ var ticksAnswered;
1998
+ async function ticksPerSecond() {
1999
+ if (ticksAnswered !== undefined) {
2000
+ return ticksAnswered;
1555
2001
  }
1556
- return left;
1557
- }
1558
- function remove(path) {
1559
- rmSync5(path, SWEPT);
1560
- }
1561
- function tryAgain(path, attempt) {
1562
- if (attempt === 0) {
1563
- if (waiting.has(path)) {
1564
- return;
2002
+ try {
2003
+ const hertz = Number((await runHidden("getconf", ["CLK_TCK"], { timeout: ASK_TIMEOUT_MS })).stdout.trim());
2004
+ if (Number.isInteger(hertz) && hertz > 0) {
2005
+ ticksAnswered = hertz;
2006
+ return hertz;
1565
2007
  }
1566
- waiting.add(path);
1567
- atExit.delete(path);
1568
- lastTryOnExit();
1569
- }
1570
- const after = AGAIN_AFTER_MS[attempt];
1571
- if (after === undefined || !existsSync2(path)) {
1572
- waiting.delete(path);
1573
- if (after === undefined) {
1574
- atExit.add(path);
2008
+ } catch {}
2009
+ return null;
2010
+ }
2011
+ var ASK_TIMEOUT_MS = 15000;
2012
+ var ONE_SHOT_OPTIONS = [
2013
+ "--import",
2014
+ "--quit",
2015
+ "--quit-after",
2016
+ "--export-release",
2017
+ "--export-debug",
2018
+ "--export-pack",
2019
+ "--export-patch",
2020
+ "--doctool",
2021
+ "--gdscript-docs",
2022
+ "--script",
2023
+ "-s",
2024
+ "--check-only",
2025
+ "--build-solutions",
2026
+ "--dump-gdextension-interface",
2027
+ "--dump-extension-api",
2028
+ "--dump-extension-api-with-docs",
2029
+ "--validate-extension-api",
2030
+ "--convert-3to4",
2031
+ "--validate-conversion-3to4"
2032
+ ];
2033
+ function readCommandLine(command) {
2034
+ const all = commandWords(command);
2035
+ const separator = all.findIndex((word) => word === "--" || word === "++");
2036
+ const words = separator === -1 ? all : all.slice(0, separator);
2037
+ const executable = words[0] === undefined ? "" : basename2(words[0].replaceAll("\\", "/"));
2038
+ const at = words.indexOf("--path");
2039
+ let projectPath = null;
2040
+ if (at !== -1 && at + 1 < words.length) {
2041
+ const taken = [];
2042
+ for (const word of words.slice(at + 1)) {
2043
+ if (taken.length > 0 && (word.startsWith("-") || /^[a-z]+:\/\//.test(word))) {
2044
+ break;
2045
+ }
2046
+ taken.push(word);
1575
2047
  }
1576
- return;
2048
+ projectPath = taken.join(" ").replaceAll("%20", " ");
1577
2049
  }
1578
- setTimeout(() => {
1579
- try {
1580
- remove(path);
1581
- waiting.delete(path);
1582
- } catch {
1583
- tryAgain(path, attempt + 1);
1584
- }
1585
- }, after).unref();
2050
+ return {
2051
+ executable,
2052
+ projectPath,
2053
+ editor: words.some((word) => word === "-e" || word === "--editor"),
2054
+ oneShot: words.find((word) => ONE_SHOT_OPTIONS.includes(word)) ?? null
2055
+ };
1586
2056
  }
1587
- var lastTryWatched = false;
1588
- function lastTryOnExit() {
1589
- if (lastTryWatched) {
1590
- return;
1591
- }
1592
- lastTryWatched = true;
1593
- process5.once("exit", () => {
1594
- for (const path of [...waiting, ...atExit]) {
1595
- try {
1596
- remove(path);
1597
- } catch {}
2057
+ function commandWords(command) {
2058
+ const words = [];
2059
+ let word = "";
2060
+ let quoted = false;
2061
+ let started = false;
2062
+ for (const char of command) {
2063
+ if (char === '"') {
2064
+ quoted = !quoted;
2065
+ started = true;
2066
+ } else if (!quoted && /\s/.test(char)) {
2067
+ if (started) {
2068
+ words.push(word);
2069
+ word = "";
2070
+ started = false;
2071
+ }
2072
+ } else {
2073
+ word += char;
2074
+ started = true;
1598
2075
  }
1599
- });
2076
+ }
2077
+ if (started) {
2078
+ words.push(word);
2079
+ }
2080
+ return words;
1600
2081
  }
1601
2082
 
1602
- // src/gdscript-source.ts
1603
- var DECLARING = new Set(["var", "const", "func", "signal", "enum", "class", "for", "class_name"]);
1604
- var NAME = "[\\p{L}_][\\p{L}\\p{N}_]*";
1605
- var ANNOTATIONS = "(?:@[\\p{L}_][\\p{L}\\p{N}_]*(?:\\([^)]*\\))?\\s+)*";
1606
- var DECLARATION_LINE = new RegExp(`^(${ANNOTATIONS})(static\\s+)?(func|var|const|signal|enum|class)\\s+(${NAME})`, "u");
1607
- var CLASS_NAME_LINE = new RegExp(`^${ANNOTATIONS}class_name\\s+(${NAME})`, "u");
1608
- var EXTENDS_LINE = new RegExp(`^${ANNOTATIONS}(?:class_name\\s+${NAME}\\s+)?extends\\s`, "u");
1609
- var FUNC_LINE = new RegExp(`^[ \\t]*${ANNOTATIONS}(?:static\\s+)?func\\s+(${NAME})\\s*\\(`, "u");
1610
- var LAMBDA_HEADER = new RegExp(`(?<![\\p{L}\\p{N}_])func(?:\\s+${NAME})?\\s*\\(`, "u");
1611
- var DOTTED = `${NAME}(?:\\.${NAME})*`;
1612
- var MEMBER_KINDS = new Set(["var", "const", "signal", "enum", "enumValue"]);
1613
- var KEYWORDS = new Set([
1614
- "and",
1615
- "as",
1616
- "assert",
1617
- "await",
1618
- "break",
1619
- "breakpoint",
1620
- "class",
1621
- "class_name",
1622
- "const",
1623
- "continue",
1624
- "elif",
1625
- "else",
1626
- "enum",
1627
- "extends",
1628
- "false",
1629
- "for",
1630
- "func",
1631
- "if",
1632
- "in",
1633
- "is",
1634
- "match",
1635
- "namespace",
1636
- "not",
1637
- "null",
1638
- "or",
1639
- "pass",
1640
- "preload",
1641
- "return",
1642
- "self",
1643
- "signal",
1644
- "static",
1645
- "super",
1646
- "trait",
1647
- "true",
1648
- "var",
1649
- "void",
1650
- "when",
1651
- "while",
1652
- "yield",
1653
- "INF",
1654
- "NAN",
1655
- "PI",
1656
- "TAU"
1657
- ]);
1658
-
1659
- // src/answer-json.ts
1660
- var SCALAR = String.raw`(?:-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null)`;
1661
- var SCALAR_ARRAY = new RegExp(String.raw`\[\n\s+(${SCALAR}(?:,\n\s+${SCALAR})*)\n\s*\]`, "g");
2083
+ // src/runtime-client.ts
2084
+ import { tmpdir as tmpdir4 } from "node:os";
2085
+ import { dirname as dirname3, join as join7, resolve as resolve2 } from "node:path";
1662
2086
 
1663
- // src/dictionary.ts
1664
- function emptyRecord() {
1665
- return Object.create(null);
2087
+ // src/launch.ts
2088
+ function envValue(name, variables = process.env) {
2089
+ const value = variables[name];
2090
+ return value === undefined || value === "" ? undefined : value;
1666
2091
  }
2092
+ var ENGINE_FLAGS = Object.freeze({
2093
+ "--verbose": "none",
2094
+ "--quiet": "none",
2095
+ "--debug-collisions": "none",
2096
+ "--debug-paths": "none",
2097
+ "--debug-navigation": "none",
2098
+ "--debug-avoidance": "none",
2099
+ "--debug-stringnames": "none",
2100
+ "--debug-canvas-item-redraw": "none",
2101
+ "--debug-mute-audio": "none",
2102
+ "--print-fps": "none",
2103
+ "--gpu-profile": "none",
2104
+ "--gpu-validation": "none",
2105
+ "--gpu-abort": "none",
2106
+ "--disable-vsync": "none",
2107
+ "--frame-delay": "number",
2108
+ "--time-scale": "number",
2109
+ "--fixed-fps": "number",
2110
+ "--max-fps": "number",
2111
+ "--resolution": "size",
2112
+ "--write-movie": "file",
2113
+ "--rendering-driver": "driver",
2114
+ "--rendering-method": "method",
2115
+ "--accessibility": "accessibility"
2116
+ });
2117
+ var SET_BY_ARGUMENT = Object.freeze({
2118
+ "--headless": "headless",
2119
+ "--audio-driver": "silent",
2120
+ "--quit-after": "frames, on editor_run check"
2121
+ });
1667
2122
 
1668
- // src/project-scan.ts
1669
- var SKIPPED = new Set(["node_modules"]);
1670
- var SCENE_EXTENSIONS = new Set(["tscn", "scn"]);
1671
- var SIDECAR_EXTENSIONS = new Set(["import", "uid"]);
1672
- var ASSET_EXTENSIONS = new Set([
1673
- "png",
1674
- "jpg",
1675
- "jpeg",
1676
- "webp",
1677
- "svg",
1678
- "bmp",
1679
- "tga",
1680
- "exr",
1681
- "hdr",
1682
- "ktx",
1683
- "dds",
1684
- "ttf",
1685
- "otf",
1686
- "woff",
1687
- "woff2",
1688
- "fnt",
1689
- "wav",
1690
- "mp3",
1691
- "ogg",
1692
- "glb",
1693
- "gltf",
1694
- "obj",
1695
- "fbx",
1696
- "blend",
1697
- "dae",
1698
- "ogv",
1699
- "gdshader",
1700
- "shader"
1701
- ]);
1702
- var CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
1703
- var BINARY_EXTENSIONS = new Set([
1704
- ...[...ASSET_EXTENSIONS].filter((extension) => !["svg", "gltf", "gdshader", "shader", "fnt"].includes(extension)),
1705
- "scn",
1706
- "res",
1707
- "zip",
1708
- "pck",
1709
- "exe",
1710
- "dll",
1711
- "so",
1712
- "dylib"
1713
- ]);
1714
-
1715
- // src/resources.ts
1716
- var STATIC_RESOURCES = [
1717
- {
1718
- uri: "godot://project/info",
1719
- name: "Project Info",
1720
- description: "Parsed Godot project.godot metadata as JSON.",
1721
- mimeType: "application/json"
1722
- }
1723
- ];
1724
- var RESOURCE_TEMPLATES = [
1725
- {
1726
- uriTemplate: "godot://scene/{path}",
1727
- name: "Scene File",
1728
- description: "Read a Godot scene (.tscn) file from the current project.",
1729
- mimeType: "text/plain"
1730
- },
1731
- {
1732
- uriTemplate: "godot://script/{path}",
1733
- name: "GDScript File",
1734
- description: "Read a GDScript (.gd) file from the current project.",
1735
- mimeType: "text/x-gdscript"
1736
- },
1737
- {
1738
- uriTemplate: "godot://resource/{path}",
1739
- name: "Resource File",
1740
- description: "Read a project resource file (.tres, .tscn, .gd).",
1741
- mimeType: "text/plain"
2123
+ // src/runtime-client.ts
2124
+ function runtimeDirectory(variables = process.env) {
2125
+ const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
2126
+ if (explicit) {
2127
+ return explicit;
1742
2128
  }
1743
- ];
1744
- var RESOURCE_COUNT = STATIC_RESOURCES.length + RESOURCE_TEMPLATES.length;
1745
- function isValueComplete(value) {
1746
- let depth = 0;
1747
- let inString = false;
1748
- let escaped = false;
1749
- for (const character of value) {
1750
- if (escaped) {
1751
- escaped = false;
1752
- continue;
1753
- }
1754
- if (character === "\\") {
1755
- escaped = true;
1756
- continue;
1757
- }
1758
- if (character === '"') {
1759
- inString = !inString;
1760
- continue;
1761
- }
1762
- if (inString) {
1763
- continue;
1764
- }
1765
- if (character === "{" || character === "[" || character === "(") {
1766
- depth += 1;
1767
- } else if (character === "}" || character === "]" || character === ")") {
1768
- depth -= 1;
2129
+ const perUser = envValue("XDG_RUNTIME_DIR", variables);
2130
+ return join7(perUser ?? tmpdir4(), "gdharness");
2131
+ }
2132
+ var ERROR_REPORT_KEEP_MS = 60 * 60 * 1000;
2133
+ var verdicts = new Map;
2134
+ var LATE_ANSWER_MS = 30 * 60000;
2135
+ var lateAnswers = new Map;
2136
+ var untaken = new Map;
2137
+
2138
+ // src/whole-file.ts
2139
+ import { renameSync, rmSync as rmSync4, writeFileSync as writeFileSync2 } from "node:fs";
2140
+ var HELD_FILE_MS = 1000;
2141
+ var HELD_FILE_RETRY_MS = 10;
2142
+ var HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
2143
+ function writeWhole(path, text) {
2144
+ const partial = `${path}.${process.pid}.partial`;
2145
+ try {
2146
+ writeFileSync2(partial, text, "utf8");
2147
+ } catch {
2148
+ return false;
2149
+ }
2150
+ const until = Date.now() + HELD_FILE_MS;
2151
+ for (;; ) {
2152
+ try {
2153
+ renameSync(partial, path);
2154
+ return true;
2155
+ } catch (error) {
2156
+ const code = error.code ?? "";
2157
+ if (!HELD_BY_A_READER.has(code) || Date.now() >= until) {
2158
+ try {
2159
+ rmSync4(partial, { force: true });
2160
+ } catch {}
2161
+ return false;
2162
+ }
2163
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, HELD_FILE_RETRY_MS);
1769
2164
  }
1770
2165
  }
1771
- return depth <= 0 && !inString;
1772
2166
  }
1773
- function parseProjectGodot(content) {
1774
- const result = emptyRecord();
1775
- let section = emptyRecord();
1776
- result["root"] = section;
1777
- const lines = content.split(/\r?\n/);
1778
- for (let index = 0;index < lines.length; index += 1) {
1779
- const line = (lines[index] ?? "").trim();
1780
- if (!line || line.startsWith(";") || line.startsWith("#")) {
1781
- continue;
1782
- }
1783
- if (line.startsWith("[") && line.endsWith("]")) {
1784
- const name = line.slice(1, -1).trim();
1785
- section = result[name] ?? emptyRecord();
1786
- result[name] = section;
1787
- continue;
1788
- }
1789
- const eqIndex = line.indexOf("=");
1790
- if (eqIndex === -1) {
1791
- continue;
1792
- }
1793
- const key = line.slice(0, eqIndex).trim();
1794
- let rawValue = line.slice(eqIndex + 1).trim();
1795
- while (index + 1 < lines.length && !isValueComplete(rawValue)) {
1796
- index += 1;
1797
- rawValue += `
1798
- ${(lines[index] ?? "").trim()}`;
1799
- }
1800
- section[key] = parseIniLikeValue(rawValue);
2167
+
2168
+ // src/run-record.ts
2169
+ function runsDirectory() {
2170
+ return join8(runtimeDirectory(), "runs");
2171
+ }
2172
+ function projectKey(projectPath) {
2173
+ const real = realAsFarAsItExists(resolve3(projectPath)).replace(/[\\/]+$/, "");
2174
+ const folded = process.platform === "win32" || process.platform === "darwin" ? real.toLowerCase() : real;
2175
+ return createHash2("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
2176
+ }
2177
+ function runRecordPath(projectPath, directory = runtimeDirectory()) {
2178
+ return join8(directory, "runs", `note-${projectKey(projectPath)}.json`);
2179
+ }
2180
+ function writeRunRecord(record) {
2181
+ mkdirSync(runsDirectory(), { recursive: true });
2182
+ const path = runRecordPath(record.projectPath);
2183
+ if (!writeWhole(path, JSON.stringify(record, null, 2))) {
2184
+ throw new Error(`the run note could not be written at ${path}`);
1801
2185
  }
1802
- return result;
1803
2186
  }
1804
- function parseIniLikeValue(value) {
1805
- if (value === "null") {
2187
+ function recordRunEnded(projectPath, pid, ending) {
2188
+ const path = runRecordPath(projectPath);
2189
+ const record = recordAt(path);
2190
+ if (record === null || record.pid !== pid) {
2191
+ return;
2192
+ }
2193
+ const { endingAt: _looked, ...kept } = record;
2194
+ const written = {
2195
+ ...kept,
2196
+ ...ending.exitCode === null ? {} : { exitCode: ending.exitCode },
2197
+ ...ending.exitSignal === null ? {} : { exitSignal: ending.exitSignal },
2198
+ ...ending.notTold === true ? { exitNotTold: true } : {},
2199
+ endedAt: Date.now()
2200
+ };
2201
+ writeWhole(path, JSON.stringify(written, null, 2));
2202
+ }
2203
+ function recordAt(path) {
2204
+ let parsed;
2205
+ try {
2206
+ parsed = JSON.parse(readFileSync5(path, "utf8"));
2207
+ } catch {
1806
2208
  return null;
1807
2209
  }
1808
- if (value === "true") {
1809
- return true;
2210
+ if (!parsed || typeof parsed !== "object") {
2211
+ return null;
1810
2212
  }
1811
- if (value === "false") {
1812
- return false;
2213
+ const fields = parsed;
2214
+ const pid = fields["pid"];
2215
+ const transcript = fields["transcript"];
2216
+ const startedAt = fields["startedAt"];
2217
+ if (typeof pid !== "number" || typeof transcript !== "string" || typeof startedAt !== "number") {
2218
+ return null;
1813
2219
  }
1814
- if (/^-?\d+$/.test(value)) {
1815
- const parsed = Number.parseInt(value, 10);
1816
- return Number.isSafeInteger(parsed) ? parsed : value;
2220
+ const args = fields["arguments"];
2221
+ const startedBy = fields["startedBy"];
2222
+ const projectPath = fields["projectPath"];
2223
+ const command = fields["command"];
2224
+ if (typeof startedBy !== "number" || typeof projectPath !== "string" || projectPath === "" || typeof command !== "string" || !Array.isArray(args)) {
2225
+ return null;
1817
2226
  }
1818
- if (/^-?\d*\.\d+$/.test(value)) {
1819
- const parsed = Number.parseFloat(value);
1820
- return Number.isFinite(parsed) ? parsed : value;
2227
+ return {
2228
+ pid,
2229
+ transcript,
2230
+ startedAt,
2231
+ startedBy,
2232
+ projectPath,
2233
+ ...typeof fields["servedBy"] === "string" && fields["servedBy"] !== "" ? { servedBy: fields["servedBy"] } : {},
2234
+ ...typeof fields["keeper"] === "string" && fields["keeper"] !== "" ? { keeper: fields["keeper"] } : {},
2235
+ ...typeof fields["keeperPid"] === "number" && Number.isInteger(fields["keeperPid"]) && fields["keeperPid"] > 0 ? { keeperPid: fields["keeperPid"] } : {},
2236
+ ...typeof fields["profile"] === "string" && fields["profile"] !== "" ? { profile: fields["profile"] } : {},
2237
+ arguments: args.filter((value) => typeof value === "string"),
2238
+ command,
2239
+ ...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
2240
+ ...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {},
2241
+ ...typeof fields["endedAt"] === "number" ? { endedAt: fields["endedAt"] } : {},
2242
+ ...fields["exitNotTold"] === true ? { exitNotTold: true } : {},
2243
+ ...Array.isArray(fields["restartedFrom"]) ? { restartedFrom: fields["restartedFrom"].flatMap(restartedFromAt) } : {},
2244
+ ...typeof fields["endingAt"] === "number" ? { endingAt: fields["endingAt"] } : {}
2245
+ };
2246
+ }
2247
+ function restartedFromAt(value) {
2248
+ if (!value || typeof value !== "object") {
2249
+ return [];
1821
2250
  }
1822
- if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
1823
- return value.slice(1, -1);
2251
+ const fields = value;
2252
+ const { pid, endedAt, exitCode, exitSignal } = fields;
2253
+ if (typeof pid !== "number" || typeof endedAt !== "number") {
2254
+ return [];
1824
2255
  }
1825
- return value;
2256
+ return [
2257
+ {
2258
+ pid,
2259
+ endedAt,
2260
+ ...typeof exitCode === "number" ? { exitCode } : {},
2261
+ ...typeof exitSignal === "string" ? { exitSignal } : {}
2262
+ }
2263
+ ];
1826
2264
  }
1827
2265
 
1828
2266
  // src/script-profile.ts
@@ -1974,6 +2412,184 @@ class ProfileObserver {
1974
2412
  }
1975
2413
  }
1976
2414
 
2415
+ // src/successor.ts
2416
+ import { readlinkSync, realpathSync as realpathSync2 } from "node:fs";
2417
+ import { basename as basename4 } from "node:path";
2418
+ import { setTimeout as delay2 } from "node:timers/promises";
2419
+ var RESTARTED_WITHIN_MS = 2000;
2420
+ var LIST_WITHIN_MS = 15000;
2421
+ var POLL_MS = 250;
2422
+ function successorAmong(listed, exitedPid, engine, notBefore, exitedAt, secondsOnly) {
2423
+ const slack = secondsOnly ? 1000 : 0;
2424
+ const fold = (name) => process.platform === "win32" ? name.toLowerCase() : name;
2425
+ const wanted = fold(basename4(engine.replaceAll("\\", "/")));
2426
+ const found = listed.filter((one) => one.pid !== exitedPid && fold(readCommandLine(one.command).executable) === wanted && one.startedAt + slack >= notBefore && one.startedAt + slack >= exitedAt - RESTARTED_WITHIN_MS);
2427
+ return found.length === 1 ? found[0] ?? null : null;
2428
+ }
2429
+ async function followSuccessor(exitedPid, engine, notBefore, exitedAt, transcript) {
2430
+ if (process.platform === "win32") {
2431
+ return await followOnWindows(exitedPid, engine, notBefore, exitedAt);
2432
+ }
2433
+ return transcript === undefined ? null : await followOnPosix(exitedPid, engine, notBefore, exitedAt, transcript);
2434
+ }
2435
+ async function followOnWindows(exitedPid, engine, notBefore, exitedAt) {
2436
+ const script = [
2437
+ "[Console]::OutputEncoding = [Text.Encoding]::UTF8",
2438
+ "$held = @{}",
2439
+ `foreach ($c in @(Get-CimInstance Win32_Process -Filter "ParentProcessId=${exitedPid}" -Property ProcessId,CreationDate,CommandLine)) {`,
2440
+ " if (-not $c.CreationDate) { continue }",
2441
+ " $began = ([DateTimeOffset]$c.CreationDate).ToUnixTimeMilliseconds()",
2442
+ " $p = Get-Process -Id $c.ProcessId -ErrorAction SilentlyContinue",
2443
+ " if ($null -eq $p -or [Math]::Abs(([DateTimeOffset]$p.StartTime).ToUnixTimeMilliseconds() - $began) -gt 1000) { continue }",
2444
+ " $null = $p.Handle",
2445
+ " $held[[int]$c.ProcessId] = $p",
2446
+ ' "$($c.ProcessId) $began $($c.CommandLine)"',
2447
+ "}",
2448
+ "'listed'",
2449
+ "$chosen = [Console]::In.ReadLine()",
2450
+ "if ($chosen -and $held.ContainsKey([int]$chosen)) { $p = $held[[int]$chosen]; $p.WaitForExit(); 'exit ' + $p.ExitCode }"
2451
+ ].join(`
2452
+ `);
2453
+ const holder = spawnHidden("powershell.exe", [
2454
+ "-NoProfile",
2455
+ "-NonInteractive",
2456
+ "-ExecutionPolicy",
2457
+ "Bypass",
2458
+ "-EncodedCommand",
2459
+ Buffer.from(script, "utf16le").toString("base64")
2460
+ ], { stdio: ["pipe", "pipe", "ignore"] });
2461
+ let said = "";
2462
+ const closed = new Promise((resolve) => {
2463
+ holder.once("close", () => {
2464
+ resolve();
2465
+ });
2466
+ holder.once("error", () => {
2467
+ resolve();
2468
+ });
2469
+ });
2470
+ const listed = await new Promise((resolve) => {
2471
+ const timer = setTimeout(() => {
2472
+ resolve(false);
2473
+ }, LIST_WITHIN_MS);
2474
+ holder.stdout.on("data", (chunk) => {
2475
+ said += chunk.toString("utf8");
2476
+ if (/^listed\r?$/m.test(said)) {
2477
+ clearTimeout(timer);
2478
+ resolve(true);
2479
+ }
2480
+ });
2481
+ closed.then(() => {
2482
+ clearTimeout(timer);
2483
+ resolve(/^listed\r?$/m.test(said));
2484
+ });
2485
+ });
2486
+ if (!listed) {
2487
+ holder.kill();
2488
+ return null;
2489
+ }
2490
+ const candidates = [];
2491
+ for (const line of said.split(/\r?\n/)) {
2492
+ const fields = /^(\d+)\s+(\d+)\s+(.*)$/.exec(line);
2493
+ if (fields?.[1] !== undefined && fields[2] !== undefined && fields[3] !== undefined) {
2494
+ candidates.push({ pid: Number(fields[1]), startedAt: Number(fields[2]), command: fields[3].trim() });
2495
+ }
2496
+ }
2497
+ const successor = successorAmong(candidates, exitedPid, engine, notBefore, exitedAt, false);
2498
+ holder.stdin.end(successor === null ? `
2499
+ ` : `${String(successor.pid)}
2500
+ `);
2501
+ if (successor === null) {
2502
+ return null;
2503
+ }
2504
+ const ended = closed.then(() => {
2505
+ const told = /^exit (-?\d+)\r?$/m.exec(said);
2506
+ return { code: told?.[1] === undefined ? null : Number(told[1]) };
2507
+ });
2508
+ return { successor, ended };
2509
+ }
2510
+ async function followOnPosix(exitedPid, engine, notBefore, exitedAt, transcript) {
2511
+ const linux = process.platform === "linux";
2512
+ let printed;
2513
+ try {
2514
+ ({ stdout: printed } = await runHidden("ps", ["-e", "-o", linux ? "pid=,args=" : "pid=,lstart=,args="], {
2515
+ timeout: LIST_WITHIN_MS,
2516
+ maxBuffer: 16 * 1024 * 1024,
2517
+ env: { ...process.env, LC_ALL: "C" }
2518
+ }));
2519
+ } catch {
2520
+ return null;
2521
+ }
2522
+ const listed = [];
2523
+ if (linux) {
2524
+ const commands = new Map;
2525
+ for (const line of printed.split(`
2526
+ `)) {
2527
+ const fields = /^\s*(\d+)\s+(.*)$/.exec(line);
2528
+ if (fields?.[1] !== undefined) {
2529
+ commands.set(Number(fields[1]), (fields[2] ?? "").trim());
2530
+ }
2531
+ }
2532
+ for (const [pid, startedAt] of await linuxStartTimes([...commands.keys()])) {
2533
+ listed.push({ pid, startedAt, command: commands.get(pid) ?? "" });
2534
+ }
2535
+ } else {
2536
+ for (const line of printed.split(`
2537
+ `)) {
2538
+ const fields = /^\s*(\d+)\s+(\w+\s+\w+\s+\d+\s+[\d:]+\s+\d+)\s+(.*)$/.exec(line);
2539
+ const startedAt = Date.parse(fields?.[2] ?? "");
2540
+ if (fields?.[1] !== undefined && !Number.isNaN(startedAt)) {
2541
+ listed.push({ pid: Number(fields[1]), startedAt, command: (fields[3] ?? "").trim() });
2542
+ }
2543
+ }
2544
+ }
2545
+ const timely = listed.filter((one) => successorAmong([one], exitedPid, engine, notBefore, exitedAt, true) !== null);
2546
+ const ours = canonical(transcript);
2547
+ const candidates = [];
2548
+ for (const one of timely) {
2549
+ const writing = await outputOf(one.pid);
2550
+ if (writing !== null && canonical(writing) === ours) {
2551
+ candidates.push(one);
2552
+ }
2553
+ }
2554
+ const successor = successorAmong(candidates, exitedPid, engine, notBefore, exitedAt, true);
2555
+ if (successor === null) {
2556
+ return null;
2557
+ }
2558
+ const ended = (async () => {
2559
+ while (alive(successor.pid)) {
2560
+ await delay2(POLL_MS);
2561
+ }
2562
+ return { code: null };
2563
+ })();
2564
+ return { successor, ended };
2565
+ }
2566
+ async function outputOf(pid) {
2567
+ if (process.platform === "linux") {
2568
+ try {
2569
+ return readlinkSync(`/proc/${String(pid)}/fd/1`);
2570
+ } catch {
2571
+ return null;
2572
+ }
2573
+ }
2574
+ try {
2575
+ const { stdout } = await runHidden("lsof", ["-a", "-p", String(pid), "-d", "1", "-Fn"], {
2576
+ timeout: LIST_WITHIN_MS
2577
+ });
2578
+ const named = stdout.split(`
2579
+ `).find((line) => line.startsWith("n"));
2580
+ return named === undefined ? null : named.slice(1);
2581
+ } catch {
2582
+ return null;
2583
+ }
2584
+ }
2585
+ function canonical(path) {
2586
+ try {
2587
+ return realpathSync2(path);
2588
+ } catch {
2589
+ return path;
2590
+ }
2591
+ }
2592
+
1977
2593
  // src/keeper.ts
1978
2594
  async function startDirectly(spec, output, hidden, detached = true, errors = output, directory = process.cwd()) {
1979
2595
  return await new Promise((resolve) => {
@@ -1993,8 +2609,9 @@ async function startDirectly(spec, output, hidden, detached = true, errors = out
1993
2609
  }
1994
2610
  async function startThroughHelper(spec, output, stopFile) {
1995
2611
  const scratch = scratchDirectory("desktop");
1996
- const pidFile = join7(scratch, "pid");
1997
- const errorFile = join7(scratch, "error");
2612
+ const pidFile = join9(scratch, "pid");
2613
+ const errorFile = join9(scratch, "error");
2614
+ const stepFile = join9(scratch, "step");
1998
2615
  const where = spec.desktop !== undefined ? `on the ${spec.desktop} desktop` : spec.noActivate === true ? "without activation" : "through the desktop helper";
1999
2616
  try {
2000
2617
  const compiled = await ensureHelperCompiled();
@@ -2002,10 +2619,11 @@ async function startThroughHelper(spec, output, stopFile) {
2002
2619
  return { error: `it could not be started ${where}: ${compiled}` };
2003
2620
  }
2004
2621
  const wrapper = throughHelper({ ...spec, directory: process.cwd() }, spec.command, spec.args, {
2005
- spec: join7(scratch, "spec.json"),
2622
+ spec: join9(scratch, "spec.json"),
2006
2623
  pid: pidFile,
2007
2624
  error: errorFile,
2008
- stop: stopFile
2625
+ stop: stopFile,
2626
+ step: stepFile
2009
2627
  });
2010
2628
  const envChanges = spec.desktop === undefined ? spec.envChanges : { ...spec.envChanges, [DESKTOP_VARIABLE]: spec.desktop };
2011
2629
  const started = await startDirectly({ ...spec, command: wrapper.command, args: wrapper.args, envChanges }, output, true, false, "ignore", tmpdir5());
@@ -2017,18 +2635,20 @@ async function startThroughHelper(spec, output, stopFile) {
2017
2635
  exited.now = true;
2018
2636
  });
2019
2637
  for (let waited = 0;waited < DESKTOP_START_MS; waited += 50) {
2020
- const pid = existsSync3(pidFile) ? Number(readFileSync3(pidFile, "utf8").trim()) : Number.NaN;
2638
+ const pid = existsSync3(pidFile) ? Number(readFileSync6(pidFile, "utf8").trim()) : Number.NaN;
2021
2639
  if (Number.isInteger(pid) && pid > 0) {
2022
2640
  return { child: started.child, pid };
2023
2641
  }
2024
2642
  if (exited.now) {
2025
- const why = existsSync3(errorFile) ? readFileSync3(errorFile, "utf8").trim() : "it exited saying nothing";
2643
+ const why = helperFailure(errorFile) ?? "it exited saying nothing";
2026
2644
  return { error: `it could not be started ${where}: ${why}` };
2027
2645
  }
2028
- await delay2(50);
2646
+ await delay3(50);
2029
2647
  }
2030
2648
  started.child.kill();
2031
- return { error: `it was not started ${where} within ${DESKTOP_START_MS} ms` };
2649
+ return {
2650
+ error: `it was not started ${where} within ${DESKTOP_START_MS} ms, and ${helperStep(stepFile)}`
2651
+ };
2032
2652
  } finally {
2033
2653
  discard(scratch);
2034
2654
  }
@@ -2046,7 +2666,7 @@ function profiledBy(path, projectPath) {
2046
2666
  };
2047
2667
  let projectGodot = null;
2048
2668
  try {
2049
- projectGodot = readFileSync3(join7(projectPath, "project.godot"), "utf8");
2669
+ projectGodot = readFileSync6(join9(projectPath, "project.godot"), "utf8");
2050
2670
  } catch {}
2051
2671
  const observer = new ProfileObserver((totals) => {
2052
2672
  const first = latest === null;
@@ -2147,11 +2767,14 @@ async function keep() {
2147
2767
  }
2148
2768
  const profiled = run?.profile === undefined ? null : profiledBy(run.profile, run.projectPath);
2149
2769
  const debuggerPeer = run === undefined || run.debugged === false ? null : await GameDebugger.open(run.breakpoints ?? [], profiled === null ? [] : [profiled.observer]);
2150
- const toStart = debuggerPeer === null ? spec : { ...spec, args: withEngineArguments(spec.args, debuggerPeer.engineArguments) };
2770
+ const exitNotes = run === undefined ? null : mkdtempSync4(join9(tmpdir5(), "gdharness-exit-"));
2771
+ const noted = exitNotes === null ? spec : { ...spec, envChanges: { ...spec.envChanges, [EXIT_NOTES_VARIABLE]: exitNotes } };
2772
+ const toStart = debuggerPeer === null ? noted : { ...noted, args: withEngineArguments(noted.args, debuggerPeer.engineArguments) };
2151
2773
  const transcript = run === undefined ? "ignore" : openSync3(run.transcript, "a");
2152
- const own = run !== undefined && throughTheHelper(spec) ? mkdtempSync4(join7(tmpdir5(), "gdharness-stop-")) : null;
2153
- const stopFile = own === null ? "" : join7(own, "stop");
2774
+ const own = run !== undefined && throughTheHelper(spec) ? mkdtempSync4(join9(tmpdir5(), "gdharness-stop-")) : null;
2775
+ const stopFile = own === null ? "" : join9(own, "stop");
2154
2776
  let started;
2777
+ const spawnedAt = Date.now();
2155
2778
  try {
2156
2779
  started = await startDetached(toStart, transcript, run === undefined, stopFile);
2157
2780
  } finally {
@@ -2172,6 +2795,7 @@ async function keep() {
2172
2795
  startedAt: run.startedAt,
2173
2796
  startedBy: Date.now(),
2174
2797
  projectPath: run.projectPath,
2798
+ keeperPid: process.pid,
2175
2799
  ...run.servedBy === undefined ? {} : { servedBy: run.servedBy },
2176
2800
  ...run.profile === undefined ? {} : { profile: run.profile },
2177
2801
  arguments: toStart.args,
@@ -2182,13 +2806,18 @@ async function keep() {
2182
2806
  }
2183
2807
  const exited = exitOf(child);
2184
2808
  let listening = null;
2809
+ let end = () => {
2810
+ if (stopFile !== "") {
2811
+ writeFileSync3(stopFile, "");
2812
+ return "signalled";
2813
+ }
2814
+ return child.kill() ? "signalled" : "gone";
2815
+ };
2816
+ const asked = { stop: false };
2185
2817
  if (record !== null) {
2186
2818
  listening = await listenForTheServer(() => {
2187
- if (stopFile !== "") {
2188
- writeFileSync3(stopFile, "");
2189
- return "signalled";
2190
- }
2191
- return child.kill() ? "signalled" : "gone";
2819
+ asked.stop = true;
2820
+ return end();
2192
2821
  }, debuggerPeer === null ? null : (request) => debuggerPeer.ask(request));
2193
2822
  if (listening !== null) {
2194
2823
  writeNoteIfItCan({ ...record, keeper: listening.address });
@@ -2201,21 +2830,87 @@ async function keep() {
2201
2830
  process.stdout.end(`pid ${pid}
2202
2831
  `, resolve);
2203
2832
  });
2204
- const { code, signal } = await exited;
2833
+ let { code, signal } = await exited;
2834
+ let exitedAt = Date.now();
2835
+ let note = record === null ? null : listening === null ? record : { ...record, keeper: listening.address };
2836
+ if (note !== null) {
2837
+ writeNoteIfItCan({ ...note, endingAt: exitedAt });
2838
+ }
2839
+ end = () => "gone";
2840
+ let looked = note === null || restartSaid(exitNotes, pid) === false ? null : followSuccessor(pid, spec.command, spawnedAt, exitedAt, run?.transcript);
2205
2841
  if (debuggerPeer !== null) {
2206
2842
  await debuggerPeer.drained(DRAIN_MS);
2207
2843
  }
2208
2844
  profiled?.finish();
2845
+ let carried = pid;
2846
+ let notTold = false;
2847
+ while (note !== null && looked !== null) {
2848
+ const followed = await looked;
2849
+ if (followed === null) {
2850
+ break;
2851
+ }
2852
+ const next = followed.successor;
2853
+ const before = {
2854
+ pid: carried,
2855
+ endedAt: exitedAt,
2856
+ ...code === null ? {} : { exitCode: code },
2857
+ ...code === null && signal !== null ? { exitSignal: signal } : {}
2858
+ };
2859
+ note = {
2860
+ ...note,
2861
+ pid: next.pid,
2862
+ startedBy: next.startedAt,
2863
+ restartedFrom: [...note.restartedFrom ?? [], before]
2864
+ };
2865
+ writeNoteIfItCan(note);
2866
+ end = () => {
2867
+ try {
2868
+ process.kill(next.pid);
2869
+ return "signalled";
2870
+ } catch {
2871
+ return "gone";
2872
+ }
2873
+ };
2874
+ if (asked.stop) {
2875
+ end();
2876
+ }
2877
+ const ended = await followed.ended;
2878
+ carried = next.pid;
2879
+ code = ended.code;
2880
+ signal = null;
2881
+ notTold = ended.code === null;
2882
+ exitedAt = Date.now();
2883
+ writeNoteIfItCan({ ...note, endingAt: exitedAt });
2884
+ looked = restartSaid(exitNotes, next.pid) === false ? null : followSuccessor(next.pid, spec.command, next.startedAt, exitedAt, run?.transcript);
2885
+ }
2209
2886
  if (run !== undefined) {
2210
- recordRunEnded(run.projectPath, pid, { exitCode: code, exitSignal: code === null ? signal : null });
2887
+ recordRunEnded(run.projectPath, carried, {
2888
+ exitCode: code,
2889
+ exitSignal: code === null ? signal : null,
2890
+ notTold
2891
+ });
2211
2892
  }
2212
2893
  debuggerPeer?.close();
2213
2894
  listening?.close();
2214
- if (own !== null) {
2215
- rmSync6(own, { recursive: true, force: true });
2895
+ for (const made of [own, exitNotes]) {
2896
+ if (made !== null) {
2897
+ rmSync6(made, { recursive: true, force: true });
2898
+ }
2216
2899
  }
2217
2900
  process.exit(0);
2218
2901
  }
2902
+ var EXIT_NOTES_VARIABLE = "GDHARNESS_EXIT_NOTES";
2903
+ function restartSaid(notes, pid) {
2904
+ if (notes === null) {
2905
+ return null;
2906
+ }
2907
+ try {
2908
+ const said = JSON.parse(readFileSync6(join9(notes, `exit-${String(pid)}.json`), "utf8"));
2909
+ return typeof said === "object" && said !== null && "restart" in said && typeof said.restart === "boolean" ? said.restart : null;
2910
+ } catch {
2911
+ return null;
2912
+ }
2913
+ }
2219
2914
  await letWindowsReportCrashes();
2220
2915
  var mode = process.argv[2];
2221
2916
  if (mode === "launch") {