gdharness 1.2.0 → 1.2.1

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
@@ -18421,7 +18421,7 @@ var init_tool_definitions = __esm(() => {
18421
18421
  },
18422
18422
  {
18423
18423
  name: "editor_output",
18424
- description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone, with errorsUnread true and no clean, since what it raised reaches nothing here. Answers with the counts and the verdict as well as the entries. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts; a run ended by a signal has no exit code, and exitSignal names the signal instead. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, which is where the output worth reading usually is, since the class cache, the plugins and the language server all say what they have to say before any harness has connected. The answer carries the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing.",
18424
+ description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone, with errorsUnread true and no clean, since what it raised reaches nothing here. Answers with the counts and the verdict as well as the entries. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts; a run ended by a signal has no exit code, and exitSignal names the signal instead. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, which is where the output worth reading usually is, since the class cache, the plugins and the language server all say what they have to say before any harness has connected. The answer carries the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing. startedAt is when the run began and elapsedMs how long it has been going; for a run that is over, endedAt is when it ended and elapsedMs how long it ran, and both are left out when nobody saw the end, as for a game closed in the editor or a run picked up from a note written before ends were recorded.",
18425
18425
  parameters: {
18426
18426
  severity: {
18427
18427
  type: "string",
@@ -18469,7 +18469,7 @@ var init_tool_definitions = __esm(() => {
18469
18469
  },
18470
18470
  {
18471
18471
  name: "editor_status",
18472
- description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote, and editor_launch restart binds it again. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
18472
+ description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. startedAt says when it began, and endedAt when it ended, wherever something saw the end: a run adopted from a note another server wrote can have ended hours before this server started. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote, and editor_launch restart binds it again. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
18473
18473
  parameters: {},
18474
18474
  requires: []
18475
18475
  },
@@ -42202,7 +42202,8 @@ function recordAt(path) {
42202
42202
  arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : [],
42203
42203
  ...typeof fields["command"] === "string" ? { command: fields["command"] } : {},
42204
42204
  ...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
42205
- ...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {}
42205
+ ...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {},
42206
+ ...typeof fields["endedAt"] === "number" ? { endedAt: fields["endedAt"] } : {}
42206
42207
  };
42207
42208
  }
42208
42209
  async function runningAs(pid) {
@@ -43482,7 +43483,7 @@ function profileAnswer(reading, limit) {
43482
43483
  notes.push(`${table.superCallers === 1 ? "One function calls" : `${table.superCallers} functions call`} through super, marked selfIncludesSuper: the engine takes every other call out of the caller's self time, and not a super call, so ${table.superCallers === 1 ? "its" : "their"} selfMs holds the overridden function's time, which that function's own row counts again. Self time therefore cannot be added along an override chain, and scriptMs counts the time under a super call once more for each level above it.`);
43483
43484
  }
43484
43485
  if (table.overheadDominated > 0 && table.perCallUs !== null) {
43485
- notes.push(`The profiler costs about ${table.perCallUs}µs a call in this run, measured on an empty function the runtime addon calls as it goes on, and that time lands in the self time of the function called. ${table.overheadDominated === 1 ? "One function has" : `${table.overheadDominated} functions have`} at least half ${table.overheadDominated === 1 ? "its" : "their"} selfMs in it, under overheadMs: a function that does little and is called often looks expensive here and costs that much less once the profiler is off, so inlining it gains little.`);
43486
+ notes.push(`The profiler costs about ${table.perCallUs}µs a call in this run, measured on an empty function the runtime addon calls as it goes on, and that time lands in the self time of the function called. ${table.overheadDominated === 1 ? "One function has" : `${table.overheadDominated} functions have`} at least half ${table.overheadDominated === 1 ? "its" : "their"} selfMs in it, under overheadMs: that much of its time is the profiler's and goes once the profiler is off, whether the function is inlined or not. What inlining saves is the call itself, which costs something without the profiler too, not the work inside it.`);
43486
43487
  }
43487
43488
  notes.push(...cappedNotes(totals));
43488
43489
  if (totals.unreadable > 0) {
@@ -45932,7 +45933,9 @@ class GodotServer {
45932
45933
  return {
45933
45934
  pid: run.pid ?? this.announcedPidOf(run) ?? run.announcedPid ?? null,
45934
45935
  ...run.throughEditor || run.projectPath === null ? {} : { project: run.projectPath },
45935
- ...run.pickedUp === true ? { adopted: true } : {}
45936
+ ...run.pickedUp === true ? { adopted: true } : {},
45937
+ startedAt: new Date(run.startedAt).toISOString(),
45938
+ ...run.endedAt === undefined ? {} : { endedAt: new Date(run.endedAt).toISOString() }
45936
45939
  };
45937
45940
  }
45938
45941
  async handleEditorStatus() {
@@ -46705,10 +46708,11 @@ class GodotServer {
46705
46708
  return "unreached";
46706
46709
  }
46707
46710
  running.endedThrough = "editor";
46708
- if (announced === undefined) {
46709
- return await this.untilTheEditorStopsPlaying() ? "gone" : "lingering";
46711
+ const stopped = announced === undefined ? await this.untilTheEditorStopsPlaying() : await untilGone([announced]);
46712
+ if (stopped) {
46713
+ running.endedAt = Date.now();
46710
46714
  }
46711
- return await untilGone([announced]) ? "gone" : "lingering";
46715
+ return stopped ? "gone" : "lingering";
46712
46716
  }
46713
46717
  if (running.pid === null) {
46714
46718
  return "gone";
@@ -46757,6 +46761,7 @@ class GodotServer {
46757
46761
  if (landed && gone) {
46758
46762
  running.exitCode = null;
46759
46763
  running.exitSignal = "SIGTERM";
46764
+ running.endedAt ??= Date.now();
46760
46765
  }
46761
46766
  if (landed) {
46762
46767
  running.endedThrough = "signal";
@@ -46781,6 +46786,7 @@ class GodotServer {
46781
46786
  if (asked === "signalled" && gone) {
46782
46787
  run.exitCode = null;
46783
46788
  run.exitSignal = "SIGTERM";
46789
+ run.endedAt ??= Date.now();
46784
46790
  }
46785
46791
  run.log.record(gone ? "info" : "warning", !gone ? `gdharness had the keeper holding pid ${pid} end it, and it was still running ${stopWaitMs() / 1000} seconds later.` : asked === "signalled" ? `gdharness ended pid ${pid} through the keeper holding it.` : `gdharness asked the keeper holding pid ${pid} to end it, and it had already gone.`);
46786
46792
  return gone ? "gone" : "lingering";
@@ -46828,6 +46834,7 @@ class GodotServer {
46828
46834
  this.logDebug(`Godot process exited with code ${code ?? "none"}, signal ${signal ?? "none"}`);
46829
46835
  started.exitCode = code;
46830
46836
  started.exitSignal = code === null ? signal : null;
46837
+ started.endedAt = Date.now();
46831
46838
  });
46832
46839
  child.on("error", (err) => {
46833
46840
  console.error("Failed to start Godot process:", err);
@@ -47142,6 +47149,9 @@ class GodotServer {
47142
47149
  adopted.log.finish();
47143
47150
  adopted.exitCode = record.exitCode ?? null;
47144
47151
  adopted.exitSignal = record.exitSignal ?? null;
47152
+ if (record.endedAt !== undefined) {
47153
+ adopted.endedAt = record.endedAt;
47154
+ }
47145
47155
  const exitMayBeComing = !alive(record.pid) && !exited(adopted);
47146
47156
  if (!exitMayBeComing) {
47147
47157
  adopted.endedUnwatched = !exited(adopted);
@@ -47435,6 +47445,9 @@ class GodotServer {
47435
47445
  if (record !== null && record.pid === run.pid && (record.exitCode !== undefined || record.exitSignal !== undefined)) {
47436
47446
  run.exitCode = record.exitCode ?? null;
47437
47447
  run.exitSignal = record.exitSignal ?? null;
47448
+ if (record.endedAt !== undefined) {
47449
+ run.endedAt = record.endedAt;
47450
+ }
47438
47451
  return true;
47439
47452
  }
47440
47453
  if (Date.now() >= until) {
@@ -47581,7 +47594,8 @@ class GodotServer {
47581
47594
  project: run.throughEditor ? undefined : run.projectPath ?? undefined,
47582
47595
  transcript: run.transcript ?? undefined,
47583
47596
  startedAt: new Date(run.startedAt).toISOString(),
47584
- elapsedMs: Date.now() - run.startedAt,
47597
+ endedAt: stillRunning(run) || run.endedAt === undefined ? undefined : new Date(run.endedAt).toISOString(),
47598
+ elapsedMs: stillRunning(run) ? Date.now() - run.startedAt : run.endedAt === undefined ? undefined : run.endedAt - run.startedAt,
47585
47599
  cpuSeconds,
47586
47600
  note: notes.length > 0 ? notes.join(" ") : undefined,
47587
47601
  omitted: selected.omitted,
@@ -201,8 +201,8 @@ func _send_the_profile() -> void:
201
201
  ## What the script profiler costs per call in this game, measured once each time it is switched on.
202
202
  ##
203
203
  ## The profiler times every call it counts, and that bookkeeping lands in the self time of the
204
- ## function called: a function doing almost nothing, called six hundred thousand times, read as a
205
- ## second of its own, and inlining it saved nothing once the profiler was off. So an empty function
204
+ ## function called, so a function doing almost nothing, called six hundred thousand times, can read
205
+ ## as a second of its own that is mostly the profiler's. So an empty function
206
206
  ## is called here [constant PROFILER_PROBE_CALLS] times while the profiler watches, under the load
207
207
  ## the game is under, and the server reads its row as the cost of one call.
208
208
  func _calibrate_the_profiler() -> void:
@@ -560,7 +560,7 @@ func click(params: Dictionary) -> Dictionary:
560
560
  ## that does not process while the game is paused is hovered and never handed the event.
561
561
  func _takes_no_click(control: Control, node_path: String) -> String:
562
562
  if not control.is_visible_in_tree():
563
- return "%s is not visible, so nothing can click it" % node_path
563
+ return "%s is not visible%s, so nothing can click it" % [node_path, Targets.why_hidden(control)]
564
564
  var button: BaseButton = control as BaseButton
565
565
  if button != null and button.disabled:
566
566
  return "%s is disabled, so clicking it presses nothing" % node_path
@@ -681,7 +681,10 @@ func _press_button(viewport: Viewport, event: InputEventMouseButton) -> void:
681
681
  ## anything the engine can be asked, so it is not claimed.
682
682
  func _click_in_the_world(node_path: String, item: Node3D, params: Dictionary) -> Dictionary:
683
683
  if not item.is_visible_in_tree():
684
- return {"type": "error", "message": "%s is not visible, so nothing can click it" % node_path}
684
+ return {
685
+ "type": "error",
686
+ "message": "%s is not visible%s, so nothing can click it" % [node_path, Targets.why_hidden(item)]
687
+ }
685
688
 
686
689
  var found: Dictionary = Queries.in_frame(item)
687
690
  if found.is_empty():
@@ -376,6 +376,93 @@ static func drawn_over(target: Control) -> Node:
376
376
  return _cover_of(target, drawn, _areas_of(drawn))
377
377
 
378
378
 
379
+ ## Why [param node], which is not visible in the tree, is not, as a parenthesis to follow its path:
380
+ ## which nodes hide it, nearest first, and what is shown beside the nearest. Reading visible off
381
+ ## the node itself says true when only an ancestor is hidden, so without this a caller had nothing
382
+ ## to unhide; every hidden one is named because unhiding only the nearest leaves the rest hiding it.
383
+ ## "" when nothing on the way up says so.
384
+ static func why_hidden(node: Node) -> String:
385
+ var hiding: Array[Node] = _hiding(node)
386
+ if hiding.is_empty():
387
+ return ""
388
+ var named: Array[String] = []
389
+ for one: Node in hiding:
390
+ if one == node:
391
+ named.append("its own visible is false")
392
+ else:
393
+ named.append("%s is hidden%s" % [one.get_path(), "" if named.is_empty() else " too"])
394
+ var beside: Array[String] = []
395
+ var parent: Node = hiding[0].get_parent()
396
+ if parent != null:
397
+ for sibling: Node in parent.get_children():
398
+ if sibling != hiding[0] and _shown(sibling):
399
+ beside.append(str(sibling.get_path()))
400
+ return " (%s%s)" % [", and ".join(named), _shown_beside(beside)]
401
+
402
+
403
+ ## "; A, B and C beside it are shown" for [param beside], naming three at most and counting the rest.
404
+ static func _shown_beside(beside: Array[String]) -> String:
405
+ if beside.is_empty():
406
+ return ""
407
+ if beside.size() == 1:
408
+ return "; %s beside it is shown" % beside[0]
409
+ var listed: Array[String] = beside.slice(0, 3)
410
+ var rest: int = beside.size() - listed.size()
411
+ if rest > 0:
412
+ return "; %s and %d more beside it are shown" % [", ".join(listed), rest]
413
+ var last: String = listed.pop_back()
414
+ return "; %s and %s beside it are shown" % [", ".join(listed), last]
415
+
416
+
417
+ ## The nodes whose visible is false and keeps [param node] from being drawn, nearest first. Walked
418
+ ## the way the engine inherits visibility rather than by parent: an item under a plain Node is drawn
419
+ ## from its canvas layer, whatever a control above that Node says, so naming that control would send
420
+ ## a caller to unhide something that hides nothing.
421
+ static func _hiding(node: Node) -> Array[Node]:
422
+ var hiding: Array[Node] = []
423
+ var at: Node = node
424
+ while at != null:
425
+ var item: CanvasItem = at as CanvasItem
426
+ var spatial: Node3D = at as Node3D
427
+ if item != null:
428
+ if not item.visible:
429
+ hiding.append(item)
430
+ var above: CanvasItem = item.get_parent() as CanvasItem
431
+ if above != null:
432
+ at = above
433
+ continue
434
+ var layer: CanvasLayer = item.get_canvas_layer_node()
435
+ if layer != null and not layer.visible:
436
+ hiding.append(layer)
437
+ at = item.get_viewport()
438
+ elif spatial != null:
439
+ if not spatial.visible:
440
+ hiding.append(spatial)
441
+ at = spatial.get_parent_node_3d()
442
+ if at == null:
443
+ at = spatial.get_viewport()
444
+ else:
445
+ var window: Window = at as Window
446
+ if window != null and not window.visible:
447
+ hiding.append(window)
448
+ at = null
449
+ return hiding
450
+
451
+
452
+ ## Whether [param node] is something drawn and on show, for naming what stands beside a hidden one.
453
+ static func _shown(node: Node) -> bool:
454
+ var item: CanvasItem = node as CanvasItem
455
+ if item != null:
456
+ return item.is_visible_in_tree()
457
+ var spatial: Node3D = node as Node3D
458
+ if spatial != null:
459
+ return spatial.is_visible_in_tree()
460
+ var layer: CanvasLayer = node as CanvasLayer
461
+ if layer != null:
462
+ return layer.visible
463
+ return false
464
+
465
+
379
466
  ## Everything in the tree [param root] heads that a pointer can land on: each control on screen
380
467
  ## that does not let the pointer through, and each window embedded in another.
381
468
  static func _pointer_takers(root: Node) -> Array[Node]:
@@ -5,6 +5,7 @@ extends RefCounted
5
5
 
6
6
  const Menus = preload("runtime_menus.gd")
7
7
  const Read = preload("reading.gd")
8
+ const Targets = preload("runtime_targets.gd")
8
9
  const Values = preload("runtime_values.gd")
9
10
  const Words = preload("runtime_words.gd")
10
11
 
@@ -251,7 +252,11 @@ func choose(params: Dictionary) -> Dictionary:
251
252
  if holder != null and holder.disabled:
252
253
  return {"type": "error", "message": "%s is disabled, so its menu cannot be opened" % node_path}
253
254
  if holder != null and not holder.is_visible_in_tree():
254
- return {"type": "error", "message": "%s is not visible, so its menu cannot be opened" % node_path}
255
+ return {
256
+ "type": "error",
257
+ "message":
258
+ "%s is not visible%s, so its menu cannot be opened" % [node_path, Targets.why_hidden(holder)]
259
+ }
255
260
 
256
261
  # Shown first, because a menu nobody has opened has no focus to move and the press would go to
257
262
  # whatever is behind it, and before the item is looked for, because opening is when a game fills
package/build/index.js CHANGED
@@ -33008,7 +33008,8 @@ function recordAt(path) {
33008
33008
  arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : [],
33009
33009
  ...typeof fields["command"] === "string" ? { command: fields["command"] } : {},
33010
33010
  ...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
33011
- ...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {}
33011
+ ...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {},
33012
+ ...typeof fields["endedAt"] === "number" ? { endedAt: fields["endedAt"] } : {}
33012
33013
  };
33013
33014
  }
33014
33015
  async function runningAs(pid) {
@@ -34497,7 +34498,7 @@ var TOOL_SPECS = [
34497
34498
  },
34498
34499
  {
34499
34500
  name: "editor_output",
34500
- description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone, with errorsUnread true and no clean, since what it raised reaches nothing here. Answers with the counts and the verdict as well as the entries. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts; a run ended by a signal has no exit code, and exitSignal names the signal instead. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, which is where the output worth reading usually is, since the class cache, the plugins and the language server all say what they have to say before any harness has connected. The answer carries the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing.",
34501
+ description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone, with errorsUnread true and no clean, since what it raised reaches nothing here. Answers with the counts and the verdict as well as the entries. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts; a run ended by a signal has no exit code, and exitSignal names the signal instead. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, which is where the output worth reading usually is, since the class cache, the plugins and the language server all say what they have to say before any harness has connected. The answer carries the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing. startedAt is when the run began and elapsedMs how long it has been going; for a run that is over, endedAt is when it ended and elapsedMs how long it ran, and both are left out when nobody saw the end, as for a game closed in the editor or a run picked up from a note written before ends were recorded.",
34501
34502
  parameters: {
34502
34503
  severity: {
34503
34504
  type: "string",
@@ -34545,7 +34546,7 @@ var TOOL_SPECS = [
34545
34546
  },
34546
34547
  {
34547
34548
  name: "editor_status",
34548
- description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote, and editor_launch restart binds it again. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
34549
+ description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. startedAt says when it began, and endedAt when it ended, wherever something saw the end: a run adopted from a note another server wrote can have ended hours before this server started. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote, and editor_launch restart binds it again. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
34549
34550
  parameters: {},
34550
34551
  requires: []
34551
34552
  },
@@ -35976,7 +35977,7 @@ function profileAnswer(reading, limit) {
35976
35977
  notes.push(`${table.superCallers === 1 ? "One function calls" : `${table.superCallers} functions call`} through super, marked selfIncludesSuper: the engine takes every other call out of the caller's self time, and not a super call, so ${table.superCallers === 1 ? "its" : "their"} selfMs holds the overridden function's time, which that function's own row counts again. Self time therefore cannot be added along an override chain, and scriptMs counts the time under a super call once more for each level above it.`);
35977
35978
  }
35978
35979
  if (table.overheadDominated > 0 && table.perCallUs !== null) {
35979
- notes.push(`The profiler costs about ${table.perCallUs}µs a call in this run, measured on an empty function the runtime addon calls as it goes on, and that time lands in the self time of the function called. ${table.overheadDominated === 1 ? "One function has" : `${table.overheadDominated} functions have`} at least half ${table.overheadDominated === 1 ? "its" : "their"} selfMs in it, under overheadMs: a function that does little and is called often looks expensive here and costs that much less once the profiler is off, so inlining it gains little.`);
35980
+ notes.push(`The profiler costs about ${table.perCallUs}µs a call in this run, measured on an empty function the runtime addon calls as it goes on, and that time lands in the self time of the function called. ${table.overheadDominated === 1 ? "One function has" : `${table.overheadDominated} functions have`} at least half ${table.overheadDominated === 1 ? "its" : "their"} selfMs in it, under overheadMs: that much of its time is the profiler's and goes once the profiler is off, whether the function is inlined or not. What inlining saves is the call itself, which costs something without the profiler too, not the work inside it.`);
35980
35981
  }
35981
35982
  notes.push(...cappedNotes(totals));
35982
35983
  if (totals.unreadable > 0) {
@@ -38429,7 +38430,9 @@ class GodotServer {
38429
38430
  return {
38430
38431
  pid: run.pid ?? this.announcedPidOf(run) ?? run.announcedPid ?? null,
38431
38432
  ...run.throughEditor || run.projectPath === null ? {} : { project: run.projectPath },
38432
- ...run.pickedUp === true ? { adopted: true } : {}
38433
+ ...run.pickedUp === true ? { adopted: true } : {},
38434
+ startedAt: new Date(run.startedAt).toISOString(),
38435
+ ...run.endedAt === undefined ? {} : { endedAt: new Date(run.endedAt).toISOString() }
38433
38436
  };
38434
38437
  }
38435
38438
  async handleEditorStatus() {
@@ -39202,10 +39205,11 @@ class GodotServer {
39202
39205
  return "unreached";
39203
39206
  }
39204
39207
  running.endedThrough = "editor";
39205
- if (announced === undefined) {
39206
- return await this.untilTheEditorStopsPlaying() ? "gone" : "lingering";
39208
+ const stopped = announced === undefined ? await this.untilTheEditorStopsPlaying() : await untilGone([announced]);
39209
+ if (stopped) {
39210
+ running.endedAt = Date.now();
39207
39211
  }
39208
- return await untilGone([announced]) ? "gone" : "lingering";
39212
+ return stopped ? "gone" : "lingering";
39209
39213
  }
39210
39214
  if (running.pid === null) {
39211
39215
  return "gone";
@@ -39254,6 +39258,7 @@ class GodotServer {
39254
39258
  if (landed && gone) {
39255
39259
  running.exitCode = null;
39256
39260
  running.exitSignal = "SIGTERM";
39261
+ running.endedAt ??= Date.now();
39257
39262
  }
39258
39263
  if (landed) {
39259
39264
  running.endedThrough = "signal";
@@ -39278,6 +39283,7 @@ class GodotServer {
39278
39283
  if (asked === "signalled" && gone) {
39279
39284
  run.exitCode = null;
39280
39285
  run.exitSignal = "SIGTERM";
39286
+ run.endedAt ??= Date.now();
39281
39287
  }
39282
39288
  run.log.record(gone ? "info" : "warning", !gone ? `gdharness had the keeper holding pid ${pid} end it, and it was still running ${stopWaitMs() / 1000} seconds later.` : asked === "signalled" ? `gdharness ended pid ${pid} through the keeper holding it.` : `gdharness asked the keeper holding pid ${pid} to end it, and it had already gone.`);
39283
39289
  return gone ? "gone" : "lingering";
@@ -39325,6 +39331,7 @@ class GodotServer {
39325
39331
  this.logDebug(`Godot process exited with code ${code ?? "none"}, signal ${signal ?? "none"}`);
39326
39332
  started.exitCode = code;
39327
39333
  started.exitSignal = code === null ? signal : null;
39334
+ started.endedAt = Date.now();
39328
39335
  });
39329
39336
  child.on("error", (err) => {
39330
39337
  console.error("Failed to start Godot process:", err);
@@ -39639,6 +39646,9 @@ class GodotServer {
39639
39646
  adopted.log.finish();
39640
39647
  adopted.exitCode = record.exitCode ?? null;
39641
39648
  adopted.exitSignal = record.exitSignal ?? null;
39649
+ if (record.endedAt !== undefined) {
39650
+ adopted.endedAt = record.endedAt;
39651
+ }
39642
39652
  const exitMayBeComing = !alive(record.pid) && !exited(adopted);
39643
39653
  if (!exitMayBeComing) {
39644
39654
  adopted.endedUnwatched = !exited(adopted);
@@ -39932,6 +39942,9 @@ class GodotServer {
39932
39942
  if (record !== null && record.pid === run.pid && (record.exitCode !== undefined || record.exitSignal !== undefined)) {
39933
39943
  run.exitCode = record.exitCode ?? null;
39934
39944
  run.exitSignal = record.exitSignal ?? null;
39945
+ if (record.endedAt !== undefined) {
39946
+ run.endedAt = record.endedAt;
39947
+ }
39935
39948
  return true;
39936
39949
  }
39937
39950
  if (Date.now() >= until) {
@@ -40078,7 +40091,8 @@ class GodotServer {
40078
40091
  project: run.throughEditor ? undefined : run.projectPath ?? undefined,
40079
40092
  transcript: run.transcript ?? undefined,
40080
40093
  startedAt: new Date(run.startedAt).toISOString(),
40081
- elapsedMs: Date.now() - run.startedAt,
40094
+ endedAt: stillRunning(run) || run.endedAt === undefined ? undefined : new Date(run.endedAt).toISOString(),
40095
+ elapsedMs: stillRunning(run) ? Date.now() - run.startedAt : run.endedAt === undefined ? undefined : run.endedAt - run.startedAt,
40082
40096
  cpuSeconds,
40083
40097
  note: notes.length > 0 ? notes.join(" ") : undefined,
40084
40098
  omitted: selected.omitted,
package/build/keeper.js CHANGED
@@ -421,7 +421,8 @@ function recordRunEnded(projectPath, pid, ending) {
421
421
  const written = {
422
422
  ...record,
423
423
  ...ending.exitCode === null ? {} : { exitCode: ending.exitCode },
424
- ...ending.exitSignal === null ? {} : { exitSignal: ending.exitSignal }
424
+ ...ending.exitSignal === null ? {} : { exitSignal: ending.exitSignal },
425
+ endedAt: Date.now()
425
426
  };
426
427
  try {
427
428
  writeFileSync2(path, JSON.stringify(written, null, 2), "utf8");
@@ -457,7 +458,8 @@ function recordAt(path) {
457
458
  arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : [],
458
459
  ...typeof fields["command"] === "string" ? { command: fields["command"] } : {},
459
460
  ...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
460
- ...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {}
461
+ ...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {},
462
+ ...typeof fields["endedAt"] === "number" ? { endedAt: fields["endedAt"] } : {}
461
463
  };
462
464
  }
463
465
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
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",