gdharness 1.2.19 → 1.2.20

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 CHANGED
@@ -11924,7 +11924,7 @@ var init_tool_definitions = __esm(() => {
11924
11924
  requires: []
11925
11925
  }
11926
11926
  },
11927
- answer: "stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked.",
11927
+ answer: "stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. arguments is the engine command line the check ran, as a start's is. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked.",
11928
11928
  defaultOperation: "start"
11929
11929
  },
11930
11930
  {
@@ -42480,19 +42480,41 @@ var init_restart_note = __esm(() => {
42480
42480
  init_errors();
42481
42481
  });
42482
42482
 
42483
+ // src/whole-file.ts
42484
+ import { renameSync as renameSync2, rmSync as rmSync12, writeFileSync as writeFileSync16 } from "node:fs";
42485
+ function writeWhole(path, text) {
42486
+ const partial = `${path}.${process.pid}.partial`;
42487
+ try {
42488
+ writeFileSync16(partial, text, "utf8");
42489
+ } catch {
42490
+ return false;
42491
+ }
42492
+ const until = Date.now() + HELD_FILE_MS;
42493
+ for (;; ) {
42494
+ try {
42495
+ renameSync2(partial, path);
42496
+ return true;
42497
+ } catch (error) {
42498
+ const code = error.code ?? "";
42499
+ if (!HELD_BY_A_READER.has(code) || Date.now() >= until) {
42500
+ try {
42501
+ rmSync12(partial, { force: true });
42502
+ } catch {}
42503
+ return false;
42504
+ }
42505
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, HELD_FILE_RETRY_MS);
42506
+ }
42507
+ }
42508
+ }
42509
+ var HELD_FILE_MS = 1000, HELD_FILE_RETRY_MS = 10, HELD_BY_A_READER;
42510
+ var init_whole_file = __esm(() => {
42511
+ HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
42512
+ });
42513
+
42483
42514
  // src/run-record.ts
42484
42515
  import { execFile as execFile4 } from "node:child_process";
42485
42516
  import { createHash as createHash4 } from "node:crypto";
42486
- import {
42487
- mkdirSync as mkdirSync14,
42488
- openSync as openSync2,
42489
- readdirSync as readdirSync10,
42490
- readFileSync as readFileSync21,
42491
- realpathSync as realpathSync6,
42492
- rmSync as rmSync12,
42493
- statSync as statSync8,
42494
- writeFileSync as writeFileSync16
42495
- } from "node:fs";
42517
+ import { mkdirSync as mkdirSync14, openSync as openSync2, readdirSync as readdirSync10, readFileSync as readFileSync21, realpathSync as realpathSync6, rmSync as rmSync13, statSync as statSync8 } from "node:fs";
42496
42518
  import { basename as basename6, dirname as dirname18, join as join24, resolve as resolve7 } from "node:path";
42497
42519
  import { promisify as promisify4 } from "node:util";
42498
42520
  function runsDirectory() {
@@ -42528,7 +42550,7 @@ function latestRunPath(servedBy, directory = runtimeDirectory()) {
42528
42550
  function writeLatestRun(servedBy, latest) {
42529
42551
  try {
42530
42552
  mkdirSync14(runsDirectory(), { recursive: true });
42531
- writeFileSync16(latestRunPath(servedBy), JSON.stringify(latest, null, 2), "utf8");
42553
+ writeWhole(latestRunPath(servedBy), JSON.stringify(latest, null, 2));
42532
42554
  } catch {}
42533
42555
  }
42534
42556
  function readLatestRun(servedBy, directory = runtimeDirectory()) {
@@ -42562,7 +42584,10 @@ function editorNotePath(projectPath, directory = runtimeDirectory()) {
42562
42584
  }
42563
42585
  function writeEditorRunNote(note) {
42564
42586
  mkdirSync14(runsDirectory(), { recursive: true });
42565
- writeFileSync16(editorNotePath(note.projectPath), JSON.stringify(note, null, 2), "utf8");
42587
+ const path = editorNotePath(note.projectPath);
42588
+ if (!writeWhole(path, JSON.stringify(note, null, 2))) {
42589
+ throw new Error(`the editor run note could not be written at ${path}`);
42590
+ }
42566
42591
  }
42567
42592
  function readEditorRunNote(projectPath) {
42568
42593
  for (const directory of runtimeDirectories()) {
@@ -42833,7 +42858,7 @@ function clearRunRecord(projectPath) {
42833
42858
  const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
42834
42859
  for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
42835
42860
  try {
42836
- rmSync12(path, { force: true });
42861
+ rmSync13(path, { force: true });
42837
42862
  } catch {}
42838
42863
  }
42839
42864
  }
@@ -42858,7 +42883,8 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
42858
42883
  return;
42859
42884
  }
42860
42885
  for (const entry of entries) {
42861
- if (!entry.startsWith("run-") || !(entry.endsWith(".log") || entry.endsWith(".profile.json"))) {
42886
+ const transcript = entry.startsWith("run-") && (entry.endsWith(".log") || entry.endsWith(".profile.json"));
42887
+ if (!transcript && !entry.endsWith(".partial")) {
42862
42888
  continue;
42863
42889
  }
42864
42890
  const path = join24(directory, entry);
@@ -42866,7 +42892,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
42866
42892
  continue;
42867
42893
  }
42868
42894
  try {
42869
- rmSync12(path);
42895
+ rmSync13(path);
42870
42896
  } catch {}
42871
42897
  }
42872
42898
  }
@@ -42876,6 +42902,7 @@ var init_run_record = __esm(() => {
42876
42902
  init_paths();
42877
42903
  init_process_children();
42878
42904
  init_runtime_client();
42905
+ init_whole_file();
42879
42906
  init_windows_ask();
42880
42907
  execFileAsync = promisify4(execFile4);
42881
42908
  });
@@ -42986,7 +43013,7 @@ var init_godot_variant = __esm(() => {
42986
43013
  });
42987
43014
 
42988
43015
  // src/script-profile.ts
42989
- import { readFileSync as readFileSync22, renameSync as renameSync2, writeFileSync as writeFileSync17 } from "node:fs";
43016
+ import { readFileSync as readFileSync22 } from "node:fs";
42990
43017
  function readProfileFile(path) {
42991
43018
  try {
42992
43019
  const parsed = JSON.parse(readFileSync22(path, "utf8"));
@@ -43140,6 +43167,7 @@ var init_script_profile = __esm(() => {
43140
43167
  init_gdscript_source();
43141
43168
  init_godot_variant();
43142
43169
  init_resources();
43170
+ init_whole_file();
43143
43171
  });
43144
43172
 
43145
43173
  // src/server-instructions.ts
@@ -47964,6 +47992,7 @@ class GodotServer {
47964
47992
  frames,
47965
47993
  errors,
47966
47994
  warnings,
47995
+ arguments: cmdArgs,
47967
47996
  argsNote: engineFlagsNote(userArgs),
47968
47997
  ...scanWaitAnswer(scanned),
47969
47998
  savesNote: asked.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
package/build/index.js CHANGED
@@ -33221,18 +33221,41 @@ function restartSettled(projectPath) {
33221
33221
  // src/run-record.ts
33222
33222
  import { execFile as execFile4 } from "node:child_process";
33223
33223
  import { createHash as createHash3 } from "node:crypto";
33224
- import {
33225
- mkdirSync as mkdirSync11,
33226
- openSync as openSync2,
33227
- readdirSync as readdirSync7,
33228
- readFileSync as readFileSync18,
33229
- realpathSync as realpathSync6,
33230
- rmSync as rmSync9,
33231
- statSync as statSync6,
33232
- writeFileSync as writeFileSync13
33233
- } from "node:fs";
33224
+ import { mkdirSync as mkdirSync11, openSync as openSync2, readdirSync as readdirSync7, readFileSync as readFileSync18, realpathSync as realpathSync6, rmSync as rmSync10, statSync as statSync6 } from "node:fs";
33234
33225
  import { basename as basename4, dirname as dirname13, join as join19, resolve as resolve6 } from "node:path";
33235
33226
  import { promisify as promisify4 } from "node:util";
33227
+
33228
+ // src/whole-file.ts
33229
+ import { renameSync as renameSync2, rmSync as rmSync9, writeFileSync as writeFileSync13 } from "node:fs";
33230
+ var HELD_FILE_MS = 1000;
33231
+ var HELD_FILE_RETRY_MS = 10;
33232
+ var HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
33233
+ function writeWhole(path, text) {
33234
+ const partial = `${path}.${process.pid}.partial`;
33235
+ try {
33236
+ writeFileSync13(partial, text, "utf8");
33237
+ } catch {
33238
+ return false;
33239
+ }
33240
+ const until = Date.now() + HELD_FILE_MS;
33241
+ for (;; ) {
33242
+ try {
33243
+ renameSync2(partial, path);
33244
+ return true;
33245
+ } catch (error) {
33246
+ const code = error.code ?? "";
33247
+ if (!HELD_BY_A_READER.has(code) || Date.now() >= until) {
33248
+ try {
33249
+ rmSync9(partial, { force: true });
33250
+ } catch {}
33251
+ return false;
33252
+ }
33253
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, HELD_FILE_RETRY_MS);
33254
+ }
33255
+ }
33256
+ }
33257
+
33258
+ // src/run-record.ts
33236
33259
  var execFileAsync = promisify4(execFile4);
33237
33260
  function runsDirectory() {
33238
33261
  return join19(runtimeDirectory(), "runs");
@@ -33267,7 +33290,7 @@ function latestRunPath(servedBy, directory = runtimeDirectory()) {
33267
33290
  function writeLatestRun(servedBy, latest) {
33268
33291
  try {
33269
33292
  mkdirSync11(runsDirectory(), { recursive: true });
33270
- writeFileSync13(latestRunPath(servedBy), JSON.stringify(latest, null, 2), "utf8");
33293
+ writeWhole(latestRunPath(servedBy), JSON.stringify(latest, null, 2));
33271
33294
  } catch {}
33272
33295
  }
33273
33296
  function readLatestRun(servedBy, directory = runtimeDirectory()) {
@@ -33302,7 +33325,10 @@ function editorNotePath(projectPath, directory = runtimeDirectory()) {
33302
33325
  }
33303
33326
  function writeEditorRunNote(note) {
33304
33327
  mkdirSync11(runsDirectory(), { recursive: true });
33305
- writeFileSync13(editorNotePath(note.projectPath), JSON.stringify(note, null, 2), "utf8");
33328
+ const path = editorNotePath(note.projectPath);
33329
+ if (!writeWhole(path, JSON.stringify(note, null, 2))) {
33330
+ throw new Error(`the editor run note could not be written at ${path}`);
33331
+ }
33306
33332
  }
33307
33333
  function readEditorRunNote(projectPath) {
33308
33334
  for (const directory of runtimeDirectories()) {
@@ -33574,7 +33600,7 @@ function clearRunRecord(projectPath) {
33574
33600
  const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
33575
33601
  for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
33576
33602
  try {
33577
- rmSync9(path, { force: true });
33603
+ rmSync10(path, { force: true });
33578
33604
  } catch {}
33579
33605
  }
33580
33606
  }
@@ -33599,7 +33625,8 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
33599
33625
  return;
33600
33626
  }
33601
33627
  for (const entry of entries) {
33602
- if (!entry.startsWith("run-") || !(entry.endsWith(".log") || entry.endsWith(".profile.json"))) {
33628
+ const transcript = entry.startsWith("run-") && (entry.endsWith(".log") || entry.endsWith(".profile.json"));
33629
+ if (!transcript && !entry.endsWith(".partial")) {
33603
33630
  continue;
33604
33631
  }
33605
33632
  const path = join19(directory, entry);
@@ -33607,7 +33634,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
33607
33634
  continue;
33608
33635
  }
33609
33636
  try {
33610
- rmSync9(path);
33637
+ rmSync10(path);
33611
33638
  } catch {}
33612
33639
  }
33613
33640
  }
@@ -33706,7 +33733,7 @@ function withoutListed(entries, errors) {
33706
33733
  }
33707
33734
 
33708
33735
  // src/script-profile.ts
33709
- import { readFileSync as readFileSync19, renameSync as renameSync2, writeFileSync as writeFileSync14 } from "node:fs";
33736
+ import { readFileSync as readFileSync19 } from "node:fs";
33710
33737
 
33711
33738
  // src/godot-variant.ts
33712
33739
  var FLAG_64 = 1 << 16;
@@ -34929,7 +34956,7 @@ var TOOL_SPECS = [
34929
34956
  requires: []
34930
34957
  }
34931
34958
  },
34932
- answer: "stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked.",
34959
+ answer: "stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. arguments is the engine command line the check ran, as a start's is. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked.",
34933
34960
  defaultOperation: "start"
34934
34961
  },
34935
34962
  {
@@ -35623,9 +35650,9 @@ import {
35623
35650
  readdirSync as readdirSync8,
35624
35651
  readFileSync as readFileSync20,
35625
35652
  rmdirSync,
35626
- rmSync as rmSync10,
35653
+ rmSync as rmSync11,
35627
35654
  statSync as statSync7,
35628
- writeFileSync as writeFileSync15
35655
+ writeFileSync as writeFileSync14
35629
35656
  } from "node:fs";
35630
35657
  import { dirname as dirname14, join as join21 } from "node:path";
35631
35658
  import { fileURLToPath as fileURLToPath3 } from "node:url";
@@ -40709,6 +40736,7 @@ class GodotServer {
40709
40736
  frames,
40710
40737
  errors,
40711
40738
  warnings,
40739
+ arguments: cmdArgs,
40712
40740
  argsNote: engineFlagsNote(userArgs),
40713
40741
  ...scanWaitAnswer(scanned),
40714
40742
  savesNote: asked.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
package/build/keeper.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/keeper.ts
4
4
  import { spawn } from "node:child_process";
5
- import { closeSync, existsSync as existsSync2, mkdtempSync as mkdtempSync3, openSync as openSync2, readFileSync as readFileSync3, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "node:fs";
5
+ import { closeSync, existsSync as existsSync2, mkdtempSync as mkdtempSync3, openSync as openSync2, readFileSync as readFileSync2, rmSync as rmSync5, writeFileSync as writeFileSync3 } from "node:fs";
6
6
  import { tmpdir as tmpdir4 } from "node:os";
7
7
  import { join as join5 } from "node:path";
8
8
  import { setTimeout as delay } from "node:timers/promises";
@@ -230,6 +230,14 @@ import { connect, createServer } from "node:net";
230
230
  import { tmpdir } from "node:os";
231
231
  import { join } from "node:path";
232
232
  import process2 from "node:process";
233
+ function endedOrWhyNot(end) {
234
+ try {
235
+ return end();
236
+ } catch (error) {
237
+ return `could not end it: ${error instanceof Error ? error.message : String(error)}`.replaceAll(`
238
+ `, " ");
239
+ }
240
+ }
233
241
  async function listenForAStop(end) {
234
242
  let directory = null;
235
243
  try {
@@ -244,7 +252,7 @@ async function listenForAStop(end) {
244
252
  if (asked.includes(`
245
253
  `)) {
246
254
  socket.end(asked.startsWith(`stop
247
- `) ? `${end()}
255
+ `) ? `${endedOrWhyNot(end)}
248
256
  ` : `unknown
249
257
  `);
250
258
  }
@@ -315,16 +323,7 @@ function readLaunched(printed) {
315
323
  // src/run-record.ts
316
324
  import { execFile as execFile2 } from "node:child_process";
317
325
  import { createHash as createHash2 } from "node:crypto";
318
- import {
319
- mkdirSync,
320
- openSync,
321
- readdirSync,
322
- readFileSync,
323
- realpathSync,
324
- rmSync as rmSync2,
325
- statSync,
326
- writeFileSync as writeFileSync2
327
- } from "node:fs";
326
+ import { mkdirSync, openSync, readdirSync, readFileSync, realpathSync, rmSync as rmSync3, statSync } from "node:fs";
328
327
  import { basename, dirname as dirname2, join as join3, resolve as resolve2 } from "node:path";
329
328
  import { promisify as promisify2 } from "node:util";
330
329
 
@@ -405,6 +404,36 @@ var verdicts = new Map;
405
404
  var LATE_ANSWER_MS = 30 * 60000;
406
405
  var lateAnswers = new Map;
407
406
 
407
+ // src/whole-file.ts
408
+ import { renameSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
409
+ var HELD_FILE_MS = 1000;
410
+ var HELD_FILE_RETRY_MS = 10;
411
+ var HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
412
+ function writeWhole(path, text) {
413
+ const partial = `${path}.${process.pid}.partial`;
414
+ try {
415
+ writeFileSync2(partial, text, "utf8");
416
+ } catch {
417
+ return false;
418
+ }
419
+ const until = Date.now() + HELD_FILE_MS;
420
+ for (;; ) {
421
+ try {
422
+ renameSync(partial, path);
423
+ return true;
424
+ } catch (error) {
425
+ const code = error.code ?? "";
426
+ if (!HELD_BY_A_READER.has(code) || Date.now() >= until) {
427
+ try {
428
+ rmSync2(partial, { force: true });
429
+ } catch {}
430
+ return false;
431
+ }
432
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, HELD_FILE_RETRY_MS);
433
+ }
434
+ }
435
+ }
436
+
408
437
  // src/run-record.ts
409
438
  var execFileAsync = promisify2(execFile2);
410
439
  function runsDirectory() {
@@ -436,7 +465,10 @@ function runRecordPath(projectPath, directory = runtimeDirectory()) {
436
465
  }
437
466
  function writeRunRecord(record) {
438
467
  mkdirSync(runsDirectory(), { recursive: true });
439
- writeFileSync2(runRecordPath(record.projectPath), JSON.stringify(record, null, 2), "utf8");
468
+ const path = runRecordPath(record.projectPath);
469
+ if (!writeWhole(path, JSON.stringify(record, null, 2))) {
470
+ throw new Error(`the run note could not be written at ${path}`);
471
+ }
440
472
  }
441
473
  function recordRunEnded(projectPath, pid, ending) {
442
474
  const path = runRecordPath(projectPath);
@@ -450,9 +482,7 @@ function recordRunEnded(projectPath, pid, ending) {
450
482
  ...ending.exitSignal === null ? {} : { exitSignal: ending.exitSignal },
451
483
  endedAt: Date.now()
452
484
  };
453
- try {
454
- writeFileSync2(path, JSON.stringify(written, null, 2), "utf8");
455
- } catch {}
485
+ writeWhole(path, JSON.stringify(written, null, 2));
456
486
  }
457
487
  function recordAt(path) {
458
488
  let parsed;
@@ -490,7 +520,7 @@ function recordAt(path) {
490
520
  }
491
521
 
492
522
  // src/scratch.ts
493
- import { existsSync, mkdtempSync as mkdtempSync2, readdirSync as readdirSync2, rmSync as rmSync3 } from "node:fs";
523
+ import { existsSync, mkdtempSync as mkdtempSync2, readdirSync as readdirSync2, rmSync as rmSync4 } from "node:fs";
494
524
  import { tmpdir as tmpdir3 } from "node:os";
495
525
  import { join as join4 } from "node:path";
496
526
  import process3 from "node:process";
@@ -523,7 +553,7 @@ function discard(...paths) {
523
553
  return left;
524
554
  }
525
555
  function remove(path) {
526
- rmSync3(path, SWEPT);
556
+ rmSync4(path, SWEPT);
527
557
  }
528
558
  function tryAgain(path, attempt) {
529
559
  if (attempt === 0) {
@@ -567,7 +597,6 @@ function lastTryOnExit() {
567
597
  }
568
598
 
569
599
  // src/script-profile.ts
570
- import { readFileSync as readFileSync2, renameSync, writeFileSync as writeFileSync3 } from "node:fs";
571
600
  import { createServer as createServer2 } from "node:net";
572
601
 
573
602
  // src/gdscript-source.ts
@@ -1099,9 +1128,7 @@ function parseIniLikeValue(value) {
1099
1128
 
1100
1129
  // src/script-profile.ts
1101
1130
  function writeProfileFile(path, file) {
1102
- const partial = `${path}.${process.pid}.partial`;
1103
- writeFileSync3(partial, JSON.stringify(file), "utf8");
1104
- renameSync(partial, path);
1131
+ return writeWhole(path, JSON.stringify(file));
1105
1132
  }
1106
1133
  var QUEUE_SETTING = "limits/debugger/max_queued_messages";
1107
1134
  var QUEUE_DEFAULT = 2048;
@@ -1341,12 +1368,12 @@ async function startThroughHelper(spec, output, stopFile) {
1341
1368
  exited.now = true;
1342
1369
  });
1343
1370
  for (let waited = 0;waited < DESKTOP_START_MS; waited += 50) {
1344
- const pid = existsSync2(pidFile) ? Number(readFileSync3(pidFile, "utf8").trim()) : Number.NaN;
1371
+ const pid = existsSync2(pidFile) ? Number(readFileSync2(pidFile, "utf8").trim()) : Number.NaN;
1345
1372
  if (Number.isInteger(pid) && pid > 0) {
1346
1373
  return { child: started.child, pid };
1347
1374
  }
1348
1375
  if (exited.now) {
1349
- const why = existsSync2(errorFile) ? readFileSync3(errorFile, "utf8").trim() : "it exited saying nothing";
1376
+ const why = existsSync2(errorFile) ? readFileSync2(errorFile, "utf8").trim() : "it exited saying nothing";
1350
1377
  return { error: `it could not be started ${where}: ${why}` };
1351
1378
  }
1352
1379
  await delay(50);
@@ -1358,18 +1385,19 @@ async function startThroughHelper(spec, output, stopFile) {
1358
1385
  }
1359
1386
  }
1360
1387
  var PROFILE_WRITE_MS = 500;
1388
+ var LAST_WRITE_TRIES = 5;
1361
1389
  async function profiledBy(path, projectPath) {
1362
1390
  let latest = null;
1363
1391
  let timer = null;
1364
1392
  const write = () => {
1365
1393
  timer = null;
1366
- if (latest !== null) {
1367
- writeProfileFile(path, { ...latest, connected: true });
1394
+ if (latest !== null && !writeProfileFile(path, { ...latest, connected: true })) {
1395
+ timer = setTimeout(write, PROFILE_WRITE_MS);
1368
1396
  }
1369
1397
  };
1370
1398
  let projectGodot = null;
1371
1399
  try {
1372
- projectGodot = readFileSync3(join5(projectPath, "project.godot"), "utf8");
1400
+ projectGodot = readFileSync2(join5(projectPath, "project.godot"), "utf8");
1373
1401
  } catch {}
1374
1402
  const listener = await ProfileListener.open((totals) => {
1375
1403
  const first = latest === null;
@@ -1388,7 +1416,10 @@ async function profiledBy(path, projectPath) {
1388
1416
  clearTimeout(timer);
1389
1417
  }
1390
1418
  latest = listener.totals() ?? latest;
1391
- write();
1419
+ let tries = 0;
1420
+ while (latest !== null && !writeProfileFile(path, { ...latest, connected: true }) && tries < LAST_WRITE_TRIES) {
1421
+ tries += 1;
1422
+ }
1392
1423
  listener.close();
1393
1424
  }
1394
1425
  };
@@ -1453,6 +1484,11 @@ async function launch() {
1453
1484
  process.exit(0);
1454
1485
  });
1455
1486
  }
1487
+ function writeNoteIfItCan(record) {
1488
+ try {
1489
+ writeRunRecord(record);
1490
+ } catch {}
1491
+ }
1456
1492
  async function keep() {
1457
1493
  const spec = await receivedSpec();
1458
1494
  const run = spec.run;
@@ -1493,7 +1529,7 @@ async function keep() {
1493
1529
  command: spec.command
1494
1530
  };
1495
1531
  if (record !== null) {
1496
- writeRunRecord(record);
1532
+ writeNoteIfItCan(record);
1497
1533
  }
1498
1534
  let listening = null;
1499
1535
  child.once("exit", (code, signal) => {
@@ -1503,20 +1539,20 @@ async function keep() {
1503
1539
  profiled?.finish();
1504
1540
  listening?.close();
1505
1541
  if (own !== null) {
1506
- rmSync4(own, { recursive: true, force: true });
1542
+ rmSync5(own, { recursive: true, force: true });
1507
1543
  }
1508
1544
  process.exit(0);
1509
1545
  });
1510
1546
  if (record !== null) {
1511
1547
  listening = await listenForAStop(() => {
1512
1548
  if (stopFile !== "") {
1513
- writeFileSync4(stopFile, "");
1549
+ writeFileSync3(stopFile, "");
1514
1550
  return "signalled";
1515
1551
  }
1516
1552
  return child.kill() ? "signalled" : "gone";
1517
1553
  });
1518
1554
  if (listening !== null) {
1519
- writeRunRecord({ ...record, keeper: listening.address });
1555
+ writeNoteIfItCan({ ...record, keeper: listening.address });
1520
1556
  }
1521
1557
  }
1522
1558
  process.stdout.write(`pid ${pid}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.2.19",
3
+ "version": "1.2.20",
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",