gdharness 1.3.9 → 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 (73) hide show
  1. package/build/cli.js +4563 -3195
  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 +102 -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 +51 -1
  9. package/build/godot/addons/gdharness_editor/scene_file.gd +77 -140
  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 +11 -22
  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 +28 -5
  16. package/build/godot/addons/gdharness_editor/tools/animation_states.gd +39 -20
  17. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +49 -362
  18. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +464 -0
  19. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd.uid +1 -0
  20. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +37 -0
  21. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +221 -0
  22. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd.uid +1 -0
  23. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +147 -492
  24. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -0
  25. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +4 -3
  26. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +2 -1
  27. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +79 -5
  28. package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +7 -30
  29. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +183 -37
  30. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +485 -0
  31. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd.uid +1 -0
  32. package/build/godot/addons/gdharness_editor/value_shapes.gd +128 -1
  33. package/build/godot/addons/gdharness_runtime/declared_values.gd +272 -0
  34. package/build/godot/addons/gdharness_runtime/reading.gd +51 -1
  35. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +35 -3
  36. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +185 -30
  37. package/build/godot/addons/gdharness_runtime/runtime_input.gd +86 -79
  38. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +17 -0
  39. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +13 -4
  40. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +309 -22
  41. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +163 -20
  42. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +49 -3
  43. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +5 -2
  44. package/build/godot/addons/gdharness_runtime/runtime_values.gd +146 -5
  45. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +279 -11
  46. package/build/godot/addons/gdharness_runtime/runtime_words.gd +27 -0
  47. package/build/godot/addons/gdharness_runtime/serialisation.gd +11 -22
  48. package/build/godot/addons/gdharness_runtime/value_shapes.gd +128 -1
  49. package/build/godot/operations/animation_reads.gd +115 -4
  50. package/build/godot/operations/audio_buses.gd +93 -7
  51. package/build/godot/operations/classdb_queries.gd +119 -7
  52. package/build/godot/operations/declared_values.gd +272 -0
  53. package/build/godot/operations/dependencies.gd +154 -41
  54. package/build/godot/operations/editor_tools.gd +22 -14
  55. package/build/godot/operations/export_presets.gd +602 -4
  56. package/build/godot/operations/file_reads.gd +102 -18
  57. package/build/godot/operations/gdscript_authoring.gd +11 -1
  58. package/build/godot/operations/godot_operations.gd +16 -2
  59. package/build/godot/operations/import_pipeline.gd +20 -221
  60. package/build/godot/operations/input_actions.gd +108 -47
  61. package/build/godot/operations/project_config.gd +83 -69
  62. package/build/godot/operations/project_diagnostics.gd +4 -0
  63. package/build/godot/operations/project_validation.gd +266 -0
  64. package/build/godot/operations/property_paths.gd +89 -16
  65. package/build/godot/operations/property_values.gd +180 -121
  66. package/build/godot/operations/reading.gd +51 -1
  67. package/build/godot/operations/scene_connections.gd +222 -0
  68. package/build/godot/operations/serialisation.gd +11 -22
  69. package/build/godot/operations/setting_reads.gd +121 -0
  70. package/build/godot/operations/value_shapes.gd +128 -1
  71. package/build/godot/tests/listed.gd +20 -13
  72. package/build/keeper.js +1371 -706
  73. 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,634 @@ 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
- breakpointLines;
818
- observers;
819
- constructor(server, port, breakpoints, observers) {
820
- this.server = server;
821
- this.port = port;
822
- this.breakpoints = breakpoints;
823
- this.breakpointLines = new Set(breakpoints);
824
- this.observers = observers;
825
- }
826
- static async open(breakpoints, observers = []) {
827
- const server = createServer();
828
- await new Promise((resolve, reject) => {
829
- server.once("error", reject);
830
- server.listen(0, "127.0.0.1", () => {
831
- resolve();
832
- });
833
- });
834
- const address = server.address();
835
- if (address === null || typeof address === "string") {
836
- server.close();
837
- throw new Error("the debugger has no port");
838
- }
839
- const peer = new GameDebugger(server, address.port, breakpoints, observers);
840
- server.on("connection", (socket) => {
841
- peer.accept(socket);
842
- });
843
- return peer;
844
- }
845
- get engineArguments() {
846
- const listed = this.breakpoints.map((one) => one.replaceAll(" ", "%20")).join(",");
847
- return [
848
- "--remote-debug",
849
- `tcp://127.0.0.1:${this.port}`,
850
- "--ignore-error-breaks",
851
- ...listed === "" ? [] : ["--breakpoints", listed]
852
- ];
853
- }
854
- accept(socket) {
855
- if (this.connection !== null) {
856
- socket.destroy();
857
- return;
858
- }
859
- this.connection = socket;
860
- this.server.close();
861
- const send = (message, data) => {
862
- this.send(message, data);
863
- };
864
- for (const observer of this.observers) {
865
- observer.connected(send);
866
- }
867
- const stream = new DebuggerStream;
868
- socket.on("data", (chunk) => {
869
- let messages;
870
- try {
871
- messages = stream.push(chunk);
872
- } catch {
873
- socket.destroy();
874
- return;
875
- }
876
- for (const received of messages) {
877
- if ("message" in received) {
878
- this.take(received);
879
- for (const observer of this.observers) {
880
- observer.message(received, send);
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
+ }
882
1377
  }
883
1378
  }
884
1379
  });
@@ -1018,7 +1513,7 @@ class GameDebugger {
1018
1513
  this.breakpointLines.delete(`${request.script}:${String(request.line)}`);
1019
1514
  }
1020
1515
  this.send("breakpoint", [request.script, request.line, request.set], this.held?.thread ?? MAIN_THREAD);
1021
- return { sent: true };
1516
+ return { sent: true, taken: await this.taken() };
1022
1517
  case "pause":
1023
1518
  return await this.pause();
1024
1519
  case "resume":
@@ -1038,6 +1533,8 @@ class GameDebugger {
1038
1533
  }
1039
1534
  case "variables":
1040
1535
  return await this.variables(request.frame);
1536
+ case "evaluate":
1537
+ return await this.evaluate(request.frame, request.expression);
1041
1538
  default:
1042
1539
  return { refused: `Nothing here answers ${JSON.stringify(request)}.` };
1043
1540
  }
@@ -1094,28 +1591,50 @@ class GameDebugger {
1094
1591
  };
1095
1592
  }
1096
1593
  }
1097
- const frames = await this.pastTheAddon();
1098
- if (frames === "lost") {
1594
+ const past = await this.pastTheAddon();
1595
+ if (past === "lost") {
1099
1596
  return {
1100
1597
  paused: false,
1101
- 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.`
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.`
1102
1599
  };
1103
1600
  }
1104
1601
  this.stoppedBy("Paused");
1105
- const stack = frames ?? [];
1602
+ const stack = past.frames ?? [];
1106
1603
  return {
1107
1604
  paused: true,
1108
1605
  held: this.heldState(),
1109
1606
  stack,
1110
- ...stack[0]?.script.startsWith(RUNTIME_ADDON) === true ? {
1111
- 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.`
1112
1611
  } : {}
1113
1612
  };
1114
1613
  }
1115
- async pastTheAddon() {
1614
+ async pastTheAddon(until) {
1116
1615
  let frames = await this.stack();
1117
- const stepping = Date.now();
1118
- while (frames !== null && this.held !== null && (frames[0]?.script.startsWith(RUNTIME_ADDON) ?? false) && Date.now() - stepping < ADDON_STEP_MS) {
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;
1119
1638
  this.forget("debug_enter");
1120
1639
  this.send("step", [], this.held.thread);
1121
1640
  if (await this.next(GameDebugger.named("debug_enter"), STEP_MS) === null) {
@@ -1123,7 +1642,25 @@ class GameDebugger {
1123
1642
  }
1124
1643
  frames = await this.stack();
1125
1644
  }
1126
- return frames;
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();
1127
1664
  }
1128
1665
  stoppedBy(cause) {
1129
1666
  if (this.held !== null) {
@@ -1153,23 +1690,26 @@ class GameDebugger {
1153
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.`
1154
1691
  };
1155
1692
  }
1156
- const frames = await this.pastTheAddon();
1157
- if (frames === "lost") {
1693
+ const past = await this.pastTheAddon(Date.now() + STEP_MS);
1694
+ if (past === "lost") {
1158
1695
  return {
1159
1696
  stepped: true,
1160
1697
  heldAgain: false,
1161
- note: `The step stopped in gdharness's runtime addon and was stepped 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.`
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.`
1162
1699
  };
1163
1700
  }
1164
1701
  this.stoppedBy("Stepped");
1165
- const stack = frames ?? [];
1702
+ const stack = past.frames ?? [];
1166
1703
  const top = stack[0];
1167
1704
  const onBreakpoint = top !== undefined && this.breakpointLines.has(`${top.script}:${String(top.line)}`);
1168
1705
  return {
1169
1706
  stepped: true,
1170
1707
  heldAgain: true,
1171
1708
  held: { ...this.heldState(), ...onBreakpoint ? { onBreakpoint: true } : {} },
1172
- stack
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
+ }
1173
1713
  };
1174
1714
  }
1175
1715
  async variables(frame) {
@@ -1181,10 +1721,9 @@ class GameDebugger {
1181
1721
  if (frames === null) {
1182
1722
  return { refused: `The game did not send its stack within ${READ_MS / 1000}s.` };
1183
1723
  }
1184
- if (frames.find((each) => each.id === frame) === undefined) {
1185
- return {
1186
- 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(", ")}.`
1187
- };
1724
+ const missing = GameDebugger.noFrame(frames, frame);
1725
+ if (missing !== null) {
1726
+ return missing;
1188
1727
  }
1189
1728
  this.forget("stack_frame_vars");
1190
1729
  this.forget("stack_frame_var");
@@ -1208,15 +1747,66 @@ class GameDebugger {
1208
1747
  };
1209
1748
  }
1210
1749
  const [name, scope, type, value, hint] = one.data;
1211
- const into = scopes[typeof scope === "number" ? scope : 0];
1212
- into?.variables.push({
1213
- 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,
1214
1756
  value: shownValue(value ?? null, typeof hint === "string" ? hint : ""),
1215
- type: typeof hint === "string" && hint !== "" ? hint : typeof type === "number" ? TYPE_NAMES[type] ?? String(type) : "Variant"
1757
+ type: typeNamed(type, hint)
1216
1758
  });
1217
1759
  }
1218
1760
  return { held: this.heldState(), frame, scopes };
1219
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
+ }
1220
1810
  close() {
1221
1811
  this.connection?.destroy();
1222
1812
  this.server.close();
@@ -1225,11 +1815,11 @@ class GameDebugger {
1225
1815
 
1226
1816
  // src/keeper-channel.ts
1227
1817
  import { randomUUID } from "node:crypto";
1228
- import { mkdtempSync as mkdtempSync2, rmSync as rmSync2 } from "node:fs";
1818
+ import { mkdtempSync as mkdtempSync3, rmSync as rmSync3 } from "node:fs";
1229
1819
  import { connect, createServer as createServer2 } from "node:net";
1230
- import { tmpdir as tmpdir2 } from "node:os";
1231
- import { join as join2 } from "node:path";
1232
- 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";
1233
1823
  var DEBUG_PREFIX = "debug ";
1234
1824
  function endedOrWhyNot(end) {
1235
1825
  try {
@@ -1256,8 +1846,8 @@ function errorText(error) {
1256
1846
  async function listenForTheServer(end, debug) {
1257
1847
  let directory = null;
1258
1848
  try {
1259
- directory = process4.platform === "win32" ? null : mkdtempSync2(join2(tmpdir2(), "gdharness-keeper-"));
1260
- 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");
1261
1851
  const server = createServer2((socket) => {
1262
1852
  socket.setEncoding("utf8");
1263
1853
  socket.on("error", () => {});
@@ -1293,13 +1883,13 @@ async function listenForTheServer(end, debug) {
1293
1883
  close: () => {
1294
1884
  server.close();
1295
1885
  if (made !== null) {
1296
- rmSync2(made, { recursive: true, force: true });
1886
+ rmSync3(made, { recursive: true, force: true });
1297
1887
  }
1298
1888
  }
1299
1889
  };
1300
1890
  } catch {
1301
1891
  if (directory !== null) {
1302
- rmSync2(directory, { recursive: true, force: true });
1892
+ rmSync3(directory, { recursive: true, force: true });
1303
1893
  }
1304
1894
  return null;
1305
1895
  }
@@ -1315,7 +1905,7 @@ function listening(server, address) {
1315
1905
  }
1316
1906
 
1317
1907
  // src/outside.ts
1318
- import { fileURLToPath } from "node:url";
1908
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
1319
1909
  function withChanges(base, changes) {
1320
1910
  const env = { ...base };
1321
1911
  for (const [name, value] of Object.entries(changes)) {
@@ -1337,7 +1927,7 @@ async function receivedSpec() {
1337
1927
  }
1338
1928
  return JSON.parse(Buffer.concat(chunks).toString("utf8"));
1339
1929
  }
1340
- var KEEPER_SCRIPT = fileURLToPath(new URL("./keeper.js", import.meta.url));
1930
+ var KEEPER_SCRIPT = fileURLToPath2(new URL("./keeper.js", import.meta.url));
1341
1931
  function readLaunched(printed) {
1342
1932
  const line = printed.split(`
1343
1933
  `).find((one) => one.trim() !== "")?.trim() ?? "";
@@ -1350,28 +1940,12 @@ function readLaunched(printed) {
1350
1940
 
1351
1941
  // src/run-record.ts
1352
1942
  import { createHash as createHash2 } from "node:crypto";
1353
- import { mkdirSync, openSync as openSync2, readdirSync, readFileSync as readFileSync2, rmSync as rmSync4, statSync as statSync3 } from "node:fs";
1354
- 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";
1355
1945
 
1356
- // src/paths.ts
1357
- import { realpathSync, statSync as statSync2 } from "node:fs";
1358
- import { basename, dirname, isAbsolute, join as join3, relative, resolve, sep } from "node:path";
1359
- function realAsFarAsItExists(path) {
1360
- const missing = [];
1361
- let at = path;
1362
- for (;; ) {
1363
- try {
1364
- return join3(realpathSync.native(at), ...missing);
1365
- } catch {
1366
- const parent = dirname(at);
1367
- if (parent === at) {
1368
- return path;
1369
- }
1370
- missing.unshift(basename(at));
1371
- at = parent;
1372
- }
1373
- }
1374
- }
1946
+ // src/process-children.ts
1947
+ import { readFileSync as readFileSync4 } from "node:fs";
1948
+ import { basename as basename2 } from "node:path";
1375
1949
 
1376
1950
  // src/windows-ask.ts
1377
1951
  var SCRIPT = [
@@ -1386,473 +1960,307 @@ var SCRIPT = [
1386
1960
  " } catch {",
1387
1961
  " $said = $_.Exception.Message",
1388
1962
  ' $tag = "ERR"',
1389
- " }",
1390
- ' [Console]::Out.Write($parts[0] + " " + $tag + " " + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($said)) + "`n")',
1391
- " [Console]::Out.Flush()",
1392
- "}"
1393
- ].join(`
1394
- `);
1395
-
1396
- // src/runtime-client.ts
1397
- import { tmpdir as tmpdir3 } from "node:os";
1398
- import { dirname as dirname2, join as join4, resolve as resolve2 } from "node:path";
1399
-
1400
- // src/launch.ts
1401
- function envValue(name, variables = process.env) {
1402
- const value = variables[name];
1403
- return value === undefined || value === "" ? undefined : value;
1404
- }
1405
- var ENGINE_FLAGS = Object.freeze({
1406
- "--verbose": "none",
1407
- "--quiet": "none",
1408
- "--debug-collisions": "none",
1409
- "--debug-paths": "none",
1410
- "--debug-navigation": "none",
1411
- "--debug-avoidance": "none",
1412
- "--debug-stringnames": "none",
1413
- "--debug-canvas-item-redraw": "none",
1414
- "--debug-mute-audio": "none",
1415
- "--print-fps": "none",
1416
- "--gpu-profile": "none",
1417
- "--gpu-validation": "none",
1418
- "--gpu-abort": "none",
1419
- "--disable-vsync": "none",
1420
- "--frame-delay": "number",
1421
- "--time-scale": "number",
1422
- "--fixed-fps": "number",
1423
- "--max-fps": "number",
1424
- "--resolution": "size",
1425
- "--write-movie": "file",
1426
- "--rendering-driver": "driver",
1427
- "--rendering-method": "method",
1428
- "--accessibility": "accessibility"
1429
- });
1430
- var SET_BY_ARGUMENT = Object.freeze({
1431
- "--headless": "headless",
1432
- "--audio-driver": "silent",
1433
- "--quit-after": "frames, on editor_run check"
1434
- });
1435
-
1436
- // src/runtime-client.ts
1437
- function runtimeDirectory(variables = process.env) {
1438
- const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
1439
- if (explicit) {
1440
- return explicit;
1441
- }
1442
- const perUser = envValue("XDG_RUNTIME_DIR", variables);
1443
- return join4(perUser ?? tmpdir3(), "gdharness");
1444
- }
1445
- var ERROR_REPORT_KEEP_MS = 60 * 60 * 1000;
1446
- var verdicts = new Map;
1447
- var LATE_ANSWER_MS = 30 * 60000;
1448
- var lateAnswers = new Map;
1449
- var untaken = new Map;
1450
-
1451
- // src/whole-file.ts
1452
- import { renameSync, rmSync as rmSync3, writeFileSync as writeFileSync2 } from "node:fs";
1453
- var HELD_FILE_MS = 1000;
1454
- var HELD_FILE_RETRY_MS = 10;
1455
- var HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
1456
- function writeWhole(path, text) {
1457
- const partial = `${path}.${process.pid}.partial`;
1458
- try {
1459
- writeFileSync2(partial, text, "utf8");
1460
- } catch {
1461
- return false;
1462
- }
1463
- const until = Date.now() + HELD_FILE_MS;
1464
- for (;; ) {
1465
- try {
1466
- renameSync(partial, path);
1467
- return true;
1468
- } catch (error) {
1469
- const code = error.code ?? "";
1470
- if (!HELD_BY_A_READER.has(code) || Date.now() >= until) {
1471
- try {
1472
- rmSync3(partial, { force: true });
1473
- } catch {}
1474
- return false;
1475
- }
1476
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, HELD_FILE_RETRY_MS);
1477
- }
1478
- }
1479
- }
1480
-
1481
- // src/run-record.ts
1482
- function runsDirectory() {
1483
- return join5(runtimeDirectory(), "runs");
1484
- }
1485
- function projectKey(projectPath) {
1486
- const real = realAsFarAsItExists(resolve3(projectPath)).replace(/[\\/]+$/, "");
1487
- const folded = process.platform === "win32" || process.platform === "darwin" ? real.toLowerCase() : real;
1488
- return createHash2("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
1489
- }
1490
- function runRecordPath(projectPath, directory = runtimeDirectory()) {
1491
- return join5(directory, "runs", `note-${projectKey(projectPath)}.json`);
1492
- }
1493
- function writeRunRecord(record) {
1494
- mkdirSync(runsDirectory(), { recursive: true });
1495
- const path = runRecordPath(record.projectPath);
1496
- if (!writeWhole(path, JSON.stringify(record, null, 2))) {
1497
- throw new Error(`the run note could not be written at ${path}`);
1498
- }
1499
- }
1500
- function recordRunEnded(projectPath, pid, ending) {
1501
- const path = runRecordPath(projectPath);
1502
- const record = recordAt(path);
1503
- if (record === null || record.pid !== pid) {
1504
- return;
1505
- }
1506
- const written = {
1507
- ...record,
1508
- ...ending.exitCode === null ? {} : { exitCode: ending.exitCode },
1509
- ...ending.exitSignal === null ? {} : { exitSignal: ending.exitSignal },
1510
- endedAt: Date.now()
1511
- };
1512
- writeWhole(path, JSON.stringify(written, null, 2));
1513
- }
1514
- function recordAt(path) {
1515
- let parsed;
1516
- try {
1517
- parsed = JSON.parse(readFileSync2(path, "utf8"));
1518
- } catch {
1519
- return null;
1520
- }
1521
- if (!parsed || typeof parsed !== "object") {
1522
- return null;
1523
- }
1524
- const fields = parsed;
1525
- const pid = fields["pid"];
1526
- const transcript = fields["transcript"];
1527
- const startedAt = fields["startedAt"];
1528
- if (typeof pid !== "number" || typeof transcript !== "string" || typeof startedAt !== "number") {
1529
- return null;
1530
- }
1531
- const args = fields["arguments"];
1532
- const startedBy = fields["startedBy"];
1533
- const projectPath = fields["projectPath"];
1534
- const command = fields["command"];
1535
- if (typeof startedBy !== "number" || typeof projectPath !== "string" || projectPath === "" || typeof command !== "string" || !Array.isArray(args)) {
1536
- return null;
1537
- }
1538
- return {
1539
- pid,
1540
- transcript,
1541
- startedAt,
1542
- startedBy,
1543
- projectPath,
1544
- ...typeof fields["servedBy"] === "string" && fields["servedBy"] !== "" ? { servedBy: fields["servedBy"] } : {},
1545
- ...typeof fields["keeper"] === "string" && fields["keeper"] !== "" ? { keeper: fields["keeper"] } : {},
1546
- ...typeof fields["profile"] === "string" && fields["profile"] !== "" ? { profile: fields["profile"] } : {},
1547
- arguments: args.filter((value) => typeof value === "string"),
1548
- command,
1549
- ...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
1550
- ...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {},
1551
- ...typeof fields["endedAt"] === "number" ? { endedAt: fields["endedAt"] } : {}
1552
- };
1553
- }
1963
+ " }",
1964
+ ' [Console]::Out.Write($parts[0] + " " + $tag + " " + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($said)) + "`n")',
1965
+ " [Console]::Out.Flush()",
1966
+ "}"
1967
+ ].join(`
1968
+ `);
1554
1969
 
1555
- // src/scratch.ts
1556
- import { existsSync as existsSync2, mkdtempSync as mkdtempSync3, readdirSync as readdirSync2, rmSync as rmSync5 } from "node:fs";
1557
- import { tmpdir as tmpdir4 } from "node:os";
1558
- import { join as join6 } from "node:path";
1559
- import process5 from "node:process";
1560
- var SWEPT = { recursive: true, force: true };
1561
- var AGAIN_AFTER_MS = [250, 500, 1000, 2000, 4000, 8000, 16000];
1562
- var waiting = new Set;
1563
- var atExit = new Set;
1564
- function scratchDirectory(kind) {
1565
- return mkdtempSync3(join6(tmpdir4(), `gdharness-${kind}-${process5.pid}-`));
1566
- }
1567
- function discardWith(remove, paths) {
1568
- const left = [];
1569
- for (const path of paths) {
1570
- 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 {
1571
1988
  continue;
1572
1989
  }
1573
- try {
1574
- remove(path);
1575
- } catch (error) {
1576
- 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));
1577
1993
  }
1578
1994
  }
1579
- return left;
1995
+ return began;
1580
1996
  }
1581
- function discard(...paths) {
1582
- const left = discardWith(remove, paths);
1583
- for (const { path } of left) {
1584
- tryAgain(path, 0);
1997
+ var ticksAnswered;
1998
+ async function ticksPerSecond() {
1999
+ if (ticksAnswered !== undefined) {
2000
+ return ticksAnswered;
1585
2001
  }
1586
- return left;
1587
- }
1588
- function remove(path) {
1589
- rmSync5(path, SWEPT);
1590
- }
1591
- function tryAgain(path, attempt) {
1592
- if (attempt === 0) {
1593
- if (waiting.has(path)) {
1594
- 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;
1595
2007
  }
1596
- waiting.add(path);
1597
- atExit.delete(path);
1598
- lastTryOnExit();
1599
- }
1600
- const after = AGAIN_AFTER_MS[attempt];
1601
- if (after === undefined || !existsSync2(path)) {
1602
- waiting.delete(path);
1603
- if (after === undefined) {
1604
- 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);
1605
2047
  }
1606
- return;
2048
+ projectPath = taken.join(" ").replaceAll("%20", " ");
1607
2049
  }
1608
- setTimeout(() => {
1609
- try {
1610
- remove(path);
1611
- waiting.delete(path);
1612
- } catch {
1613
- tryAgain(path, attempt + 1);
1614
- }
1615
- }, 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
+ };
1616
2056
  }
1617
- var lastTryWatched = false;
1618
- function lastTryOnExit() {
1619
- if (lastTryWatched) {
1620
- return;
1621
- }
1622
- lastTryWatched = true;
1623
- process5.once("exit", () => {
1624
- for (const path of [...waiting, ...atExit]) {
1625
- try {
1626
- remove(path);
1627
- } 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;
1628
2075
  }
1629
- });
2076
+ }
2077
+ if (started) {
2078
+ words.push(word);
2079
+ }
2080
+ return words;
1630
2081
  }
1631
2082
 
1632
- // src/gdscript-source.ts
1633
- var DECLARING = new Set(["var", "const", "func", "signal", "enum", "class", "for", "class_name"]);
1634
- var NAME = "[\\p{L}_][\\p{L}\\p{N}_]*";
1635
- var ANNOTATIONS = "(?:@[\\p{L}_][\\p{L}\\p{N}_]*(?:\\([^)]*\\))?\\s+)*";
1636
- var DECLARATION_LINE = new RegExp(`^(${ANNOTATIONS})(static\\s+)?(func|var|const|signal|enum|class)\\s+(${NAME})`, "u");
1637
- var CLASS_NAME_LINE = new RegExp(`^${ANNOTATIONS}class_name\\s+(${NAME})`, "u");
1638
- var EXTENDS_LINE = new RegExp(`^${ANNOTATIONS}(?:class_name\\s+${NAME}\\s+)?extends\\s`, "u");
1639
- var FUNC_LINE = new RegExp(`^[ \\t]*${ANNOTATIONS}(?:static\\s+)?func\\s+(${NAME})\\s*\\(`, "u");
1640
- var LAMBDA_HEADER = new RegExp(`(?<![\\p{L}\\p{N}_])func(?:\\s+${NAME})?\\s*\\(`, "u");
1641
- var DOTTED = `${NAME}(?:\\.${NAME})*`;
1642
- var MEMBER_KINDS = new Set(["var", "const", "signal", "enum", "enumValue"]);
1643
- var KEYWORDS = new Set([
1644
- "and",
1645
- "as",
1646
- "assert",
1647
- "await",
1648
- "break",
1649
- "breakpoint",
1650
- "class",
1651
- "class_name",
1652
- "const",
1653
- "continue",
1654
- "elif",
1655
- "else",
1656
- "enum",
1657
- "extends",
1658
- "false",
1659
- "for",
1660
- "func",
1661
- "if",
1662
- "in",
1663
- "is",
1664
- "match",
1665
- "namespace",
1666
- "not",
1667
- "null",
1668
- "or",
1669
- "pass",
1670
- "preload",
1671
- "return",
1672
- "self",
1673
- "signal",
1674
- "static",
1675
- "super",
1676
- "trait",
1677
- "true",
1678
- "var",
1679
- "void",
1680
- "when",
1681
- "while",
1682
- "yield",
1683
- "INF",
1684
- "NAN",
1685
- "PI",
1686
- "TAU"
1687
- ]);
1688
-
1689
- // src/answer-json.ts
1690
- var SCALAR = String.raw`(?:-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null)`;
1691
- 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";
1692
2086
 
1693
- // src/dictionary.ts
1694
- function emptyRecord() {
1695
- 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;
1696
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
+ });
1697
2122
 
1698
- // src/project-scan.ts
1699
- var SKIPPED = new Set(["node_modules"]);
1700
- var SCENE_EXTENSIONS = new Set(["tscn", "scn"]);
1701
- var SIDECAR_EXTENSIONS = new Set(["import", "uid"]);
1702
- var ASSET_EXTENSIONS = new Set([
1703
- "png",
1704
- "jpg",
1705
- "jpeg",
1706
- "webp",
1707
- "svg",
1708
- "bmp",
1709
- "tga",
1710
- "exr",
1711
- "hdr",
1712
- "ktx",
1713
- "dds",
1714
- "ttf",
1715
- "otf",
1716
- "woff",
1717
- "woff2",
1718
- "fnt",
1719
- "wav",
1720
- "mp3",
1721
- "ogg",
1722
- "glb",
1723
- "gltf",
1724
- "obj",
1725
- "fbx",
1726
- "blend",
1727
- "dae",
1728
- "ogv",
1729
- "gdshader",
1730
- "shader"
1731
- ]);
1732
- var CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
1733
- var BINARY_EXTENSIONS = new Set([
1734
- ...[...ASSET_EXTENSIONS].filter((extension) => !["svg", "gltf", "gdshader", "shader", "fnt"].includes(extension)),
1735
- "scn",
1736
- "res",
1737
- "zip",
1738
- "pck",
1739
- "exe",
1740
- "dll",
1741
- "so",
1742
- "dylib"
1743
- ]);
1744
-
1745
- // src/resources.ts
1746
- var STATIC_RESOURCES = [
1747
- {
1748
- uri: "godot://project/info",
1749
- name: "Project Info",
1750
- description: "Parsed Godot project.godot metadata as JSON.",
1751
- mimeType: "application/json"
1752
- }
1753
- ];
1754
- var RESOURCE_TEMPLATES = [
1755
- {
1756
- uriTemplate: "godot://scene/{path}",
1757
- name: "Scene File",
1758
- description: "Read a Godot scene (.tscn) file from the current project.",
1759
- mimeType: "text/plain"
1760
- },
1761
- {
1762
- uriTemplate: "godot://script/{path}",
1763
- name: "GDScript File",
1764
- description: "Read a GDScript (.gd) file from the current project.",
1765
- mimeType: "text/x-gdscript"
1766
- },
1767
- {
1768
- uriTemplate: "godot://resource/{path}",
1769
- name: "Resource File",
1770
- description: "Read a project resource file (.tres, .tscn, .gd).",
1771
- 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;
1772
2128
  }
1773
- ];
1774
- var RESOURCE_COUNT = STATIC_RESOURCES.length + RESOURCE_TEMPLATES.length;
1775
- function isValueComplete(value) {
1776
- let depth = 0;
1777
- let inString = false;
1778
- let escaped = false;
1779
- for (const character of value) {
1780
- if (escaped) {
1781
- escaped = false;
1782
- continue;
1783
- }
1784
- if (character === "\\") {
1785
- escaped = true;
1786
- continue;
1787
- }
1788
- if (character === '"') {
1789
- inString = !inString;
1790
- continue;
1791
- }
1792
- if (inString) {
1793
- continue;
1794
- }
1795
- if (character === "{" || character === "[" || character === "(") {
1796
- depth += 1;
1797
- } else if (character === "}" || character === "]" || character === ")") {
1798
- 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);
1799
2164
  }
1800
2165
  }
1801
- return depth <= 0 && !inString;
1802
2166
  }
1803
- function parseProjectGodot(content) {
1804
- const result = emptyRecord();
1805
- let section = emptyRecord();
1806
- result["root"] = section;
1807
- const lines = content.split(/\r?\n/);
1808
- for (let index = 0;index < lines.length; index += 1) {
1809
- const line = (lines[index] ?? "").trim();
1810
- if (!line || line.startsWith(";") || line.startsWith("#")) {
1811
- continue;
1812
- }
1813
- if (line.startsWith("[") && line.endsWith("]")) {
1814
- const name = line.slice(1, -1).trim();
1815
- section = result[name] ?? emptyRecord();
1816
- result[name] = section;
1817
- continue;
1818
- }
1819
- const eqIndex = line.indexOf("=");
1820
- if (eqIndex === -1) {
1821
- continue;
1822
- }
1823
- const key = line.slice(0, eqIndex).trim();
1824
- let rawValue = line.slice(eqIndex + 1).trim();
1825
- while (index + 1 < lines.length && !isValueComplete(rawValue)) {
1826
- index += 1;
1827
- rawValue += `
1828
- ${(lines[index] ?? "").trim()}`;
1829
- }
1830
- 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}`);
1831
2185
  }
1832
- return result;
1833
2186
  }
1834
- function parseIniLikeValue(value) {
1835
- 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 {
1836
2208
  return null;
1837
2209
  }
1838
- if (value === "true") {
1839
- return true;
2210
+ if (!parsed || typeof parsed !== "object") {
2211
+ return null;
1840
2212
  }
1841
- if (value === "false") {
1842
- 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;
1843
2219
  }
1844
- if (/^-?\d+$/.test(value)) {
1845
- const parsed = Number.parseInt(value, 10);
1846
- 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;
1847
2226
  }
1848
- if (/^-?\d*\.\d+$/.test(value)) {
1849
- const parsed = Number.parseFloat(value);
1850
- 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 [];
1851
2250
  }
1852
- if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
1853
- 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 [];
1854
2255
  }
1855
- return value;
2256
+ return [
2257
+ {
2258
+ pid,
2259
+ endedAt,
2260
+ ...typeof exitCode === "number" ? { exitCode } : {},
2261
+ ...typeof exitSignal === "string" ? { exitSignal } : {}
2262
+ }
2263
+ ];
1856
2264
  }
1857
2265
 
1858
2266
  // src/script-profile.ts
@@ -2004,6 +2412,184 @@ class ProfileObserver {
2004
2412
  }
2005
2413
  }
2006
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
+
2007
2593
  // src/keeper.ts
2008
2594
  async function startDirectly(spec, output, hidden, detached = true, errors = output, directory = process.cwd()) {
2009
2595
  return await new Promise((resolve) => {
@@ -2023,8 +2609,9 @@ async function startDirectly(spec, output, hidden, detached = true, errors = out
2023
2609
  }
2024
2610
  async function startThroughHelper(spec, output, stopFile) {
2025
2611
  const scratch = scratchDirectory("desktop");
2026
- const pidFile = join7(scratch, "pid");
2027
- const errorFile = join7(scratch, "error");
2612
+ const pidFile = join9(scratch, "pid");
2613
+ const errorFile = join9(scratch, "error");
2614
+ const stepFile = join9(scratch, "step");
2028
2615
  const where = spec.desktop !== undefined ? `on the ${spec.desktop} desktop` : spec.noActivate === true ? "without activation" : "through the desktop helper";
2029
2616
  try {
2030
2617
  const compiled = await ensureHelperCompiled();
@@ -2032,10 +2619,11 @@ async function startThroughHelper(spec, output, stopFile) {
2032
2619
  return { error: `it could not be started ${where}: ${compiled}` };
2033
2620
  }
2034
2621
  const wrapper = throughHelper({ ...spec, directory: process.cwd() }, spec.command, spec.args, {
2035
- spec: join7(scratch, "spec.json"),
2622
+ spec: join9(scratch, "spec.json"),
2036
2623
  pid: pidFile,
2037
2624
  error: errorFile,
2038
- stop: stopFile
2625
+ stop: stopFile,
2626
+ step: stepFile
2039
2627
  });
2040
2628
  const envChanges = spec.desktop === undefined ? spec.envChanges : { ...spec.envChanges, [DESKTOP_VARIABLE]: spec.desktop };
2041
2629
  const started = await startDirectly({ ...spec, command: wrapper.command, args: wrapper.args, envChanges }, output, true, false, "ignore", tmpdir5());
@@ -2047,18 +2635,20 @@ async function startThroughHelper(spec, output, stopFile) {
2047
2635
  exited.now = true;
2048
2636
  });
2049
2637
  for (let waited = 0;waited < DESKTOP_START_MS; waited += 50) {
2050
- const pid = existsSync3(pidFile) ? Number(readFileSync3(pidFile, "utf8").trim()) : Number.NaN;
2638
+ const pid = existsSync3(pidFile) ? Number(readFileSync6(pidFile, "utf8").trim()) : Number.NaN;
2051
2639
  if (Number.isInteger(pid) && pid > 0) {
2052
2640
  return { child: started.child, pid };
2053
2641
  }
2054
2642
  if (exited.now) {
2055
- const why = existsSync3(errorFile) ? readFileSync3(errorFile, "utf8").trim() : "it exited saying nothing";
2643
+ const why = helperFailure(errorFile) ?? "it exited saying nothing";
2056
2644
  return { error: `it could not be started ${where}: ${why}` };
2057
2645
  }
2058
- await delay2(50);
2646
+ await delay3(50);
2059
2647
  }
2060
2648
  started.child.kill();
2061
- 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
+ };
2062
2652
  } finally {
2063
2653
  discard(scratch);
2064
2654
  }
@@ -2076,7 +2666,7 @@ function profiledBy(path, projectPath) {
2076
2666
  };
2077
2667
  let projectGodot = null;
2078
2668
  try {
2079
- projectGodot = readFileSync3(join7(projectPath, "project.godot"), "utf8");
2669
+ projectGodot = readFileSync6(join9(projectPath, "project.godot"), "utf8");
2080
2670
  } catch {}
2081
2671
  const observer = new ProfileObserver((totals) => {
2082
2672
  const first = latest === null;
@@ -2177,11 +2767,14 @@ async function keep() {
2177
2767
  }
2178
2768
  const profiled = run?.profile === undefined ? null : profiledBy(run.profile, run.projectPath);
2179
2769
  const debuggerPeer = run === undefined || run.debugged === false ? null : await GameDebugger.open(run.breakpoints ?? [], profiled === null ? [] : [profiled.observer]);
2180
- 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) };
2181
2773
  const transcript = run === undefined ? "ignore" : openSync3(run.transcript, "a");
2182
- const own = run !== undefined && throughTheHelper(spec) ? mkdtempSync4(join7(tmpdir5(), "gdharness-stop-")) : null;
2183
- 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");
2184
2776
  let started;
2777
+ const spawnedAt = Date.now();
2185
2778
  try {
2186
2779
  started = await startDetached(toStart, transcript, run === undefined, stopFile);
2187
2780
  } finally {
@@ -2202,6 +2795,7 @@ async function keep() {
2202
2795
  startedAt: run.startedAt,
2203
2796
  startedBy: Date.now(),
2204
2797
  projectPath: run.projectPath,
2798
+ keeperPid: process.pid,
2205
2799
  ...run.servedBy === undefined ? {} : { servedBy: run.servedBy },
2206
2800
  ...run.profile === undefined ? {} : { profile: run.profile },
2207
2801
  arguments: toStart.args,
@@ -2212,13 +2806,18 @@ async function keep() {
2212
2806
  }
2213
2807
  const exited = exitOf(child);
2214
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 };
2215
2817
  if (record !== null) {
2216
2818
  listening = await listenForTheServer(() => {
2217
- if (stopFile !== "") {
2218
- writeFileSync3(stopFile, "");
2219
- return "signalled";
2220
- }
2221
- return child.kill() ? "signalled" : "gone";
2819
+ asked.stop = true;
2820
+ return end();
2222
2821
  }, debuggerPeer === null ? null : (request) => debuggerPeer.ask(request));
2223
2822
  if (listening !== null) {
2224
2823
  writeNoteIfItCan({ ...record, keeper: listening.address });
@@ -2231,21 +2830,87 @@ async function keep() {
2231
2830
  process.stdout.end(`pid ${pid}
2232
2831
  `, resolve);
2233
2832
  });
2234
- 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);
2235
2841
  if (debuggerPeer !== null) {
2236
2842
  await debuggerPeer.drained(DRAIN_MS);
2237
2843
  }
2238
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
+ }
2239
2886
  if (run !== undefined) {
2240
- 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
+ });
2241
2892
  }
2242
2893
  debuggerPeer?.close();
2243
2894
  listening?.close();
2244
- if (own !== null) {
2245
- 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
+ }
2246
2899
  }
2247
2900
  process.exit(0);
2248
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
+ }
2249
2914
  await letWindowsReportCrashes();
2250
2915
  var mode = process.argv[2];
2251
2916
  if (mode === "launch") {