gdharness 1.3.5 → 1.3.7

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/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
 
@@ -668,6 +703,8 @@ function readMessage(body) {
668
703
  // src/game-debugger.ts
669
704
  var PAUSE_MS = 30000;
670
705
  var STEP_MS = 1e4;
706
+ var ADDON_STEP_MS = 5000;
707
+ var RUNTIME_ADDON = "res://addons/gdharness_runtime/";
671
708
  var READ_MS = 5000;
672
709
  var REPLIES = new Set([
673
710
  "debug_enter",
@@ -1050,9 +1087,29 @@ class GameDebugger {
1050
1087
  };
1051
1088
  }
1052
1089
  }
1090
+ let frames = await this.stack();
1091
+ const stepping = Date.now();
1092
+ while (frames !== null && this.held !== null && (frames[0]?.script.startsWith(RUNTIME_ADDON) ?? false) && Date.now() - stepping < ADDON_STEP_MS) {
1093
+ this.forget("debug_enter");
1094
+ this.send("step", [], this.held.thread);
1095
+ if (await this.next(GameDebugger.named("debug_enter"), STEP_MS) === null) {
1096
+ return {
1097
+ paused: false,
1098
+ note: `The game stopped in gdharness's runtime addon and was stepped on to reach its own script, and did not stop again within ${STEP_MS / 1000}s. It stops at the next line of script it runs; debug_state stack reads it then.`
1099
+ };
1100
+ }
1101
+ frames = await this.stack();
1102
+ }
1053
1103
  this.stoppedBy("Paused");
1054
- const frames = await this.stack();
1055
- return { paused: true, held: this.heldState(), stack: frames ?? [] };
1104
+ const stack = frames ?? [];
1105
+ return {
1106
+ paused: true,
1107
+ held: this.heldState(),
1108
+ stack,
1109
+ ...stack[0]?.script.startsWith(RUNTIME_ADDON) === true ? {
1110
+ note: `The game is held in gdharness's runtime addon, whose _process runs first in every frame: ${ADDON_STEP_MS / 1000}s of stepping reached no line of the game's own script, so it runs none from frame to frame. A breakpoint in the game's script stops it there.`
1111
+ } : {}
1112
+ };
1056
1113
  }
1057
1114
  stoppedBy(cause) {
1058
1115
  if (this.held !== null) {
@@ -1073,7 +1130,7 @@ class GameDebugger {
1073
1130
  return await exited === null ? { refused: `The game did not say it had gone on within ${READ_MS / 1000}s.` } : { continued: true };
1074
1131
  }
1075
1132
  this.forget("debug_enter");
1076
- this.send(how, [], held.thread);
1133
+ this.send(held.hasStack ? how : "step", [], held.thread);
1077
1134
  const entered = await this.next(GameDebugger.named("debug_enter"), STEP_MS);
1078
1135
  if (entered === null) {
1079
1136
  return {
@@ -1142,7 +1199,7 @@ import { mkdtempSync as mkdtempSync2, rmSync as rmSync2 } from "node:fs";
1142
1199
  import { connect, createServer as createServer2 } from "node:net";
1143
1200
  import { tmpdir as tmpdir2 } from "node:os";
1144
1201
  import { join as join2 } from "node:path";
1145
- import process3 from "node:process";
1202
+ import process4 from "node:process";
1146
1203
  var DEBUG_PREFIX = "debug ";
1147
1204
  function endedOrWhyNot(end) {
1148
1205
  try {
@@ -1169,7 +1226,7 @@ function errorText(error) {
1169
1226
  async function listenForTheServer(end, debug) {
1170
1227
  let directory = null;
1171
1228
  try {
1172
- directory = process3.platform === "win32" ? null : mkdtempSync2(join2(tmpdir2(), "gdharness-keeper-"));
1229
+ directory = process4.platform === "win32" ? null : mkdtempSync2(join2(tmpdir2(), "gdharness-keeper-"));
1173
1230
  const address = directory === null ? `\\\\.\\pipe\\gdharness-keeper-${randomUUID()}` : join2(directory, "keeper.sock");
1174
1231
  const server = createServer2((socket) => {
1175
1232
  socket.setEncoding("utf8");
@@ -1469,13 +1526,13 @@ function recordAt(path) {
1469
1526
  import { existsSync as existsSync2, mkdtempSync as mkdtempSync3, readdirSync as readdirSync2, rmSync as rmSync5 } from "node:fs";
1470
1527
  import { tmpdir as tmpdir4 } from "node:os";
1471
1528
  import { join as join6 } from "node:path";
1472
- import process4 from "node:process";
1529
+ import process5 from "node:process";
1473
1530
  var SWEPT = { recursive: true, force: true };
1474
1531
  var AGAIN_AFTER_MS = [250, 500, 1000, 2000, 4000, 8000, 16000];
1475
1532
  var waiting = new Set;
1476
1533
  var atExit = new Set;
1477
1534
  function scratchDirectory(kind) {
1478
- return mkdtempSync3(join6(tmpdir4(), `gdharness-${kind}-${process4.pid}-`));
1535
+ return mkdtempSync3(join6(tmpdir4(), `gdharness-${kind}-${process5.pid}-`));
1479
1536
  }
1480
1537
  function discardWith(remove, paths) {
1481
1538
  const left = [];
@@ -1533,7 +1590,7 @@ function lastTryOnExit() {
1533
1590
  return;
1534
1591
  }
1535
1592
  lastTryWatched = true;
1536
- process4.once("exit", () => {
1593
+ process5.once("exit", () => {
1537
1594
  for (const path of [...waiting, ...atExit]) {
1538
1595
  try {
1539
1596
  remove(path);
@@ -1937,7 +1994,7 @@ async function startThroughHelper(spec, output, stopFile) {
1937
1994
  const scratch = scratchDirectory("desktop");
1938
1995
  const pidFile = join7(scratch, "pid");
1939
1996
  const errorFile = join7(scratch, "error");
1940
- const where = spec.desktop === undefined ? "without activation" : `on the ${spec.desktop} desktop`;
1997
+ const where = spec.desktop !== undefined ? `on the ${spec.desktop} desktop` : spec.noActivate === true ? "without activation" : "through the desktop helper";
1941
1998
  try {
1942
1999
  const compiled = await ensureHelperCompiled();
1943
2000
  if (compiled !== null) {
@@ -2020,10 +2077,10 @@ async function startDetached(spec, output, hidden, stopFile = "") {
2020
2077
  return throughTheHelper(spec) ? await startThroughHelper(spec, output, stopFile) : await startDirectly(spec, output, hidden);
2021
2078
  }
2022
2079
  function throughTheHelper(spec) {
2023
- return process.platform === "win32" && (spec.desktop !== undefined || spec.noActivate === true);
2080
+ return needsItsHelperKept(spec) || process.platform === "win32" && !crashesReported();
2024
2081
  }
2025
2082
  function needsItsHelperKept(spec) {
2026
- return throughTheHelper(spec);
2083
+ return process.platform === "win32" && (spec.desktop !== undefined || spec.noActivate === true);
2027
2084
  }
2028
2085
  async function launch() {
2029
2086
  const spec = await receivedSpec();
@@ -2158,6 +2215,7 @@ async function keep() {
2158
2215
  }
2159
2216
  process.exit(0);
2160
2217
  }
2218
+ await letWindowsReportCrashes();
2161
2219
  var mode = process.argv[2];
2162
2220
  if (mode === "launch") {
2163
2221
  await launch();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.3.5",
3
+ "version": "1.3.7",
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",