gdharness 1.3.9 → 1.3.11
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 +4740 -3212
- 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 +102 -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 +51 -1
- package/build/godot/addons/gdharness_editor/scene_file.gd +77 -140
- 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 +11 -22
- 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 +28 -5
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd +39 -20
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +49 -362
- 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 +147 -492
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -0
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +4 -3
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +2 -1
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +79 -5
- package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +7 -30
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +183 -37
- 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/declared_values.gd +272 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +51 -1
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +35 -3
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +185 -30
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +86 -79
- 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 +309 -22
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +163 -20
- package/build/godot/addons/gdharness_runtime/runtime_screen.gd +49 -3
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +5 -2
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +146 -5
- 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 +11 -22
- 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 +22 -14
- package/build/godot/operations/export_presets.gd +602 -4
- package/build/godot/operations/file_reads.gd +102 -18
- package/build/godot/operations/gdscript_authoring.gd +11 -1
- package/build/godot/operations/godot_operations.gd +16 -2
- package/build/godot/operations/import_pipeline.gd +20 -221
- package/build/godot/operations/input_actions.gd +108 -47
- package/build/godot/operations/project_config.gd +83 -69
- 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 +51 -1
- package/build/godot/operations/scene_connections.gd +222 -0
- package/build/godot/operations/serialisation.gd +11 -22
- package/build/godot/operations/setting_reads.gd +121 -0
- package/build/godot/operations/value_shapes.gd +128 -1
- package/build/godot/tests/listed.gd +73 -13
- package/build/keeper.js +1371 -706
- 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,634 @@ 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
|
+
}
|
|
882
1377
|
}
|
|
883
1378
|
}
|
|
884
1379
|
});
|
|
@@ -1018,7 +1513,7 @@ class GameDebugger {
|
|
|
1018
1513
|
this.breakpointLines.delete(`${request.script}:${String(request.line)}`);
|
|
1019
1514
|
}
|
|
1020
1515
|
this.send("breakpoint", [request.script, request.line, request.set], this.held?.thread ?? MAIN_THREAD);
|
|
1021
|
-
return { sent: true };
|
|
1516
|
+
return { sent: true, taken: await this.taken() };
|
|
1022
1517
|
case "pause":
|
|
1023
1518
|
return await this.pause();
|
|
1024
1519
|
case "resume":
|
|
@@ -1038,6 +1533,8 @@ class GameDebugger {
|
|
|
1038
1533
|
}
|
|
1039
1534
|
case "variables":
|
|
1040
1535
|
return await this.variables(request.frame);
|
|
1536
|
+
case "evaluate":
|
|
1537
|
+
return await this.evaluate(request.frame, request.expression);
|
|
1041
1538
|
default:
|
|
1042
1539
|
return { refused: `Nothing here answers ${JSON.stringify(request)}.` };
|
|
1043
1540
|
}
|
|
@@ -1094,28 +1591,50 @@ class GameDebugger {
|
|
|
1094
1591
|
};
|
|
1095
1592
|
}
|
|
1096
1593
|
}
|
|
1097
|
-
const
|
|
1098
|
-
if (
|
|
1594
|
+
const past = await this.pastTheAddon();
|
|
1595
|
+
if (past === "lost") {
|
|
1099
1596
|
return {
|
|
1100
1597
|
paused: false,
|
|
1101
|
-
note: `The game stopped in gdharness's runtime addon and was
|
|
1598
|
+
note: `The game stopped in gdharness's runtime addon and was let go on to reach its own script, and did not stop again within ${STEP_MS / 1000}s. It stops at the next line of script it runs; debug_state stack reads it then.`
|
|
1102
1599
|
};
|
|
1103
1600
|
}
|
|
1104
1601
|
this.stoppedBy("Paused");
|
|
1105
|
-
const stack = frames ?? [];
|
|
1602
|
+
const stack = past.frames ?? [];
|
|
1106
1603
|
return {
|
|
1107
1604
|
paused: true,
|
|
1108
1605
|
held: this.heldState(),
|
|
1109
1606
|
stack,
|
|
1110
|
-
...
|
|
1111
|
-
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.`
|
|
1112
1611
|
} : {}
|
|
1113
1612
|
};
|
|
1114
1613
|
}
|
|
1115
|
-
async pastTheAddon() {
|
|
1614
|
+
async pastTheAddon(until) {
|
|
1116
1615
|
let frames = await this.stack();
|
|
1117
|
-
|
|
1118
|
-
|
|
1616
|
+
let framesLeft = ADDON_FRAMES;
|
|
1617
|
+
let linesLeft = ADDON_LINES;
|
|
1618
|
+
while (frames !== null && this.held !== null && (frames[0]?.script.startsWith(RUNTIME_ADDON) ?? false)) {
|
|
1619
|
+
if (until !== undefined && Date.now() >= until) {
|
|
1620
|
+
return { frames, gaveUp: "time" };
|
|
1621
|
+
}
|
|
1622
|
+
if (frames.some((frame) => frame.script === RUNTIME_AUTOLOAD && frame.name === "_process")) {
|
|
1623
|
+
if (until === undefined && framesLeft === 0) {
|
|
1624
|
+
return { frames, gaveUp: "frames" };
|
|
1625
|
+
}
|
|
1626
|
+
framesLeft -= 1;
|
|
1627
|
+
const reached = await this.toTheFrameEnd();
|
|
1628
|
+
if (reached === "lost") {
|
|
1629
|
+
return "lost";
|
|
1630
|
+
}
|
|
1631
|
+
frames = reached;
|
|
1632
|
+
continue;
|
|
1633
|
+
}
|
|
1634
|
+
if (until === undefined && linesLeft === 0) {
|
|
1635
|
+
return { frames, gaveUp: "lines" };
|
|
1636
|
+
}
|
|
1637
|
+
linesLeft -= 1;
|
|
1119
1638
|
this.forget("debug_enter");
|
|
1120
1639
|
this.send("step", [], this.held.thread);
|
|
1121
1640
|
if (await this.next(GameDebugger.named("debug_enter"), STEP_MS) === null) {
|
|
@@ -1123,7 +1642,25 @@ class GameDebugger {
|
|
|
1123
1642
|
}
|
|
1124
1643
|
frames = await this.stack();
|
|
1125
1644
|
}
|
|
1126
|
-
return frames;
|
|
1645
|
+
return { frames };
|
|
1646
|
+
}
|
|
1647
|
+
async toTheFrameEnd() {
|
|
1648
|
+
const held = this.held;
|
|
1649
|
+
if (held === null) {
|
|
1650
|
+
return null;
|
|
1651
|
+
}
|
|
1652
|
+
const line = frameEndLine();
|
|
1653
|
+
const theirs = this.breakpointLines.has(`${RUNTIME_AUTOLOAD}:${String(line)}`);
|
|
1654
|
+
if (!theirs) {
|
|
1655
|
+
this.send("breakpoint", [RUNTIME_AUTOLOAD, line, true], held.thread);
|
|
1656
|
+
}
|
|
1657
|
+
this.forget("debug_enter");
|
|
1658
|
+
this.send("continue", [], held.thread);
|
|
1659
|
+
const stopped = await this.next(GameDebugger.named("debug_enter"), STEP_MS);
|
|
1660
|
+
if (!theirs) {
|
|
1661
|
+
this.send("breakpoint", [RUNTIME_AUTOLOAD, line, false], this.held?.thread ?? MAIN_THREAD);
|
|
1662
|
+
}
|
|
1663
|
+
return stopped === null ? "lost" : await this.stack();
|
|
1127
1664
|
}
|
|
1128
1665
|
stoppedBy(cause) {
|
|
1129
1666
|
if (this.held !== null) {
|
|
@@ -1153,23 +1690,26 @@ class GameDebugger {
|
|
|
1153
1690
|
note: `The game had not stopped again ${STEP_MS / 1000}s after the step: the step is still running, as one out of a function that never returns does, or it went on without reaching another line. debug_state stack answers once it stops, and debug_control pause stops it wherever it is.`
|
|
1154
1691
|
};
|
|
1155
1692
|
}
|
|
1156
|
-
const
|
|
1157
|
-
if (
|
|
1693
|
+
const past = await this.pastTheAddon(Date.now() + STEP_MS);
|
|
1694
|
+
if (past === "lost") {
|
|
1158
1695
|
return {
|
|
1159
1696
|
stepped: true,
|
|
1160
1697
|
heldAgain: false,
|
|
1161
|
-
note: `The step stopped in gdharness's runtime addon and was
|
|
1698
|
+
note: `The step stopped in gdharness's runtime addon and was let go on to reach the game's own script, and did not stop again within ${STEP_MS / 1000}s. It stops at the next line of script the game runs; debug_state stack reads it then.`
|
|
1162
1699
|
};
|
|
1163
1700
|
}
|
|
1164
1701
|
this.stoppedBy("Stepped");
|
|
1165
|
-
const stack = frames ?? [];
|
|
1702
|
+
const stack = past.frames ?? [];
|
|
1166
1703
|
const top = stack[0];
|
|
1167
1704
|
const onBreakpoint = top !== undefined && this.breakpointLines.has(`${top.script}:${String(top.line)}`);
|
|
1168
1705
|
return {
|
|
1169
1706
|
stepped: true,
|
|
1170
1707
|
heldAgain: true,
|
|
1171
1708
|
held: { ...this.heldState(), ...onBreakpoint ? { onBreakpoint: true } : {} },
|
|
1172
|
-
stack
|
|
1709
|
+
stack,
|
|
1710
|
+
...past.gaveUp === undefined ? {} : {
|
|
1711
|
+
note: `The step went on through gdharness's runtime addon, whose _process runs first in every frame, for ${STEP_MS / 1000}s without the game running a line of its own script, and is held in the addon. debug_control continue lets the game go, and a breakpoint in its script stops it there.`
|
|
1712
|
+
}
|
|
1173
1713
|
};
|
|
1174
1714
|
}
|
|
1175
1715
|
async variables(frame) {
|
|
@@ -1181,10 +1721,9 @@ class GameDebugger {
|
|
|
1181
1721
|
if (frames === null) {
|
|
1182
1722
|
return { refused: `The game did not send its stack within ${READ_MS / 1000}s.` };
|
|
1183
1723
|
}
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
};
|
|
1724
|
+
const missing = GameDebugger.noFrame(frames, frame);
|
|
1725
|
+
if (missing !== null) {
|
|
1726
|
+
return missing;
|
|
1188
1727
|
}
|
|
1189
1728
|
this.forget("stack_frame_vars");
|
|
1190
1729
|
this.forget("stack_frame_var");
|
|
@@ -1208,15 +1747,66 @@ class GameDebugger {
|
|
|
1208
1747
|
};
|
|
1209
1748
|
}
|
|
1210
1749
|
const [name, scope, type, value, hint] = one.data;
|
|
1211
|
-
const
|
|
1212
|
-
|
|
1213
|
-
|
|
1750
|
+
const named = typeof name === "string" ? name : shownValue(name ?? null);
|
|
1751
|
+
if (named.startsWith("@")) {
|
|
1752
|
+
continue;
|
|
1753
|
+
}
|
|
1754
|
+
scopes[typeof scope === "number" ? scope : 0]?.variables.push({
|
|
1755
|
+
name: named,
|
|
1214
1756
|
value: shownValue(value ?? null, typeof hint === "string" ? hint : ""),
|
|
1215
|
-
type:
|
|
1757
|
+
type: typeNamed(type, hint)
|
|
1216
1758
|
});
|
|
1217
1759
|
}
|
|
1218
1760
|
return { held: this.heldState(), frame, scopes };
|
|
1219
1761
|
}
|
|
1762
|
+
async evaluate(frame, expression) {
|
|
1763
|
+
const held = this.held;
|
|
1764
|
+
if (held === null) {
|
|
1765
|
+
return this.notHeld();
|
|
1766
|
+
}
|
|
1767
|
+
const read = await this.variables(frame);
|
|
1768
|
+
if (!Array.isArray(read["scopes"])) {
|
|
1769
|
+
return read;
|
|
1770
|
+
}
|
|
1771
|
+
const members = read["scopes"].find((scope) => scope.name === "Members");
|
|
1772
|
+
if (members?.variables.some((variable) => variable.name === "self") !== true) {
|
|
1773
|
+
return {
|
|
1774
|
+
refused: `Frame ${frame} has no instance of its script to evaluate on, as a static function called on its class has none, and the engine lets the game go on rather than answer one there, so it was not asked. Evaluate in a frame below it, or read this one's locals with debug_state variables.`
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
this.forget("evaluation_return");
|
|
1778
|
+
this.send("evaluate", [expression, frame], held.thread);
|
|
1779
|
+
const answered = await this.next(GameDebugger.named("evaluation_return"), READ_MS);
|
|
1780
|
+
if (answered === null || !("data" in answered)) {
|
|
1781
|
+
return {
|
|
1782
|
+
refused: `The game worked out nothing for ${expression} within ${READ_MS / 1000}s. Ask again.`
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
const [, , type, value, hint] = answered.data;
|
|
1786
|
+
return {
|
|
1787
|
+
held: this.heldState(),
|
|
1788
|
+
frame,
|
|
1789
|
+
expression,
|
|
1790
|
+
value: shownValue(value ?? null, typeof hint === "string" ? hint : ""),
|
|
1791
|
+
type: typeNamed(type, hint),
|
|
1792
|
+
...value === null || value === undefined ? {
|
|
1793
|
+
note: "null is also what an expression comes to when it does not parse or fails as it runs. The game prints why as an error, which editor_output reads, since its debugger hands errors over apart from the console debug_state output keeps."
|
|
1794
|
+
} : {}
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
async taken() {
|
|
1798
|
+
this.forget("scene:inspect_object");
|
|
1799
|
+
this.send("scene:inspect_object", [0], this.held?.thread ?? MAIN_THREAD);
|
|
1800
|
+
return await this.next(GameDebugger.named("scene:inspect_object"), READ_MS) !== null;
|
|
1801
|
+
}
|
|
1802
|
+
static noFrame(frames, frame) {
|
|
1803
|
+
if (frames.find((each) => each.id === frame) !== undefined) {
|
|
1804
|
+
return null;
|
|
1805
|
+
}
|
|
1806
|
+
return {
|
|
1807
|
+
refused: frames.length === 0 ? "The game is held where no script runs, so it has no frames: debug_control step_over takes it to the next line of script." : `The stack has no frame ${frame}; its frames are ${frames.map((each) => each.id).join(", ")}.`
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1220
1810
|
close() {
|
|
1221
1811
|
this.connection?.destroy();
|
|
1222
1812
|
this.server.close();
|
|
@@ -1225,11 +1815,11 @@ class GameDebugger {
|
|
|
1225
1815
|
|
|
1226
1816
|
// src/keeper-channel.ts
|
|
1227
1817
|
import { randomUUID } from "node:crypto";
|
|
1228
|
-
import { mkdtempSync as
|
|
1818
|
+
import { mkdtempSync as mkdtempSync3, rmSync as rmSync3 } from "node:fs";
|
|
1229
1819
|
import { connect, createServer as createServer2 } from "node:net";
|
|
1230
|
-
import { tmpdir as
|
|
1231
|
-
import { join as
|
|
1232
|
-
import
|
|
1820
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
1821
|
+
import { join as join6 } from "node:path";
|
|
1822
|
+
import process5 from "node:process";
|
|
1233
1823
|
var DEBUG_PREFIX = "debug ";
|
|
1234
1824
|
function endedOrWhyNot(end) {
|
|
1235
1825
|
try {
|
|
@@ -1256,8 +1846,8 @@ function errorText(error) {
|
|
|
1256
1846
|
async function listenForTheServer(end, debug) {
|
|
1257
1847
|
let directory = null;
|
|
1258
1848
|
try {
|
|
1259
|
-
directory =
|
|
1260
|
-
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");
|
|
1261
1851
|
const server = createServer2((socket) => {
|
|
1262
1852
|
socket.setEncoding("utf8");
|
|
1263
1853
|
socket.on("error", () => {});
|
|
@@ -1293,13 +1883,13 @@ async function listenForTheServer(end, debug) {
|
|
|
1293
1883
|
close: () => {
|
|
1294
1884
|
server.close();
|
|
1295
1885
|
if (made !== null) {
|
|
1296
|
-
|
|
1886
|
+
rmSync3(made, { recursive: true, force: true });
|
|
1297
1887
|
}
|
|
1298
1888
|
}
|
|
1299
1889
|
};
|
|
1300
1890
|
} catch {
|
|
1301
1891
|
if (directory !== null) {
|
|
1302
|
-
|
|
1892
|
+
rmSync3(directory, { recursive: true, force: true });
|
|
1303
1893
|
}
|
|
1304
1894
|
return null;
|
|
1305
1895
|
}
|
|
@@ -1315,7 +1905,7 @@ function listening(server, address) {
|
|
|
1315
1905
|
}
|
|
1316
1906
|
|
|
1317
1907
|
// src/outside.ts
|
|
1318
|
-
import { fileURLToPath } from "node:url";
|
|
1908
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1319
1909
|
function withChanges(base, changes) {
|
|
1320
1910
|
const env = { ...base };
|
|
1321
1911
|
for (const [name, value] of Object.entries(changes)) {
|
|
@@ -1337,7 +1927,7 @@ async function receivedSpec() {
|
|
|
1337
1927
|
}
|
|
1338
1928
|
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
1339
1929
|
}
|
|
1340
|
-
var KEEPER_SCRIPT =
|
|
1930
|
+
var KEEPER_SCRIPT = fileURLToPath2(new URL("./keeper.js", import.meta.url));
|
|
1341
1931
|
function readLaunched(printed) {
|
|
1342
1932
|
const line = printed.split(`
|
|
1343
1933
|
`).find((one) => one.trim() !== "")?.trim() ?? "";
|
|
@@ -1350,28 +1940,12 @@ function readLaunched(printed) {
|
|
|
1350
1940
|
|
|
1351
1941
|
// src/run-record.ts
|
|
1352
1942
|
import { createHash as createHash2 } from "node:crypto";
|
|
1353
|
-
import { mkdirSync, openSync as openSync2, readdirSync, readFileSync as
|
|
1354
|
-
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";
|
|
1355
1945
|
|
|
1356
|
-
// src/
|
|
1357
|
-
import {
|
|
1358
|
-
import { basename
|
|
1359
|
-
function realAsFarAsItExists(path) {
|
|
1360
|
-
const missing = [];
|
|
1361
|
-
let at = path;
|
|
1362
|
-
for (;; ) {
|
|
1363
|
-
try {
|
|
1364
|
-
return join3(realpathSync.native(at), ...missing);
|
|
1365
|
-
} catch {
|
|
1366
|
-
const parent = dirname(at);
|
|
1367
|
-
if (parent === at) {
|
|
1368
|
-
return path;
|
|
1369
|
-
}
|
|
1370
|
-
missing.unshift(basename(at));
|
|
1371
|
-
at = parent;
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1946
|
+
// src/process-children.ts
|
|
1947
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
1948
|
+
import { basename as basename2 } from "node:path";
|
|
1375
1949
|
|
|
1376
1950
|
// src/windows-ask.ts
|
|
1377
1951
|
var SCRIPT = [
|
|
@@ -1386,473 +1960,307 @@ var SCRIPT = [
|
|
|
1386
1960
|
" } catch {",
|
|
1387
1961
|
" $said = $_.Exception.Message",
|
|
1388
1962
|
' $tag = "ERR"',
|
|
1389
|
-
" }",
|
|
1390
|
-
' [Console]::Out.Write($parts[0] + " " + $tag + " " + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($said)) + "`n")',
|
|
1391
|
-
" [Console]::Out.Flush()",
|
|
1392
|
-
"}"
|
|
1393
|
-
].join(`
|
|
1394
|
-
`);
|
|
1395
|
-
|
|
1396
|
-
// src/runtime-client.ts
|
|
1397
|
-
import { tmpdir as tmpdir3 } from "node:os";
|
|
1398
|
-
import { dirname as dirname2, join as join4, resolve as resolve2 } from "node:path";
|
|
1399
|
-
|
|
1400
|
-
// src/launch.ts
|
|
1401
|
-
function envValue(name, variables = process.env) {
|
|
1402
|
-
const value = variables[name];
|
|
1403
|
-
return value === undefined || value === "" ? undefined : value;
|
|
1404
|
-
}
|
|
1405
|
-
var ENGINE_FLAGS = Object.freeze({
|
|
1406
|
-
"--verbose": "none",
|
|
1407
|
-
"--quiet": "none",
|
|
1408
|
-
"--debug-collisions": "none",
|
|
1409
|
-
"--debug-paths": "none",
|
|
1410
|
-
"--debug-navigation": "none",
|
|
1411
|
-
"--debug-avoidance": "none",
|
|
1412
|
-
"--debug-stringnames": "none",
|
|
1413
|
-
"--debug-canvas-item-redraw": "none",
|
|
1414
|
-
"--debug-mute-audio": "none",
|
|
1415
|
-
"--print-fps": "none",
|
|
1416
|
-
"--gpu-profile": "none",
|
|
1417
|
-
"--gpu-validation": "none",
|
|
1418
|
-
"--gpu-abort": "none",
|
|
1419
|
-
"--disable-vsync": "none",
|
|
1420
|
-
"--frame-delay": "number",
|
|
1421
|
-
"--time-scale": "number",
|
|
1422
|
-
"--fixed-fps": "number",
|
|
1423
|
-
"--max-fps": "number",
|
|
1424
|
-
"--resolution": "size",
|
|
1425
|
-
"--write-movie": "file",
|
|
1426
|
-
"--rendering-driver": "driver",
|
|
1427
|
-
"--rendering-method": "method",
|
|
1428
|
-
"--accessibility": "accessibility"
|
|
1429
|
-
});
|
|
1430
|
-
var SET_BY_ARGUMENT = Object.freeze({
|
|
1431
|
-
"--headless": "headless",
|
|
1432
|
-
"--audio-driver": "silent",
|
|
1433
|
-
"--quit-after": "frames, on editor_run check"
|
|
1434
|
-
});
|
|
1435
|
-
|
|
1436
|
-
// src/runtime-client.ts
|
|
1437
|
-
function runtimeDirectory(variables = process.env) {
|
|
1438
|
-
const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
|
|
1439
|
-
if (explicit) {
|
|
1440
|
-
return explicit;
|
|
1441
|
-
}
|
|
1442
|
-
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
1443
|
-
return join4(perUser ?? tmpdir3(), "gdharness");
|
|
1444
|
-
}
|
|
1445
|
-
var ERROR_REPORT_KEEP_MS = 60 * 60 * 1000;
|
|
1446
|
-
var verdicts = new Map;
|
|
1447
|
-
var LATE_ANSWER_MS = 30 * 60000;
|
|
1448
|
-
var lateAnswers = new Map;
|
|
1449
|
-
var untaken = new Map;
|
|
1450
|
-
|
|
1451
|
-
// src/whole-file.ts
|
|
1452
|
-
import { renameSync, rmSync as rmSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1453
|
-
var HELD_FILE_MS = 1000;
|
|
1454
|
-
var HELD_FILE_RETRY_MS = 10;
|
|
1455
|
-
var HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
1456
|
-
function writeWhole(path, text) {
|
|
1457
|
-
const partial = `${path}.${process.pid}.partial`;
|
|
1458
|
-
try {
|
|
1459
|
-
writeFileSync2(partial, text, "utf8");
|
|
1460
|
-
} catch {
|
|
1461
|
-
return false;
|
|
1462
|
-
}
|
|
1463
|
-
const until = Date.now() + HELD_FILE_MS;
|
|
1464
|
-
for (;; ) {
|
|
1465
|
-
try {
|
|
1466
|
-
renameSync(partial, path);
|
|
1467
|
-
return true;
|
|
1468
|
-
} catch (error) {
|
|
1469
|
-
const code = error.code ?? "";
|
|
1470
|
-
if (!HELD_BY_A_READER.has(code) || Date.now() >= until) {
|
|
1471
|
-
try {
|
|
1472
|
-
rmSync3(partial, { force: true });
|
|
1473
|
-
} catch {}
|
|
1474
|
-
return false;
|
|
1475
|
-
}
|
|
1476
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, HELD_FILE_RETRY_MS);
|
|
1477
|
-
}
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
// src/run-record.ts
|
|
1482
|
-
function runsDirectory() {
|
|
1483
|
-
return join5(runtimeDirectory(), "runs");
|
|
1484
|
-
}
|
|
1485
|
-
function projectKey(projectPath) {
|
|
1486
|
-
const real = realAsFarAsItExists(resolve3(projectPath)).replace(/[\\/]+$/, "");
|
|
1487
|
-
const folded = process.platform === "win32" || process.platform === "darwin" ? real.toLowerCase() : real;
|
|
1488
|
-
return createHash2("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
|
|
1489
|
-
}
|
|
1490
|
-
function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
1491
|
-
return join5(directory, "runs", `note-${projectKey(projectPath)}.json`);
|
|
1492
|
-
}
|
|
1493
|
-
function writeRunRecord(record) {
|
|
1494
|
-
mkdirSync(runsDirectory(), { recursive: true });
|
|
1495
|
-
const path = runRecordPath(record.projectPath);
|
|
1496
|
-
if (!writeWhole(path, JSON.stringify(record, null, 2))) {
|
|
1497
|
-
throw new Error(`the run note could not be written at ${path}`);
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
|
-
function recordRunEnded(projectPath, pid, ending) {
|
|
1501
|
-
const path = runRecordPath(projectPath);
|
|
1502
|
-
const record = recordAt(path);
|
|
1503
|
-
if (record === null || record.pid !== pid) {
|
|
1504
|
-
return;
|
|
1505
|
-
}
|
|
1506
|
-
const written = {
|
|
1507
|
-
...record,
|
|
1508
|
-
...ending.exitCode === null ? {} : { exitCode: ending.exitCode },
|
|
1509
|
-
...ending.exitSignal === null ? {} : { exitSignal: ending.exitSignal },
|
|
1510
|
-
endedAt: Date.now()
|
|
1511
|
-
};
|
|
1512
|
-
writeWhole(path, JSON.stringify(written, null, 2));
|
|
1513
|
-
}
|
|
1514
|
-
function recordAt(path) {
|
|
1515
|
-
let parsed;
|
|
1516
|
-
try {
|
|
1517
|
-
parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
1518
|
-
} catch {
|
|
1519
|
-
return null;
|
|
1520
|
-
}
|
|
1521
|
-
if (!parsed || typeof parsed !== "object") {
|
|
1522
|
-
return null;
|
|
1523
|
-
}
|
|
1524
|
-
const fields = parsed;
|
|
1525
|
-
const pid = fields["pid"];
|
|
1526
|
-
const transcript = fields["transcript"];
|
|
1527
|
-
const startedAt = fields["startedAt"];
|
|
1528
|
-
if (typeof pid !== "number" || typeof transcript !== "string" || typeof startedAt !== "number") {
|
|
1529
|
-
return null;
|
|
1530
|
-
}
|
|
1531
|
-
const args = fields["arguments"];
|
|
1532
|
-
const startedBy = fields["startedBy"];
|
|
1533
|
-
const projectPath = fields["projectPath"];
|
|
1534
|
-
const command = fields["command"];
|
|
1535
|
-
if (typeof startedBy !== "number" || typeof projectPath !== "string" || projectPath === "" || typeof command !== "string" || !Array.isArray(args)) {
|
|
1536
|
-
return null;
|
|
1537
|
-
}
|
|
1538
|
-
return {
|
|
1539
|
-
pid,
|
|
1540
|
-
transcript,
|
|
1541
|
-
startedAt,
|
|
1542
|
-
startedBy,
|
|
1543
|
-
projectPath,
|
|
1544
|
-
...typeof fields["servedBy"] === "string" && fields["servedBy"] !== "" ? { servedBy: fields["servedBy"] } : {},
|
|
1545
|
-
...typeof fields["keeper"] === "string" && fields["keeper"] !== "" ? { keeper: fields["keeper"] } : {},
|
|
1546
|
-
...typeof fields["profile"] === "string" && fields["profile"] !== "" ? { profile: fields["profile"] } : {},
|
|
1547
|
-
arguments: args.filter((value) => typeof value === "string"),
|
|
1548
|
-
command,
|
|
1549
|
-
...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
|
|
1550
|
-
...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {},
|
|
1551
|
-
...typeof fields["endedAt"] === "number" ? { endedAt: fields["endedAt"] } : {}
|
|
1552
|
-
};
|
|
1553
|
-
}
|
|
1963
|
+
" }",
|
|
1964
|
+
' [Console]::Out.Write($parts[0] + " " + $tag + " " + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($said)) + "`n")',
|
|
1965
|
+
" [Console]::Out.Flush()",
|
|
1966
|
+
"}"
|
|
1967
|
+
].join(`
|
|
1968
|
+
`);
|
|
1554
1969
|
|
|
1555
|
-
// src/
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
const
|
|
1569
|
-
|
|
1570
|
-
|
|
1970
|
+
// src/process-children.ts
|
|
1971
|
+
async function linuxStartTimes(pids) {
|
|
1972
|
+
const began = new Map;
|
|
1973
|
+
const hertz = await ticksPerSecond();
|
|
1974
|
+
let booted;
|
|
1975
|
+
try {
|
|
1976
|
+
booted = Number(/^btime\s+(\d+)$/m.exec(readFileSync4("/proc/stat", "utf8"))?.[1] ?? Number.NaN) * 1000;
|
|
1977
|
+
} catch {
|
|
1978
|
+
return began;
|
|
1979
|
+
}
|
|
1980
|
+
if (hertz === null || Number.isNaN(booted)) {
|
|
1981
|
+
return began;
|
|
1982
|
+
}
|
|
1983
|
+
for (const pid of pids) {
|
|
1984
|
+
let stat;
|
|
1985
|
+
try {
|
|
1986
|
+
stat = readFileSync4(`/proc/${String(pid)}/stat`, "utf8");
|
|
1987
|
+
} catch {
|
|
1571
1988
|
continue;
|
|
1572
1989
|
}
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
left.push({ path, reason: error instanceof Error ? error.message : String(error) });
|
|
1990
|
+
const ticks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
|
|
1991
|
+
if (Number.isInteger(ticks) && ticks >= 0) {
|
|
1992
|
+
began.set(pid, booted + Math.round(ticks * 1000 / hertz));
|
|
1577
1993
|
}
|
|
1578
1994
|
}
|
|
1579
|
-
return
|
|
1995
|
+
return began;
|
|
1580
1996
|
}
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1997
|
+
var ticksAnswered;
|
|
1998
|
+
async function ticksPerSecond() {
|
|
1999
|
+
if (ticksAnswered !== undefined) {
|
|
2000
|
+
return ticksAnswered;
|
|
1585
2001
|
}
|
|
1586
|
-
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
function tryAgain(path, attempt) {
|
|
1592
|
-
if (attempt === 0) {
|
|
1593
|
-
if (waiting.has(path)) {
|
|
1594
|
-
return;
|
|
2002
|
+
try {
|
|
2003
|
+
const hertz = Number((await runHidden("getconf", ["CLK_TCK"], { timeout: ASK_TIMEOUT_MS })).stdout.trim());
|
|
2004
|
+
if (Number.isInteger(hertz) && hertz > 0) {
|
|
2005
|
+
ticksAnswered = hertz;
|
|
2006
|
+
return hertz;
|
|
1595
2007
|
}
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
2008
|
+
} catch {}
|
|
2009
|
+
return null;
|
|
2010
|
+
}
|
|
2011
|
+
var ASK_TIMEOUT_MS = 15000;
|
|
2012
|
+
var ONE_SHOT_OPTIONS = [
|
|
2013
|
+
"--import",
|
|
2014
|
+
"--quit",
|
|
2015
|
+
"--quit-after",
|
|
2016
|
+
"--export-release",
|
|
2017
|
+
"--export-debug",
|
|
2018
|
+
"--export-pack",
|
|
2019
|
+
"--export-patch",
|
|
2020
|
+
"--doctool",
|
|
2021
|
+
"--gdscript-docs",
|
|
2022
|
+
"--script",
|
|
2023
|
+
"-s",
|
|
2024
|
+
"--check-only",
|
|
2025
|
+
"--build-solutions",
|
|
2026
|
+
"--dump-gdextension-interface",
|
|
2027
|
+
"--dump-extension-api",
|
|
2028
|
+
"--dump-extension-api-with-docs",
|
|
2029
|
+
"--validate-extension-api",
|
|
2030
|
+
"--convert-3to4",
|
|
2031
|
+
"--validate-conversion-3to4"
|
|
2032
|
+
];
|
|
2033
|
+
function readCommandLine(command) {
|
|
2034
|
+
const all = commandWords(command);
|
|
2035
|
+
const separator = all.findIndex((word) => word === "--" || word === "++");
|
|
2036
|
+
const words = separator === -1 ? all : all.slice(0, separator);
|
|
2037
|
+
const executable = words[0] === undefined ? "" : basename2(words[0].replaceAll("\\", "/"));
|
|
2038
|
+
const at = words.indexOf("--path");
|
|
2039
|
+
let projectPath = null;
|
|
2040
|
+
if (at !== -1 && at + 1 < words.length) {
|
|
2041
|
+
const taken = [];
|
|
2042
|
+
for (const word of words.slice(at + 1)) {
|
|
2043
|
+
if (taken.length > 0 && (word.startsWith("-") || /^[a-z]+:\/\//.test(word))) {
|
|
2044
|
+
break;
|
|
2045
|
+
}
|
|
2046
|
+
taken.push(word);
|
|
1605
2047
|
}
|
|
1606
|
-
|
|
2048
|
+
projectPath = taken.join(" ").replaceAll("%20", " ");
|
|
1607
2049
|
}
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
}
|
|
1615
|
-
}, after).unref();
|
|
2050
|
+
return {
|
|
2051
|
+
executable,
|
|
2052
|
+
projectPath,
|
|
2053
|
+
editor: words.some((word) => word === "-e" || word === "--editor"),
|
|
2054
|
+
oneShot: words.find((word) => ONE_SHOT_OPTIONS.includes(word)) ?? null
|
|
2055
|
+
};
|
|
1616
2056
|
}
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
2057
|
+
function commandWords(command) {
|
|
2058
|
+
const words = [];
|
|
2059
|
+
let word = "";
|
|
2060
|
+
let quoted = false;
|
|
2061
|
+
let started = false;
|
|
2062
|
+
for (const char of command) {
|
|
2063
|
+
if (char === '"') {
|
|
2064
|
+
quoted = !quoted;
|
|
2065
|
+
started = true;
|
|
2066
|
+
} else if (!quoted && /\s/.test(char)) {
|
|
2067
|
+
if (started) {
|
|
2068
|
+
words.push(word);
|
|
2069
|
+
word = "";
|
|
2070
|
+
started = false;
|
|
2071
|
+
}
|
|
2072
|
+
} else {
|
|
2073
|
+
word += char;
|
|
2074
|
+
started = true;
|
|
1628
2075
|
}
|
|
1629
|
-
}
|
|
2076
|
+
}
|
|
2077
|
+
if (started) {
|
|
2078
|
+
words.push(word);
|
|
2079
|
+
}
|
|
2080
|
+
return words;
|
|
1630
2081
|
}
|
|
1631
2082
|
|
|
1632
|
-
// src/
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
var ANNOTATIONS = "(?:@[\\p{L}_][\\p{L}\\p{N}_]*(?:\\([^)]*\\))?\\s+)*";
|
|
1636
|
-
var DECLARATION_LINE = new RegExp(`^(${ANNOTATIONS})(static\\s+)?(func|var|const|signal|enum|class)\\s+(${NAME})`, "u");
|
|
1637
|
-
var CLASS_NAME_LINE = new RegExp(`^${ANNOTATIONS}class_name\\s+(${NAME})`, "u");
|
|
1638
|
-
var EXTENDS_LINE = new RegExp(`^${ANNOTATIONS}(?:class_name\\s+${NAME}\\s+)?extends\\s`, "u");
|
|
1639
|
-
var FUNC_LINE = new RegExp(`^[ \\t]*${ANNOTATIONS}(?:static\\s+)?func\\s+(${NAME})\\s*\\(`, "u");
|
|
1640
|
-
var LAMBDA_HEADER = new RegExp(`(?<![\\p{L}\\p{N}_])func(?:\\s+${NAME})?\\s*\\(`, "u");
|
|
1641
|
-
var DOTTED = `${NAME}(?:\\.${NAME})*`;
|
|
1642
|
-
var MEMBER_KINDS = new Set(["var", "const", "signal", "enum", "enumValue"]);
|
|
1643
|
-
var KEYWORDS = new Set([
|
|
1644
|
-
"and",
|
|
1645
|
-
"as",
|
|
1646
|
-
"assert",
|
|
1647
|
-
"await",
|
|
1648
|
-
"break",
|
|
1649
|
-
"breakpoint",
|
|
1650
|
-
"class",
|
|
1651
|
-
"class_name",
|
|
1652
|
-
"const",
|
|
1653
|
-
"continue",
|
|
1654
|
-
"elif",
|
|
1655
|
-
"else",
|
|
1656
|
-
"enum",
|
|
1657
|
-
"extends",
|
|
1658
|
-
"false",
|
|
1659
|
-
"for",
|
|
1660
|
-
"func",
|
|
1661
|
-
"if",
|
|
1662
|
-
"in",
|
|
1663
|
-
"is",
|
|
1664
|
-
"match",
|
|
1665
|
-
"namespace",
|
|
1666
|
-
"not",
|
|
1667
|
-
"null",
|
|
1668
|
-
"or",
|
|
1669
|
-
"pass",
|
|
1670
|
-
"preload",
|
|
1671
|
-
"return",
|
|
1672
|
-
"self",
|
|
1673
|
-
"signal",
|
|
1674
|
-
"static",
|
|
1675
|
-
"super",
|
|
1676
|
-
"trait",
|
|
1677
|
-
"true",
|
|
1678
|
-
"var",
|
|
1679
|
-
"void",
|
|
1680
|
-
"when",
|
|
1681
|
-
"while",
|
|
1682
|
-
"yield",
|
|
1683
|
-
"INF",
|
|
1684
|
-
"NAN",
|
|
1685
|
-
"PI",
|
|
1686
|
-
"TAU"
|
|
1687
|
-
]);
|
|
1688
|
-
|
|
1689
|
-
// src/answer-json.ts
|
|
1690
|
-
var SCALAR = String.raw`(?:-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null)`;
|
|
1691
|
-
var SCALAR_ARRAY = new RegExp(String.raw`\[\n\s+(${SCALAR}(?:,\n\s+${SCALAR})*)\n\s*\]`, "g");
|
|
2083
|
+
// src/runtime-client.ts
|
|
2084
|
+
import { tmpdir as tmpdir4 } from "node:os";
|
|
2085
|
+
import { dirname as dirname3, join as join7, resolve as resolve2 } from "node:path";
|
|
1692
2086
|
|
|
1693
|
-
// src/
|
|
1694
|
-
function
|
|
1695
|
-
|
|
2087
|
+
// src/launch.ts
|
|
2088
|
+
function envValue(name, variables = process.env) {
|
|
2089
|
+
const value = variables[name];
|
|
2090
|
+
return value === undefined || value === "" ? undefined : value;
|
|
1696
2091
|
}
|
|
2092
|
+
var ENGINE_FLAGS = Object.freeze({
|
|
2093
|
+
"--verbose": "none",
|
|
2094
|
+
"--quiet": "none",
|
|
2095
|
+
"--debug-collisions": "none",
|
|
2096
|
+
"--debug-paths": "none",
|
|
2097
|
+
"--debug-navigation": "none",
|
|
2098
|
+
"--debug-avoidance": "none",
|
|
2099
|
+
"--debug-stringnames": "none",
|
|
2100
|
+
"--debug-canvas-item-redraw": "none",
|
|
2101
|
+
"--debug-mute-audio": "none",
|
|
2102
|
+
"--print-fps": "none",
|
|
2103
|
+
"--gpu-profile": "none",
|
|
2104
|
+
"--gpu-validation": "none",
|
|
2105
|
+
"--gpu-abort": "none",
|
|
2106
|
+
"--disable-vsync": "none",
|
|
2107
|
+
"--frame-delay": "number",
|
|
2108
|
+
"--time-scale": "number",
|
|
2109
|
+
"--fixed-fps": "number",
|
|
2110
|
+
"--max-fps": "number",
|
|
2111
|
+
"--resolution": "size",
|
|
2112
|
+
"--write-movie": "file",
|
|
2113
|
+
"--rendering-driver": "driver",
|
|
2114
|
+
"--rendering-method": "method",
|
|
2115
|
+
"--accessibility": "accessibility"
|
|
2116
|
+
});
|
|
2117
|
+
var SET_BY_ARGUMENT = Object.freeze({
|
|
2118
|
+
"--headless": "headless",
|
|
2119
|
+
"--audio-driver": "silent",
|
|
2120
|
+
"--quit-after": "frames, on editor_run check"
|
|
2121
|
+
});
|
|
1697
2122
|
|
|
1698
|
-
// src/
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
"png",
|
|
1704
|
-
"jpg",
|
|
1705
|
-
"jpeg",
|
|
1706
|
-
"webp",
|
|
1707
|
-
"svg",
|
|
1708
|
-
"bmp",
|
|
1709
|
-
"tga",
|
|
1710
|
-
"exr",
|
|
1711
|
-
"hdr",
|
|
1712
|
-
"ktx",
|
|
1713
|
-
"dds",
|
|
1714
|
-
"ttf",
|
|
1715
|
-
"otf",
|
|
1716
|
-
"woff",
|
|
1717
|
-
"woff2",
|
|
1718
|
-
"fnt",
|
|
1719
|
-
"wav",
|
|
1720
|
-
"mp3",
|
|
1721
|
-
"ogg",
|
|
1722
|
-
"glb",
|
|
1723
|
-
"gltf",
|
|
1724
|
-
"obj",
|
|
1725
|
-
"fbx",
|
|
1726
|
-
"blend",
|
|
1727
|
-
"dae",
|
|
1728
|
-
"ogv",
|
|
1729
|
-
"gdshader",
|
|
1730
|
-
"shader"
|
|
1731
|
-
]);
|
|
1732
|
-
var CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
|
|
1733
|
-
var BINARY_EXTENSIONS = new Set([
|
|
1734
|
-
...[...ASSET_EXTENSIONS].filter((extension) => !["svg", "gltf", "gdshader", "shader", "fnt"].includes(extension)),
|
|
1735
|
-
"scn",
|
|
1736
|
-
"res",
|
|
1737
|
-
"zip",
|
|
1738
|
-
"pck",
|
|
1739
|
-
"exe",
|
|
1740
|
-
"dll",
|
|
1741
|
-
"so",
|
|
1742
|
-
"dylib"
|
|
1743
|
-
]);
|
|
1744
|
-
|
|
1745
|
-
// src/resources.ts
|
|
1746
|
-
var STATIC_RESOURCES = [
|
|
1747
|
-
{
|
|
1748
|
-
uri: "godot://project/info",
|
|
1749
|
-
name: "Project Info",
|
|
1750
|
-
description: "Parsed Godot project.godot metadata as JSON.",
|
|
1751
|
-
mimeType: "application/json"
|
|
1752
|
-
}
|
|
1753
|
-
];
|
|
1754
|
-
var RESOURCE_TEMPLATES = [
|
|
1755
|
-
{
|
|
1756
|
-
uriTemplate: "godot://scene/{path}",
|
|
1757
|
-
name: "Scene File",
|
|
1758
|
-
description: "Read a Godot scene (.tscn) file from the current project.",
|
|
1759
|
-
mimeType: "text/plain"
|
|
1760
|
-
},
|
|
1761
|
-
{
|
|
1762
|
-
uriTemplate: "godot://script/{path}",
|
|
1763
|
-
name: "GDScript File",
|
|
1764
|
-
description: "Read a GDScript (.gd) file from the current project.",
|
|
1765
|
-
mimeType: "text/x-gdscript"
|
|
1766
|
-
},
|
|
1767
|
-
{
|
|
1768
|
-
uriTemplate: "godot://resource/{path}",
|
|
1769
|
-
name: "Resource File",
|
|
1770
|
-
description: "Read a project resource file (.tres, .tscn, .gd).",
|
|
1771
|
-
mimeType: "text/plain"
|
|
2123
|
+
// src/runtime-client.ts
|
|
2124
|
+
function runtimeDirectory(variables = process.env) {
|
|
2125
|
+
const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
|
|
2126
|
+
if (explicit) {
|
|
2127
|
+
return explicit;
|
|
1772
2128
|
}
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
2129
|
+
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
2130
|
+
return join7(perUser ?? tmpdir4(), "gdharness");
|
|
2131
|
+
}
|
|
2132
|
+
var ERROR_REPORT_KEEP_MS = 60 * 60 * 1000;
|
|
2133
|
+
var verdicts = new Map;
|
|
2134
|
+
var LATE_ANSWER_MS = 30 * 60000;
|
|
2135
|
+
var lateAnswers = new Map;
|
|
2136
|
+
var untaken = new Map;
|
|
2137
|
+
|
|
2138
|
+
// src/whole-file.ts
|
|
2139
|
+
import { renameSync, rmSync as rmSync4, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2140
|
+
var HELD_FILE_MS = 1000;
|
|
2141
|
+
var HELD_FILE_RETRY_MS = 10;
|
|
2142
|
+
var HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
2143
|
+
function writeWhole(path, text) {
|
|
2144
|
+
const partial = `${path}.${process.pid}.partial`;
|
|
2145
|
+
try {
|
|
2146
|
+
writeFileSync2(partial, text, "utf8");
|
|
2147
|
+
} catch {
|
|
2148
|
+
return false;
|
|
2149
|
+
}
|
|
2150
|
+
const until = Date.now() + HELD_FILE_MS;
|
|
2151
|
+
for (;; ) {
|
|
2152
|
+
try {
|
|
2153
|
+
renameSync(partial, path);
|
|
2154
|
+
return true;
|
|
2155
|
+
} catch (error) {
|
|
2156
|
+
const code = error.code ?? "";
|
|
2157
|
+
if (!HELD_BY_A_READER.has(code) || Date.now() >= until) {
|
|
2158
|
+
try {
|
|
2159
|
+
rmSync4(partial, { force: true });
|
|
2160
|
+
} catch {}
|
|
2161
|
+
return false;
|
|
2162
|
+
}
|
|
2163
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, HELD_FILE_RETRY_MS);
|
|
1799
2164
|
}
|
|
1800
2165
|
}
|
|
1801
|
-
return depth <= 0 && !inString;
|
|
1802
2166
|
}
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
continue;
|
|
1822
|
-
}
|
|
1823
|
-
const key = line.slice(0, eqIndex).trim();
|
|
1824
|
-
let rawValue = line.slice(eqIndex + 1).trim();
|
|
1825
|
-
while (index + 1 < lines.length && !isValueComplete(rawValue)) {
|
|
1826
|
-
index += 1;
|
|
1827
|
-
rawValue += `
|
|
1828
|
-
${(lines[index] ?? "").trim()}`;
|
|
1829
|
-
}
|
|
1830
|
-
section[key] = parseIniLikeValue(rawValue);
|
|
2167
|
+
|
|
2168
|
+
// src/run-record.ts
|
|
2169
|
+
function runsDirectory() {
|
|
2170
|
+
return join8(runtimeDirectory(), "runs");
|
|
2171
|
+
}
|
|
2172
|
+
function projectKey(projectPath) {
|
|
2173
|
+
const real = realAsFarAsItExists(resolve3(projectPath)).replace(/[\\/]+$/, "");
|
|
2174
|
+
const folded = process.platform === "win32" || process.platform === "darwin" ? real.toLowerCase() : real;
|
|
2175
|
+
return createHash2("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
|
|
2176
|
+
}
|
|
2177
|
+
function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
2178
|
+
return join8(directory, "runs", `note-${projectKey(projectPath)}.json`);
|
|
2179
|
+
}
|
|
2180
|
+
function writeRunRecord(record) {
|
|
2181
|
+
mkdirSync(runsDirectory(), { recursive: true });
|
|
2182
|
+
const path = runRecordPath(record.projectPath);
|
|
2183
|
+
if (!writeWhole(path, JSON.stringify(record, null, 2))) {
|
|
2184
|
+
throw new Error(`the run note could not be written at ${path}`);
|
|
1831
2185
|
}
|
|
1832
|
-
return result;
|
|
1833
2186
|
}
|
|
1834
|
-
function
|
|
1835
|
-
|
|
2187
|
+
function recordRunEnded(projectPath, pid, ending) {
|
|
2188
|
+
const path = runRecordPath(projectPath);
|
|
2189
|
+
const record = recordAt(path);
|
|
2190
|
+
if (record === null || record.pid !== pid) {
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
const { endingAt: _looked, ...kept } = record;
|
|
2194
|
+
const written = {
|
|
2195
|
+
...kept,
|
|
2196
|
+
...ending.exitCode === null ? {} : { exitCode: ending.exitCode },
|
|
2197
|
+
...ending.exitSignal === null ? {} : { exitSignal: ending.exitSignal },
|
|
2198
|
+
...ending.notTold === true ? { exitNotTold: true } : {},
|
|
2199
|
+
endedAt: Date.now()
|
|
2200
|
+
};
|
|
2201
|
+
writeWhole(path, JSON.stringify(written, null, 2));
|
|
2202
|
+
}
|
|
2203
|
+
function recordAt(path) {
|
|
2204
|
+
let parsed;
|
|
2205
|
+
try {
|
|
2206
|
+
parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
2207
|
+
} catch {
|
|
1836
2208
|
return null;
|
|
1837
2209
|
}
|
|
1838
|
-
if (
|
|
1839
|
-
return
|
|
2210
|
+
if (!parsed || typeof parsed !== "object") {
|
|
2211
|
+
return null;
|
|
1840
2212
|
}
|
|
1841
|
-
|
|
1842
|
-
|
|
2213
|
+
const fields = parsed;
|
|
2214
|
+
const pid = fields["pid"];
|
|
2215
|
+
const transcript = fields["transcript"];
|
|
2216
|
+
const startedAt = fields["startedAt"];
|
|
2217
|
+
if (typeof pid !== "number" || typeof transcript !== "string" || typeof startedAt !== "number") {
|
|
2218
|
+
return null;
|
|
1843
2219
|
}
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
2220
|
+
const args = fields["arguments"];
|
|
2221
|
+
const startedBy = fields["startedBy"];
|
|
2222
|
+
const projectPath = fields["projectPath"];
|
|
2223
|
+
const command = fields["command"];
|
|
2224
|
+
if (typeof startedBy !== "number" || typeof projectPath !== "string" || projectPath === "" || typeof command !== "string" || !Array.isArray(args)) {
|
|
2225
|
+
return null;
|
|
1847
2226
|
}
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
2227
|
+
return {
|
|
2228
|
+
pid,
|
|
2229
|
+
transcript,
|
|
2230
|
+
startedAt,
|
|
2231
|
+
startedBy,
|
|
2232
|
+
projectPath,
|
|
2233
|
+
...typeof fields["servedBy"] === "string" && fields["servedBy"] !== "" ? { servedBy: fields["servedBy"] } : {},
|
|
2234
|
+
...typeof fields["keeper"] === "string" && fields["keeper"] !== "" ? { keeper: fields["keeper"] } : {},
|
|
2235
|
+
...typeof fields["keeperPid"] === "number" && Number.isInteger(fields["keeperPid"]) && fields["keeperPid"] > 0 ? { keeperPid: fields["keeperPid"] } : {},
|
|
2236
|
+
...typeof fields["profile"] === "string" && fields["profile"] !== "" ? { profile: fields["profile"] } : {},
|
|
2237
|
+
arguments: args.filter((value) => typeof value === "string"),
|
|
2238
|
+
command,
|
|
2239
|
+
...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
|
|
2240
|
+
...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {},
|
|
2241
|
+
...typeof fields["endedAt"] === "number" ? { endedAt: fields["endedAt"] } : {},
|
|
2242
|
+
...fields["exitNotTold"] === true ? { exitNotTold: true } : {},
|
|
2243
|
+
...Array.isArray(fields["restartedFrom"]) ? { restartedFrom: fields["restartedFrom"].flatMap(restartedFromAt) } : {},
|
|
2244
|
+
...typeof fields["endingAt"] === "number" ? { endingAt: fields["endingAt"] } : {}
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
function restartedFromAt(value) {
|
|
2248
|
+
if (!value || typeof value !== "object") {
|
|
2249
|
+
return [];
|
|
1851
2250
|
}
|
|
1852
|
-
|
|
1853
|
-
|
|
2251
|
+
const fields = value;
|
|
2252
|
+
const { pid, endedAt, exitCode, exitSignal } = fields;
|
|
2253
|
+
if (typeof pid !== "number" || typeof endedAt !== "number") {
|
|
2254
|
+
return [];
|
|
1854
2255
|
}
|
|
1855
|
-
return
|
|
2256
|
+
return [
|
|
2257
|
+
{
|
|
2258
|
+
pid,
|
|
2259
|
+
endedAt,
|
|
2260
|
+
...typeof exitCode === "number" ? { exitCode } : {},
|
|
2261
|
+
...typeof exitSignal === "string" ? { exitSignal } : {}
|
|
2262
|
+
}
|
|
2263
|
+
];
|
|
1856
2264
|
}
|
|
1857
2265
|
|
|
1858
2266
|
// src/script-profile.ts
|
|
@@ -2004,6 +2412,184 @@ class ProfileObserver {
|
|
|
2004
2412
|
}
|
|
2005
2413
|
}
|
|
2006
2414
|
|
|
2415
|
+
// src/successor.ts
|
|
2416
|
+
import { readlinkSync, realpathSync as realpathSync2 } from "node:fs";
|
|
2417
|
+
import { basename as basename4 } from "node:path";
|
|
2418
|
+
import { setTimeout as delay2 } from "node:timers/promises";
|
|
2419
|
+
var RESTARTED_WITHIN_MS = 2000;
|
|
2420
|
+
var LIST_WITHIN_MS = 15000;
|
|
2421
|
+
var POLL_MS = 250;
|
|
2422
|
+
function successorAmong(listed, exitedPid, engine, notBefore, exitedAt, secondsOnly) {
|
|
2423
|
+
const slack = secondsOnly ? 1000 : 0;
|
|
2424
|
+
const fold = (name) => process.platform === "win32" ? name.toLowerCase() : name;
|
|
2425
|
+
const wanted = fold(basename4(engine.replaceAll("\\", "/")));
|
|
2426
|
+
const found = listed.filter((one) => one.pid !== exitedPid && fold(readCommandLine(one.command).executable) === wanted && one.startedAt + slack >= notBefore && one.startedAt + slack >= exitedAt - RESTARTED_WITHIN_MS);
|
|
2427
|
+
return found.length === 1 ? found[0] ?? null : null;
|
|
2428
|
+
}
|
|
2429
|
+
async function followSuccessor(exitedPid, engine, notBefore, exitedAt, transcript) {
|
|
2430
|
+
if (process.platform === "win32") {
|
|
2431
|
+
return await followOnWindows(exitedPid, engine, notBefore, exitedAt);
|
|
2432
|
+
}
|
|
2433
|
+
return transcript === undefined ? null : await followOnPosix(exitedPid, engine, notBefore, exitedAt, transcript);
|
|
2434
|
+
}
|
|
2435
|
+
async function followOnWindows(exitedPid, engine, notBefore, exitedAt) {
|
|
2436
|
+
const script = [
|
|
2437
|
+
"[Console]::OutputEncoding = [Text.Encoding]::UTF8",
|
|
2438
|
+
"$held = @{}",
|
|
2439
|
+
`foreach ($c in @(Get-CimInstance Win32_Process -Filter "ParentProcessId=${exitedPid}" -Property ProcessId,CreationDate,CommandLine)) {`,
|
|
2440
|
+
" if (-not $c.CreationDate) { continue }",
|
|
2441
|
+
" $began = ([DateTimeOffset]$c.CreationDate).ToUnixTimeMilliseconds()",
|
|
2442
|
+
" $p = Get-Process -Id $c.ProcessId -ErrorAction SilentlyContinue",
|
|
2443
|
+
" if ($null -eq $p -or [Math]::Abs(([DateTimeOffset]$p.StartTime).ToUnixTimeMilliseconds() - $began) -gt 1000) { continue }",
|
|
2444
|
+
" $null = $p.Handle",
|
|
2445
|
+
" $held[[int]$c.ProcessId] = $p",
|
|
2446
|
+
' "$($c.ProcessId) $began $($c.CommandLine)"',
|
|
2447
|
+
"}",
|
|
2448
|
+
"'listed'",
|
|
2449
|
+
"$chosen = [Console]::In.ReadLine()",
|
|
2450
|
+
"if ($chosen -and $held.ContainsKey([int]$chosen)) { $p = $held[[int]$chosen]; $p.WaitForExit(); 'exit ' + $p.ExitCode }"
|
|
2451
|
+
].join(`
|
|
2452
|
+
`);
|
|
2453
|
+
const holder = spawnHidden("powershell.exe", [
|
|
2454
|
+
"-NoProfile",
|
|
2455
|
+
"-NonInteractive",
|
|
2456
|
+
"-ExecutionPolicy",
|
|
2457
|
+
"Bypass",
|
|
2458
|
+
"-EncodedCommand",
|
|
2459
|
+
Buffer.from(script, "utf16le").toString("base64")
|
|
2460
|
+
], { stdio: ["pipe", "pipe", "ignore"] });
|
|
2461
|
+
let said = "";
|
|
2462
|
+
const closed = new Promise((resolve) => {
|
|
2463
|
+
holder.once("close", () => {
|
|
2464
|
+
resolve();
|
|
2465
|
+
});
|
|
2466
|
+
holder.once("error", () => {
|
|
2467
|
+
resolve();
|
|
2468
|
+
});
|
|
2469
|
+
});
|
|
2470
|
+
const listed = await new Promise((resolve) => {
|
|
2471
|
+
const timer = setTimeout(() => {
|
|
2472
|
+
resolve(false);
|
|
2473
|
+
}, LIST_WITHIN_MS);
|
|
2474
|
+
holder.stdout.on("data", (chunk) => {
|
|
2475
|
+
said += chunk.toString("utf8");
|
|
2476
|
+
if (/^listed\r?$/m.test(said)) {
|
|
2477
|
+
clearTimeout(timer);
|
|
2478
|
+
resolve(true);
|
|
2479
|
+
}
|
|
2480
|
+
});
|
|
2481
|
+
closed.then(() => {
|
|
2482
|
+
clearTimeout(timer);
|
|
2483
|
+
resolve(/^listed\r?$/m.test(said));
|
|
2484
|
+
});
|
|
2485
|
+
});
|
|
2486
|
+
if (!listed) {
|
|
2487
|
+
holder.kill();
|
|
2488
|
+
return null;
|
|
2489
|
+
}
|
|
2490
|
+
const candidates = [];
|
|
2491
|
+
for (const line of said.split(/\r?\n/)) {
|
|
2492
|
+
const fields = /^(\d+)\s+(\d+)\s+(.*)$/.exec(line);
|
|
2493
|
+
if (fields?.[1] !== undefined && fields[2] !== undefined && fields[3] !== undefined) {
|
|
2494
|
+
candidates.push({ pid: Number(fields[1]), startedAt: Number(fields[2]), command: fields[3].trim() });
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
const successor = successorAmong(candidates, exitedPid, engine, notBefore, exitedAt, false);
|
|
2498
|
+
holder.stdin.end(successor === null ? `
|
|
2499
|
+
` : `${String(successor.pid)}
|
|
2500
|
+
`);
|
|
2501
|
+
if (successor === null) {
|
|
2502
|
+
return null;
|
|
2503
|
+
}
|
|
2504
|
+
const ended = closed.then(() => {
|
|
2505
|
+
const told = /^exit (-?\d+)\r?$/m.exec(said);
|
|
2506
|
+
return { code: told?.[1] === undefined ? null : Number(told[1]) };
|
|
2507
|
+
});
|
|
2508
|
+
return { successor, ended };
|
|
2509
|
+
}
|
|
2510
|
+
async function followOnPosix(exitedPid, engine, notBefore, exitedAt, transcript) {
|
|
2511
|
+
const linux = process.platform === "linux";
|
|
2512
|
+
let printed;
|
|
2513
|
+
try {
|
|
2514
|
+
({ stdout: printed } = await runHidden("ps", ["-e", "-o", linux ? "pid=,args=" : "pid=,lstart=,args="], {
|
|
2515
|
+
timeout: LIST_WITHIN_MS,
|
|
2516
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
2517
|
+
env: { ...process.env, LC_ALL: "C" }
|
|
2518
|
+
}));
|
|
2519
|
+
} catch {
|
|
2520
|
+
return null;
|
|
2521
|
+
}
|
|
2522
|
+
const listed = [];
|
|
2523
|
+
if (linux) {
|
|
2524
|
+
const commands = new Map;
|
|
2525
|
+
for (const line of printed.split(`
|
|
2526
|
+
`)) {
|
|
2527
|
+
const fields = /^\s*(\d+)\s+(.*)$/.exec(line);
|
|
2528
|
+
if (fields?.[1] !== undefined) {
|
|
2529
|
+
commands.set(Number(fields[1]), (fields[2] ?? "").trim());
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
for (const [pid, startedAt] of await linuxStartTimes([...commands.keys()])) {
|
|
2533
|
+
listed.push({ pid, startedAt, command: commands.get(pid) ?? "" });
|
|
2534
|
+
}
|
|
2535
|
+
} else {
|
|
2536
|
+
for (const line of printed.split(`
|
|
2537
|
+
`)) {
|
|
2538
|
+
const fields = /^\s*(\d+)\s+(\w+\s+\w+\s+\d+\s+[\d:]+\s+\d+)\s+(.*)$/.exec(line);
|
|
2539
|
+
const startedAt = Date.parse(fields?.[2] ?? "");
|
|
2540
|
+
if (fields?.[1] !== undefined && !Number.isNaN(startedAt)) {
|
|
2541
|
+
listed.push({ pid: Number(fields[1]), startedAt, command: (fields[3] ?? "").trim() });
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
const timely = listed.filter((one) => successorAmong([one], exitedPid, engine, notBefore, exitedAt, true) !== null);
|
|
2546
|
+
const ours = canonical(transcript);
|
|
2547
|
+
const candidates = [];
|
|
2548
|
+
for (const one of timely) {
|
|
2549
|
+
const writing = await outputOf(one.pid);
|
|
2550
|
+
if (writing !== null && canonical(writing) === ours) {
|
|
2551
|
+
candidates.push(one);
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
const successor = successorAmong(candidates, exitedPid, engine, notBefore, exitedAt, true);
|
|
2555
|
+
if (successor === null) {
|
|
2556
|
+
return null;
|
|
2557
|
+
}
|
|
2558
|
+
const ended = (async () => {
|
|
2559
|
+
while (alive(successor.pid)) {
|
|
2560
|
+
await delay2(POLL_MS);
|
|
2561
|
+
}
|
|
2562
|
+
return { code: null };
|
|
2563
|
+
})();
|
|
2564
|
+
return { successor, ended };
|
|
2565
|
+
}
|
|
2566
|
+
async function outputOf(pid) {
|
|
2567
|
+
if (process.platform === "linux") {
|
|
2568
|
+
try {
|
|
2569
|
+
return readlinkSync(`/proc/${String(pid)}/fd/1`);
|
|
2570
|
+
} catch {
|
|
2571
|
+
return null;
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
try {
|
|
2575
|
+
const { stdout } = await runHidden("lsof", ["-a", "-p", String(pid), "-d", "1", "-Fn"], {
|
|
2576
|
+
timeout: LIST_WITHIN_MS
|
|
2577
|
+
});
|
|
2578
|
+
const named = stdout.split(`
|
|
2579
|
+
`).find((line) => line.startsWith("n"));
|
|
2580
|
+
return named === undefined ? null : named.slice(1);
|
|
2581
|
+
} catch {
|
|
2582
|
+
return null;
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
function canonical(path) {
|
|
2586
|
+
try {
|
|
2587
|
+
return realpathSync2(path);
|
|
2588
|
+
} catch {
|
|
2589
|
+
return path;
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2007
2593
|
// src/keeper.ts
|
|
2008
2594
|
async function startDirectly(spec, output, hidden, detached = true, errors = output, directory = process.cwd()) {
|
|
2009
2595
|
return await new Promise((resolve) => {
|
|
@@ -2023,8 +2609,9 @@ async function startDirectly(spec, output, hidden, detached = true, errors = out
|
|
|
2023
2609
|
}
|
|
2024
2610
|
async function startThroughHelper(spec, output, stopFile) {
|
|
2025
2611
|
const scratch = scratchDirectory("desktop");
|
|
2026
|
-
const pidFile =
|
|
2027
|
-
const errorFile =
|
|
2612
|
+
const pidFile = join9(scratch, "pid");
|
|
2613
|
+
const errorFile = join9(scratch, "error");
|
|
2614
|
+
const stepFile = join9(scratch, "step");
|
|
2028
2615
|
const where = spec.desktop !== undefined ? `on the ${spec.desktop} desktop` : spec.noActivate === true ? "without activation" : "through the desktop helper";
|
|
2029
2616
|
try {
|
|
2030
2617
|
const compiled = await ensureHelperCompiled();
|
|
@@ -2032,10 +2619,11 @@ async function startThroughHelper(spec, output, stopFile) {
|
|
|
2032
2619
|
return { error: `it could not be started ${where}: ${compiled}` };
|
|
2033
2620
|
}
|
|
2034
2621
|
const wrapper = throughHelper({ ...spec, directory: process.cwd() }, spec.command, spec.args, {
|
|
2035
|
-
spec:
|
|
2622
|
+
spec: join9(scratch, "spec.json"),
|
|
2036
2623
|
pid: pidFile,
|
|
2037
2624
|
error: errorFile,
|
|
2038
|
-
stop: stopFile
|
|
2625
|
+
stop: stopFile,
|
|
2626
|
+
step: stepFile
|
|
2039
2627
|
});
|
|
2040
2628
|
const envChanges = spec.desktop === undefined ? spec.envChanges : { ...spec.envChanges, [DESKTOP_VARIABLE]: spec.desktop };
|
|
2041
2629
|
const started = await startDirectly({ ...spec, command: wrapper.command, args: wrapper.args, envChanges }, output, true, false, "ignore", tmpdir5());
|
|
@@ -2047,18 +2635,20 @@ async function startThroughHelper(spec, output, stopFile) {
|
|
|
2047
2635
|
exited.now = true;
|
|
2048
2636
|
});
|
|
2049
2637
|
for (let waited = 0;waited < DESKTOP_START_MS; waited += 50) {
|
|
2050
|
-
const pid = existsSync3(pidFile) ? Number(
|
|
2638
|
+
const pid = existsSync3(pidFile) ? Number(readFileSync6(pidFile, "utf8").trim()) : Number.NaN;
|
|
2051
2639
|
if (Number.isInteger(pid) && pid > 0) {
|
|
2052
2640
|
return { child: started.child, pid };
|
|
2053
2641
|
}
|
|
2054
2642
|
if (exited.now) {
|
|
2055
|
-
const why =
|
|
2643
|
+
const why = helperFailure(errorFile) ?? "it exited saying nothing";
|
|
2056
2644
|
return { error: `it could not be started ${where}: ${why}` };
|
|
2057
2645
|
}
|
|
2058
|
-
await
|
|
2646
|
+
await delay3(50);
|
|
2059
2647
|
}
|
|
2060
2648
|
started.child.kill();
|
|
2061
|
-
return {
|
|
2649
|
+
return {
|
|
2650
|
+
error: `it was not started ${where} within ${DESKTOP_START_MS} ms, and ${helperStep(stepFile)}`
|
|
2651
|
+
};
|
|
2062
2652
|
} finally {
|
|
2063
2653
|
discard(scratch);
|
|
2064
2654
|
}
|
|
@@ -2076,7 +2666,7 @@ function profiledBy(path, projectPath) {
|
|
|
2076
2666
|
};
|
|
2077
2667
|
let projectGodot = null;
|
|
2078
2668
|
try {
|
|
2079
|
-
projectGodot =
|
|
2669
|
+
projectGodot = readFileSync6(join9(projectPath, "project.godot"), "utf8");
|
|
2080
2670
|
} catch {}
|
|
2081
2671
|
const observer = new ProfileObserver((totals) => {
|
|
2082
2672
|
const first = latest === null;
|
|
@@ -2177,11 +2767,14 @@ async function keep() {
|
|
|
2177
2767
|
}
|
|
2178
2768
|
const profiled = run?.profile === undefined ? null : profiledBy(run.profile, run.projectPath);
|
|
2179
2769
|
const debuggerPeer = run === undefined || run.debugged === false ? null : await GameDebugger.open(run.breakpoints ?? [], profiled === null ? [] : [profiled.observer]);
|
|
2180
|
-
const
|
|
2770
|
+
const exitNotes = run === undefined ? null : mkdtempSync4(join9(tmpdir5(), "gdharness-exit-"));
|
|
2771
|
+
const noted = exitNotes === null ? spec : { ...spec, envChanges: { ...spec.envChanges, [EXIT_NOTES_VARIABLE]: exitNotes } };
|
|
2772
|
+
const toStart = debuggerPeer === null ? noted : { ...noted, args: withEngineArguments(noted.args, debuggerPeer.engineArguments) };
|
|
2181
2773
|
const transcript = run === undefined ? "ignore" : openSync3(run.transcript, "a");
|
|
2182
|
-
const own = run !== undefined && throughTheHelper(spec) ? mkdtempSync4(
|
|
2183
|
-
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");
|
|
2184
2776
|
let started;
|
|
2777
|
+
const spawnedAt = Date.now();
|
|
2185
2778
|
try {
|
|
2186
2779
|
started = await startDetached(toStart, transcript, run === undefined, stopFile);
|
|
2187
2780
|
} finally {
|
|
@@ -2202,6 +2795,7 @@ async function keep() {
|
|
|
2202
2795
|
startedAt: run.startedAt,
|
|
2203
2796
|
startedBy: Date.now(),
|
|
2204
2797
|
projectPath: run.projectPath,
|
|
2798
|
+
keeperPid: process.pid,
|
|
2205
2799
|
...run.servedBy === undefined ? {} : { servedBy: run.servedBy },
|
|
2206
2800
|
...run.profile === undefined ? {} : { profile: run.profile },
|
|
2207
2801
|
arguments: toStart.args,
|
|
@@ -2212,13 +2806,18 @@ async function keep() {
|
|
|
2212
2806
|
}
|
|
2213
2807
|
const exited = exitOf(child);
|
|
2214
2808
|
let listening = null;
|
|
2809
|
+
let end = () => {
|
|
2810
|
+
if (stopFile !== "") {
|
|
2811
|
+
writeFileSync3(stopFile, "");
|
|
2812
|
+
return "signalled";
|
|
2813
|
+
}
|
|
2814
|
+
return child.kill() ? "signalled" : "gone";
|
|
2815
|
+
};
|
|
2816
|
+
const asked = { stop: false };
|
|
2215
2817
|
if (record !== null) {
|
|
2216
2818
|
listening = await listenForTheServer(() => {
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
return "signalled";
|
|
2220
|
-
}
|
|
2221
|
-
return child.kill() ? "signalled" : "gone";
|
|
2819
|
+
asked.stop = true;
|
|
2820
|
+
return end();
|
|
2222
2821
|
}, debuggerPeer === null ? null : (request) => debuggerPeer.ask(request));
|
|
2223
2822
|
if (listening !== null) {
|
|
2224
2823
|
writeNoteIfItCan({ ...record, keeper: listening.address });
|
|
@@ -2231,21 +2830,87 @@ async function keep() {
|
|
|
2231
2830
|
process.stdout.end(`pid ${pid}
|
|
2232
2831
|
`, resolve);
|
|
2233
2832
|
});
|
|
2234
|
-
|
|
2833
|
+
let { code, signal } = await exited;
|
|
2834
|
+
let exitedAt = Date.now();
|
|
2835
|
+
let note = record === null ? null : listening === null ? record : { ...record, keeper: listening.address };
|
|
2836
|
+
if (note !== null) {
|
|
2837
|
+
writeNoteIfItCan({ ...note, endingAt: exitedAt });
|
|
2838
|
+
}
|
|
2839
|
+
end = () => "gone";
|
|
2840
|
+
let looked = note === null || restartSaid(exitNotes, pid) === false ? null : followSuccessor(pid, spec.command, spawnedAt, exitedAt, run?.transcript);
|
|
2235
2841
|
if (debuggerPeer !== null) {
|
|
2236
2842
|
await debuggerPeer.drained(DRAIN_MS);
|
|
2237
2843
|
}
|
|
2238
2844
|
profiled?.finish();
|
|
2845
|
+
let carried = pid;
|
|
2846
|
+
let notTold = false;
|
|
2847
|
+
while (note !== null && looked !== null) {
|
|
2848
|
+
const followed = await looked;
|
|
2849
|
+
if (followed === null) {
|
|
2850
|
+
break;
|
|
2851
|
+
}
|
|
2852
|
+
const next = followed.successor;
|
|
2853
|
+
const before = {
|
|
2854
|
+
pid: carried,
|
|
2855
|
+
endedAt: exitedAt,
|
|
2856
|
+
...code === null ? {} : { exitCode: code },
|
|
2857
|
+
...code === null && signal !== null ? { exitSignal: signal } : {}
|
|
2858
|
+
};
|
|
2859
|
+
note = {
|
|
2860
|
+
...note,
|
|
2861
|
+
pid: next.pid,
|
|
2862
|
+
startedBy: next.startedAt,
|
|
2863
|
+
restartedFrom: [...note.restartedFrom ?? [], before]
|
|
2864
|
+
};
|
|
2865
|
+
writeNoteIfItCan(note);
|
|
2866
|
+
end = () => {
|
|
2867
|
+
try {
|
|
2868
|
+
process.kill(next.pid);
|
|
2869
|
+
return "signalled";
|
|
2870
|
+
} catch {
|
|
2871
|
+
return "gone";
|
|
2872
|
+
}
|
|
2873
|
+
};
|
|
2874
|
+
if (asked.stop) {
|
|
2875
|
+
end();
|
|
2876
|
+
}
|
|
2877
|
+
const ended = await followed.ended;
|
|
2878
|
+
carried = next.pid;
|
|
2879
|
+
code = ended.code;
|
|
2880
|
+
signal = null;
|
|
2881
|
+
notTold = ended.code === null;
|
|
2882
|
+
exitedAt = Date.now();
|
|
2883
|
+
writeNoteIfItCan({ ...note, endingAt: exitedAt });
|
|
2884
|
+
looked = restartSaid(exitNotes, next.pid) === false ? null : followSuccessor(next.pid, spec.command, next.startedAt, exitedAt, run?.transcript);
|
|
2885
|
+
}
|
|
2239
2886
|
if (run !== undefined) {
|
|
2240
|
-
recordRunEnded(run.projectPath,
|
|
2887
|
+
recordRunEnded(run.projectPath, carried, {
|
|
2888
|
+
exitCode: code,
|
|
2889
|
+
exitSignal: code === null ? signal : null,
|
|
2890
|
+
notTold
|
|
2891
|
+
});
|
|
2241
2892
|
}
|
|
2242
2893
|
debuggerPeer?.close();
|
|
2243
2894
|
listening?.close();
|
|
2244
|
-
|
|
2245
|
-
|
|
2895
|
+
for (const made of [own, exitNotes]) {
|
|
2896
|
+
if (made !== null) {
|
|
2897
|
+
rmSync6(made, { recursive: true, force: true });
|
|
2898
|
+
}
|
|
2246
2899
|
}
|
|
2247
2900
|
process.exit(0);
|
|
2248
2901
|
}
|
|
2902
|
+
var EXIT_NOTES_VARIABLE = "GDHARNESS_EXIT_NOTES";
|
|
2903
|
+
function restartSaid(notes, pid) {
|
|
2904
|
+
if (notes === null) {
|
|
2905
|
+
return null;
|
|
2906
|
+
}
|
|
2907
|
+
try {
|
|
2908
|
+
const said = JSON.parse(readFileSync6(join9(notes, `exit-${String(pid)}.json`), "utf8"));
|
|
2909
|
+
return typeof said === "object" && said !== null && "restart" in said && typeof said.restart === "boolean" ? said.restart : null;
|
|
2910
|
+
} catch {
|
|
2911
|
+
return null;
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2249
2914
|
await letWindowsReportCrashes();
|
|
2250
2915
|
var mode = process.argv[2];
|
|
2251
2916
|
if (mode === "launch") {
|