gdharness 1.3.5 → 1.3.6
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.
|
@@ -120,6 +120,7 @@ func _init_tools() -> void:
|
|
|
120
120
|
"play_scene": [_play_tools, "play_scene"],
|
|
121
121
|
"restart_editor": [_play_tools, "restart_editor"],
|
|
122
122
|
"quit_editor": [_play_tools, "quit_editor"],
|
|
123
|
+
"editor_servers": [_play_tools, "editor_servers"],
|
|
123
124
|
"stop_playing": [_play_tools, "stop_playing"],
|
|
124
125
|
"playing_status": [_play_tools, "playing_status"],
|
|
125
126
|
# The script profiler on a game the editor plays
|
|
@@ -231,6 +231,54 @@ func quit_editor(_args: Dictionary) -> Dictionary:
|
|
|
231
231
|
return {"ok": true, "quitting": true, "saved": true}
|
|
232
232
|
|
|
233
233
|
|
|
234
|
+
## What the editor said as it started each of its servers, and the debug adapter started again when
|
|
235
|
+
## [param args] names it under `rebind`.
|
|
236
|
+
##
|
|
237
|
+
## Godot binds each server once and says whether it could only in the editor's log panel, as a line
|
|
238
|
+
## like `--- Failed to start Debug adapter server on port 6006: Already in use ---`, so those lines
|
|
239
|
+
## are the one account of why a port it names has nothing on it. They are read before anything is
|
|
240
|
+
## started again, so the answer carries the first attempt's reason as well as the next one's.
|
|
241
|
+
##
|
|
242
|
+
## The debug adapter binds as its node enters the tree, which is early in the editor's start and
|
|
243
|
+
## seconds before the language server, and does not try again; taking its node out of the tree and
|
|
244
|
+
## putting it back runs that start once more. The language server starts on the editor's first
|
|
245
|
+
## frame after it is ready and only once, so re-entering the tree would stop it for good, and it is
|
|
246
|
+
## never touched here.
|
|
247
|
+
func editor_servers(args: Dictionary) -> Dictionary:
|
|
248
|
+
var said: PackedStringArray = _servers_said()
|
|
249
|
+
var rebound: bool = false
|
|
250
|
+
if GdhRead.as_bool(args.get("rebind", false)):
|
|
251
|
+
var adapter: Node = _debug_adapter()
|
|
252
|
+
if adapter != null:
|
|
253
|
+
var parent: Node = adapter.get_parent()
|
|
254
|
+
var at: int = adapter.get_index()
|
|
255
|
+
parent.remove_child(adapter)
|
|
256
|
+
parent.add_child(adapter)
|
|
257
|
+
parent.move_child(adapter, at)
|
|
258
|
+
rebound = true
|
|
259
|
+
return {"ok": true, "said": said, "rebound": rebound, "saidSince": _servers_said().slice(said.size())}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
## The lines the editor's log panel holds about its language server and debug adapter, in order.
|
|
263
|
+
static func _servers_said() -> PackedStringArray:
|
|
264
|
+
var lines: PackedStringArray = []
|
|
265
|
+
var root: Node = EditorInterface.get_base_control().get_tree().root
|
|
266
|
+
for panel: Node in root.find_children("*", "EditorLog", true, false):
|
|
267
|
+
for shown: Node in panel.find_children("*", "RichTextLabel", true, false):
|
|
268
|
+
for line: String in (shown as RichTextLabel).get_parsed_text().split("\n", false):
|
|
269
|
+
var stripped: String = line.strip_edges()
|
|
270
|
+
if stripped.begins_with("---") and stripped.contains(" server"):
|
|
271
|
+
var _kept: bool = lines.append(stripped)
|
|
272
|
+
return lines
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
## The editor's debug adapter node, found by its engine class, or null in an editor without one.
|
|
276
|
+
static func _debug_adapter() -> Node:
|
|
277
|
+
var root: Node = EditorInterface.get_base_control().get_tree().root
|
|
278
|
+
var found: Array[Node] = root.find_children("*", "DebugAdapterServer", true, false)
|
|
279
|
+
return found[0] if not found.is_empty() else null
|
|
280
|
+
|
|
281
|
+
|
|
234
282
|
## Asked of the tree the editor's own window is in, because this object is not in one.
|
|
235
283
|
static func _leave() -> void:
|
|
236
284
|
var base: Control = EditorInterface.get_base_control()
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
extends SceneTree
|
|
2
|
+
|
|
3
|
+
## gdUnit4's command-line runner over the suites a file lists, in the file's order. One `-a` a suite
|
|
4
|
+
## outgrows the command line once a game has a few hundred suites (Windows stops at 32767
|
|
5
|
+
## characters), so the suites come from the file named after `--` with [constant LISTED], and the
|
|
6
|
+
## cases to leave out from the one named with [constant SKIPPING]. Every other argument is gdUnit4's
|
|
7
|
+
## own, as `GdUnitCmdTool.gd` takes them.
|
|
8
|
+
##
|
|
9
|
+
## Started from outside the project, `-s` with this file's absolute path, so it declares no class of
|
|
10
|
+
## its own and reaches gdUnit4 through the project's global classes. Besides the file of suites it
|
|
11
|
+
## finds each case by its id in a dictionary ([GdhIndexedSession]), can compile the suites together
|
|
12
|
+
## before gdUnit4 loads them ([constant TOGETHER]), can turn off the reports the caller does
|
|
13
|
+
## not read ([constant REPORTS_OFF]), and writes the errors each suite declares it expects
|
|
14
|
+
## ([constant EXPECTED_OUT]).
|
|
15
|
+
|
|
16
|
+
const LISTED: String = "--suites-in"
|
|
17
|
+
const SKIPPING: String = "--skipping-in"
|
|
18
|
+
## The flags turning off a report, with the gdUnit4 hook that writes it. A gate reads what a run
|
|
19
|
+
## prints, and each report finds every case it hears of in the session to write them all out as the
|
|
20
|
+
## run ends; project_test reads the JUnit one and turns off only the HTML one.
|
|
21
|
+
const REPORTS_OFF: Dictionary[String, String] = {
|
|
22
|
+
"--html-report-off": "res://addons/gdUnit4/src/core/hooks/GdUnitHtmlReporterTestSessionHook.gd",
|
|
23
|
+
"--junit-report-off": "res://addons/gdUnit4/src/core/hooks/GdUnitXMLReporterTestSessionHook.gd",
|
|
24
|
+
}
|
|
25
|
+
## Compiles the suites together beside every class of the game ([method compiled_together]) before
|
|
26
|
+
## gdUnit4 loads them. A gate asks for it, since it runs every suite and wants every script that
|
|
27
|
+
## does not compile named. project_test does not: its answer is about the scripts its run reached,
|
|
28
|
+
## and the compile names every broken script of the game, then once more as the one script it made.
|
|
29
|
+
const TOGETHER: String = "--compile-together"
|
|
30
|
+
## The file to write the errors every suite declares it expects into, as JSON keyed by suite path and
|
|
31
|
+
## then case name.
|
|
32
|
+
const EXPECTED_OUT: String = "--expected-errors-out"
|
|
33
|
+
## The constant a suite declares its expected errors in: case name to the exact texts it prints.
|
|
34
|
+
const EXPECTED: String = "GDHARNESS_EXPECTED_ERRORS"
|
|
35
|
+
## The tool name gdUnit4 reads its options after.
|
|
36
|
+
const TOOL: String = "GdUnitCmdTool.gd"
|
|
37
|
+
## How long a windowless engine sleeps after each frame. The engine's own default is the
|
|
38
|
+
## low-processor pause, 6.8 ms measured, which a run of suites awaiting frames spends thousands of
|
|
39
|
+
## times; zero makes a frame's delta round to nothing, and a tween then never moves.
|
|
40
|
+
const FRAME_SLEEP_USEC: int = 1000
|
|
41
|
+
|
|
42
|
+
var _runner: GdhRunner = null
|
|
43
|
+
## The suites of the run, compiled by [method compiled_together] and held for as long as it runs.
|
|
44
|
+
var _together: GDScript = null
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
## gdUnit4's runner reading its options after this script's path rather than after its own tool's,
|
|
48
|
+
## which it looks for by name and otherwise finds none of them, printing its help.
|
|
49
|
+
class GdhRunner:
|
|
50
|
+
extends GdUnitTestCIRunner
|
|
51
|
+
|
|
52
|
+
var own_path: String = ""
|
|
53
|
+
|
|
54
|
+
func get_cmdline_args() -> PackedStringArray:
|
|
55
|
+
return options_of(OS.get_cmdline_args(), own_path)
|
|
56
|
+
|
|
57
|
+
## The session gdUnit4 has just made, swapped for a [GdhIndexedSession] of the same cases as it
|
|
58
|
+
## is announced: everything that reads the session, the hooks, the cases and the console printing
|
|
59
|
+
## every case, is handed it after this.
|
|
60
|
+
func _on_gdunit_event(event: GdUnitEvent) -> void:
|
|
61
|
+
if event.type() == GdUnitEvent.SESSION_START:
|
|
62
|
+
_test_session = GdhIndexedSession.of(_test_session)
|
|
63
|
+
super(event)
|
|
64
|
+
|
|
65
|
+
## [param args] with everything up to the script at [param own] given as gdUnit4's tool name. The
|
|
66
|
+
## path is compared with its separators and, on Windows, its case folded, since the engine
|
|
67
|
+
## reports the script as `X:/a/listed.gd` when it was started as `X:\a\listed.gd`.
|
|
68
|
+
static func options_of(args: PackedStringArray, own: String) -> PackedStringArray:
|
|
69
|
+
var wanted: String = spelt(own)
|
|
70
|
+
var at: int = -1
|
|
71
|
+
for index: int in args.size():
|
|
72
|
+
if spelt(args[index]) == wanted:
|
|
73
|
+
at = index
|
|
74
|
+
break
|
|
75
|
+
if at < 0:
|
|
76
|
+
return args
|
|
77
|
+
var options: PackedStringArray = [TOOL]
|
|
78
|
+
options.append_array(args.slice(at + 1))
|
|
79
|
+
return options
|
|
80
|
+
|
|
81
|
+
static func spelt(path: String) -> String:
|
|
82
|
+
var forward: String = path.replace("\\", "/")
|
|
83
|
+
return forward.to_lower() if OS.get_name() == "Windows" else forward
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
## gdUnit4's session finding a case by its id in a dictionary, where its own walks every case of the
|
|
87
|
+
## session, `GdUnitTestSession.find_test_by_id`: the console asks it twice a case and each report
|
|
88
|
+
## twice more, so a run's time in it grew with the square of its cases (17.9 s of processor time
|
|
89
|
+
## in an engine of 2300 cases).
|
|
90
|
+
class GdhIndexedSession:
|
|
91
|
+
extends GdUnitTestSession
|
|
92
|
+
|
|
93
|
+
var _by_id: Dictionary[String, GdUnitTestCase] = {}
|
|
94
|
+
|
|
95
|
+
func _init(cases: Array[GdUnitTestCase], reports_in: String) -> void:
|
|
96
|
+
super(cases, reports_in)
|
|
97
|
+
for test: GdUnitTestCase in _test_cases:
|
|
98
|
+
_by_id[str(test.guid)] = test
|
|
99
|
+
|
|
100
|
+
func find_test_by_id(id: GdUnitGUID) -> GdUnitTestCase:
|
|
101
|
+
var found: GdUnitTestCase = _by_id.get(str(id), null)
|
|
102
|
+
return found
|
|
103
|
+
|
|
104
|
+
## [param made] as an indexed session of its cases, reporting where it does; one already indexed
|
|
105
|
+
## as it is.
|
|
106
|
+
static func of(made: GdUnitTestSession) -> GdUnitTestSession:
|
|
107
|
+
if made is GdhIndexedSession:
|
|
108
|
+
return made
|
|
109
|
+
return GdhIndexedSession.new(made._test_cases, made.report_path)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
func _initialize() -> void:
|
|
113
|
+
OS.low_processor_usage_mode_sleep_usec = FRAME_SLEEP_USEC
|
|
114
|
+
var asked: PackedStringArray = OS.get_cmdline_user_args()
|
|
115
|
+
var unread: PackedStringArray = []
|
|
116
|
+
for flag: String in REPORTS_OFF:
|
|
117
|
+
if asked.has(flag):
|
|
118
|
+
var _unread: bool = unread.append(REPORTS_OFF[flag])
|
|
119
|
+
if not unread.is_empty():
|
|
120
|
+
var _off: PackedStringArray = unread_off(GdUnitTestSessionHookService.instance(), unread)
|
|
121
|
+
var suites: PackedStringArray = listed(asked, LISTED)
|
|
122
|
+
if asked.has(TOGETHER):
|
|
123
|
+
_together = compiled_together(suites)
|
|
124
|
+
var expected_out: String = argument_after(asked, EXPECTED_OUT)
|
|
125
|
+
if not expected_out.is_empty():
|
|
126
|
+
write_expected(expected_out, suites)
|
|
127
|
+
_runner = GdhRunner.new()
|
|
128
|
+
_runner.own_path = get_script().resource_path
|
|
129
|
+
_runner.add_test_suites(suites)
|
|
130
|
+
_runner.skip_test_suites(listed(asked, SKIPPING))
|
|
131
|
+
root.add_child(_runner)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
## As gdUnit4's own `GdUnitCmdTool.gd` ends, which names in a verbose run the nodes left orphaned.
|
|
135
|
+
func _finalize() -> void:
|
|
136
|
+
queue_delete(_runner)
|
|
137
|
+
if OS.is_stdout_verbose():
|
|
138
|
+
print("Orphan nodes left as the run ends:")
|
|
139
|
+
Window.print_orphan_nodes()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
## Turns off the hooks among [param hooks] whose scripts [param unread] names, answering the scripts
|
|
143
|
+
## of those it turned off. Through the mark gdUnit4's own `is_enabled` reads, rather than its
|
|
144
|
+
## `enable_hook`, which writes the setting into `project.godot`: a run changes nothing in the tree.
|
|
145
|
+
static func unread_off(hooks: GdUnitTestSessionHookService, unread: PackedStringArray) -> PackedStringArray:
|
|
146
|
+
var off: PackedStringArray = []
|
|
147
|
+
for hook: GdUnitTestSessionHook in hooks.enigne_hooks:
|
|
148
|
+
var path: String = hook.get_script().resource_path
|
|
149
|
+
if unread.has(path):
|
|
150
|
+
hook.set_meta("enabled", false)
|
|
151
|
+
var _added: bool = off.append(path)
|
|
152
|
+
return off
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
## [param suites] compiled inside one script, answered for the caller to hold so gdUnit4's own loads
|
|
156
|
+
## find them compiled. The engine keeps what it read of a class only while the compile that asked for
|
|
157
|
+
## it runs (`GDScriptCache`), so a suite compiled on its own reads again every class it reaches a
|
|
158
|
+
## member of, some seventy milliseconds a suite. As the preloads of one script naming every class of
|
|
159
|
+
## the game's own first, held from that script's compile, they share one reading of each: two hundred
|
|
160
|
+
## suites compiled in a third of the time they took one at a time. The names are public, since a
|
|
161
|
+
## private one unread is a warning and every warning an error, and each a variable, since a class is
|
|
162
|
+
## no constant expression. A suite that does not compile fails here and again when gdUnit4 loads it,
|
|
163
|
+
## which reports it as it would have. A directory listed is left to gdUnit4 to walk.
|
|
164
|
+
static func compiled_together(suites: PackedStringArray) -> GDScript:
|
|
165
|
+
var lines: PackedStringArray = ["extends RefCounted"]
|
|
166
|
+
var at: int = 0
|
|
167
|
+
for named: String in held_classes():
|
|
168
|
+
var _held: bool = lines.append("var held_%d: Variant = %s" % [at, named])
|
|
169
|
+
at += 1
|
|
170
|
+
at = 0
|
|
171
|
+
for path: String in suites:
|
|
172
|
+
if not path.ends_with(".gd"):
|
|
173
|
+
continue
|
|
174
|
+
var _suite: bool = lines.append('var suite_%d: Variant = preload("%s")' % [at, path])
|
|
175
|
+
at += 1
|
|
176
|
+
var together: GDScript = GDScript.new()
|
|
177
|
+
together.source_code = "\n".join(lines) + "\n"
|
|
178
|
+
var _compiled: Error = together.reload()
|
|
179
|
+
return together
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
## The classes [method compiled_together] names before the suites: the game's own GDScript classes,
|
|
183
|
+
## none of an addon's or a suite's, then gdUnit4's suite. Only GDScript shares the engine's reading,
|
|
184
|
+
## and a sandboxed mod's SafeGDScript class would load the sandbox's compiler, some 290 MB and up to
|
|
185
|
+
## 2.5 s, for nothing.
|
|
186
|
+
static func held_classes(
|
|
187
|
+
classes: Array[Dictionary] = ProjectSettings.get_global_class_list()
|
|
188
|
+
) -> PackedStringArray:
|
|
189
|
+
var held: PackedStringArray = []
|
|
190
|
+
for known: Dictionary in classes:
|
|
191
|
+
var path: String = known["path"]
|
|
192
|
+
var own: bool = not path.begins_with("res://addons/") and not path.ends_with("_test.gd")
|
|
193
|
+
if own and known["language"] == &"GDScript":
|
|
194
|
+
var _own: bool = held.append(str(known["class"]))
|
|
195
|
+
var _suite: bool = held.append("GdUnitTestSuite")
|
|
196
|
+
return held
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
## What the file after [param flag] in [param args] names, one a line in order, or none when no file
|
|
200
|
+
## is named: gdUnit4 then says it found nothing, which a gate refuses.
|
|
201
|
+
static func listed(args: PackedStringArray, flag: String) -> PackedStringArray:
|
|
202
|
+
var path: String = argument_after(args, flag)
|
|
203
|
+
if path.is_empty():
|
|
204
|
+
return PackedStringArray()
|
|
205
|
+
var named: PackedStringArray = []
|
|
206
|
+
for line: String in FileAccess.get_file_as_string(path).split("\n", false):
|
|
207
|
+
if not line.strip_edges().is_empty():
|
|
208
|
+
var _named: bool = named.append(line.strip_edges())
|
|
209
|
+
return named
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
## The argument after [param flag] in [param args], or "" when there is none.
|
|
213
|
+
static func argument_after(args: PackedStringArray, flag: String) -> String:
|
|
214
|
+
var at: int = args.find(flag)
|
|
215
|
+
if at < 0 or at + 1 >= args.size():
|
|
216
|
+
return ""
|
|
217
|
+
return args[at + 1]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
## The errors each of [param suites] declares it expects, by suite path and then case name, read off
|
|
221
|
+
## the compiled suite's constants so a declaration is whatever the engine made of it. A suite
|
|
222
|
+
## declaring none is left out.
|
|
223
|
+
static func expected_by_suite(suites: PackedStringArray) -> Dictionary:
|
|
224
|
+
var expected: Dictionary = {}
|
|
225
|
+
for path: String in suites:
|
|
226
|
+
if not path.ends_with(".gd") or not ResourceLoader.exists(path):
|
|
227
|
+
continue
|
|
228
|
+
var suite: Script = load(path)
|
|
229
|
+
var declared: Variant = suite.get_script_constant_map().get(EXPECTED)
|
|
230
|
+
if declared is Dictionary:
|
|
231
|
+
expected[path] = declared
|
|
232
|
+
return expected
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
static func write_expected(path: String, suites: PackedStringArray) -> void:
|
|
236
|
+
var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
|
|
237
|
+
if file == null:
|
|
238
|
+
push_error("Could not write the expected errors to %s: %s" % [path, FileAccess.get_open_error()])
|
|
239
|
+
return
|
|
240
|
+
var _stored: bool = file.store_string(JSON.stringify(expected_by_suite(suites)))
|
|
241
|
+
file.close()
|
package/build/keeper.js
CHANGED
|
@@ -106,6 +106,9 @@ public static class GdharnessDesktop {
|
|
|
106
106
|
[DllImport("kernel32.dll")] static extern bool GetExitCodeProcess(IntPtr handle, out uint code);
|
|
107
107
|
[DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr handle);
|
|
108
108
|
[DllImport("kernel32.dll")] static extern bool TerminateProcess(IntPtr handle, uint code);
|
|
109
|
+
[DllImport("kernel32.dll")] static extern uint SetErrorMode(uint mode);
|
|
110
|
+
const uint FailCriticalErrors = 0x1;
|
|
111
|
+
const uint NoOpenFileErrorBox = 0x8000;
|
|
109
112
|
|
|
110
113
|
delegate bool EachWindow(IntPtr window, IntPtr unused);
|
|
111
114
|
[DllImport("user32.dll")] static extern bool EnumWindows(EachWindow each, IntPtr unused);
|
|
@@ -179,6 +182,11 @@ public static class GdharnessDesktop {
|
|
|
179
182
|
startup.output = GetStdHandle(-11);
|
|
180
183
|
startup.error = startup.output;
|
|
181
184
|
Started started;
|
|
185
|
+
// Inherited by what it starts: without SEM_NOGPFAULTERRORBOX, Windows Error Reporting runs when
|
|
186
|
+
// the target crashes and writes its dump, and with the other two no hard-error box can open.
|
|
187
|
+
// Set outright rather than cleared from this process's own: Windows PowerShell changes the mode
|
|
188
|
+
// it inherited as it starts, and was seen reading 0x1 here, without the open-file bit.
|
|
189
|
+
SetErrorMode(FailCriticalErrors | NoOpenFileErrorBox);
|
|
182
190
|
if (noActivate) {
|
|
183
191
|
LockSetForegroundWindow(LockForeground);
|
|
184
192
|
}
|
|
@@ -360,6 +368,33 @@ function throughHelper(how, command, args, files, assembly = helperAssembly()) {
|
|
|
360
368
|
};
|
|
361
369
|
}
|
|
362
370
|
|
|
371
|
+
// src/error-mode.ts
|
|
372
|
+
import process3 from "node:process";
|
|
373
|
+
var NO_FAULT_ERROR_BOX = 2;
|
|
374
|
+
var cleared = process3.platform !== "win32";
|
|
375
|
+
async function letWindowsReportCrashes() {
|
|
376
|
+
if (cleared || !("bun" in process3.versions)) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
try {
|
|
380
|
+
const { dlopen, FFIType } = await import("bun:ffi");
|
|
381
|
+
const kernel32 = dlopen("kernel32.dll", {
|
|
382
|
+
GetErrorMode: { args: [], returns: FFIType.u32 },
|
|
383
|
+
SetErrorMode: { args: [FFIType.u32], returns: FFIType.u32 }
|
|
384
|
+
});
|
|
385
|
+
try {
|
|
386
|
+
const mode = kernel32.symbols.GetErrorMode();
|
|
387
|
+
kernel32.symbols.SetErrorMode(mode & ~NO_FAULT_ERROR_BOX);
|
|
388
|
+
cleared = true;
|
|
389
|
+
} finally {
|
|
390
|
+
kernel32.close();
|
|
391
|
+
}
|
|
392
|
+
} catch {}
|
|
393
|
+
}
|
|
394
|
+
function crashesReported() {
|
|
395
|
+
return cleared;
|
|
396
|
+
}
|
|
397
|
+
|
|
363
398
|
// src/game-debugger.ts
|
|
364
399
|
import { createServer } from "node:net";
|
|
365
400
|
|
|
@@ -1142,7 +1177,7 @@ import { mkdtempSync as mkdtempSync2, rmSync as rmSync2 } from "node:fs";
|
|
|
1142
1177
|
import { connect, createServer as createServer2 } from "node:net";
|
|
1143
1178
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
1144
1179
|
import { join as join2 } from "node:path";
|
|
1145
|
-
import
|
|
1180
|
+
import process4 from "node:process";
|
|
1146
1181
|
var DEBUG_PREFIX = "debug ";
|
|
1147
1182
|
function endedOrWhyNot(end) {
|
|
1148
1183
|
try {
|
|
@@ -1169,7 +1204,7 @@ function errorText(error) {
|
|
|
1169
1204
|
async function listenForTheServer(end, debug) {
|
|
1170
1205
|
let directory = null;
|
|
1171
1206
|
try {
|
|
1172
|
-
directory =
|
|
1207
|
+
directory = process4.platform === "win32" ? null : mkdtempSync2(join2(tmpdir2(), "gdharness-keeper-"));
|
|
1173
1208
|
const address = directory === null ? `\\\\.\\pipe\\gdharness-keeper-${randomUUID()}` : join2(directory, "keeper.sock");
|
|
1174
1209
|
const server = createServer2((socket) => {
|
|
1175
1210
|
socket.setEncoding("utf8");
|
|
@@ -1469,13 +1504,13 @@ function recordAt(path) {
|
|
|
1469
1504
|
import { existsSync as existsSync2, mkdtempSync as mkdtempSync3, readdirSync as readdirSync2, rmSync as rmSync5 } from "node:fs";
|
|
1470
1505
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
1471
1506
|
import { join as join6 } from "node:path";
|
|
1472
|
-
import
|
|
1507
|
+
import process5 from "node:process";
|
|
1473
1508
|
var SWEPT = { recursive: true, force: true };
|
|
1474
1509
|
var AGAIN_AFTER_MS = [250, 500, 1000, 2000, 4000, 8000, 16000];
|
|
1475
1510
|
var waiting = new Set;
|
|
1476
1511
|
var atExit = new Set;
|
|
1477
1512
|
function scratchDirectory(kind) {
|
|
1478
|
-
return mkdtempSync3(join6(tmpdir4(), `gdharness-${kind}-${
|
|
1513
|
+
return mkdtempSync3(join6(tmpdir4(), `gdharness-${kind}-${process5.pid}-`));
|
|
1479
1514
|
}
|
|
1480
1515
|
function discardWith(remove, paths) {
|
|
1481
1516
|
const left = [];
|
|
@@ -1533,7 +1568,7 @@ function lastTryOnExit() {
|
|
|
1533
1568
|
return;
|
|
1534
1569
|
}
|
|
1535
1570
|
lastTryWatched = true;
|
|
1536
|
-
|
|
1571
|
+
process5.once("exit", () => {
|
|
1537
1572
|
for (const path of [...waiting, ...atExit]) {
|
|
1538
1573
|
try {
|
|
1539
1574
|
remove(path);
|
|
@@ -1937,7 +1972,7 @@ async function startThroughHelper(spec, output, stopFile) {
|
|
|
1937
1972
|
const scratch = scratchDirectory("desktop");
|
|
1938
1973
|
const pidFile = join7(scratch, "pid");
|
|
1939
1974
|
const errorFile = join7(scratch, "error");
|
|
1940
|
-
const where = spec.desktop
|
|
1975
|
+
const where = spec.desktop !== undefined ? `on the ${spec.desktop} desktop` : spec.noActivate === true ? "without activation" : "through the desktop helper";
|
|
1941
1976
|
try {
|
|
1942
1977
|
const compiled = await ensureHelperCompiled();
|
|
1943
1978
|
if (compiled !== null) {
|
|
@@ -2020,10 +2055,10 @@ async function startDetached(spec, output, hidden, stopFile = "") {
|
|
|
2020
2055
|
return throughTheHelper(spec) ? await startThroughHelper(spec, output, stopFile) : await startDirectly(spec, output, hidden);
|
|
2021
2056
|
}
|
|
2022
2057
|
function throughTheHelper(spec) {
|
|
2023
|
-
return process.platform === "win32" && (
|
|
2058
|
+
return needsItsHelperKept(spec) || process.platform === "win32" && !crashesReported();
|
|
2024
2059
|
}
|
|
2025
2060
|
function needsItsHelperKept(spec) {
|
|
2026
|
-
return
|
|
2061
|
+
return process.platform === "win32" && (spec.desktop !== undefined || spec.noActivate === true);
|
|
2027
2062
|
}
|
|
2028
2063
|
async function launch() {
|
|
2029
2064
|
const spec = await receivedSpec();
|
|
@@ -2158,6 +2193,7 @@ async function keep() {
|
|
|
2158
2193
|
}
|
|
2159
2194
|
process.exit(0);
|
|
2160
2195
|
}
|
|
2196
|
+
await letWindowsReportCrashes();
|
|
2161
2197
|
var mode = process.argv[2];
|
|
2162
2198
|
if (mode === "launch") {
|
|
2163
2199
|
await launch();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.6",
|
|
4
4
|
"mcpName": "io.github.Aureliolo/gdharness",
|
|
5
5
|
"description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
|
|
6
6
|
"type": "module",
|