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