gdharness 0.5.5 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js
CHANGED
|
@@ -7153,10 +7153,10 @@ function runLine(runner, version, rest = "") {
|
|
|
7153
7153
|
function spawnFor(runner, version, execPath = process2.execPath) {
|
|
7154
7154
|
const spec = `gdharness@${version}`;
|
|
7155
7155
|
if (runner !== currentRunner()) {
|
|
7156
|
-
return { command: runner, args: runner === "npx" ? ["-y", spec] : [spec] };
|
|
7156
|
+
return { command: runner, args: runner === "npx" ? ["-y", spec] : ["--bun", spec] };
|
|
7157
7157
|
}
|
|
7158
7158
|
if (runner === "bunx") {
|
|
7159
|
-
return { command: execPath, args: ["x", spec] };
|
|
7159
|
+
return { command: execPath, args: ["x", "--bun", spec] };
|
|
7160
7160
|
}
|
|
7161
7161
|
return { command: alongside(execPath, "npx") ?? "npx", args: ["-y", spec] };
|
|
7162
7162
|
}
|
|
@@ -34164,6 +34164,8 @@ var init_godot_bridge = __esm(() => {
|
|
|
34164
34164
|
server.once("error", (error) => {
|
|
34165
34165
|
if (!settled) {
|
|
34166
34166
|
settled = true;
|
|
34167
|
+
server.close();
|
|
34168
|
+
godotWss.close();
|
|
34167
34169
|
reject(error);
|
|
34168
34170
|
return;
|
|
34169
34171
|
}
|
|
@@ -35730,6 +35732,7 @@ class GodotServer {
|
|
|
35730
35732
|
lspClient = null;
|
|
35731
35733
|
dapClient = null;
|
|
35732
35734
|
bridgeStartupError = null;
|
|
35735
|
+
bridgeRetry = null;
|
|
35733
35736
|
lastProjectPath = null;
|
|
35734
35737
|
shutdownInitiated = false;
|
|
35735
35738
|
constructor() {
|
|
@@ -35786,11 +35789,43 @@ class GodotServer {
|
|
|
35786
35789
|
const reason = errorMessage(bridgeError);
|
|
35787
35790
|
this.bridgeStartupError = code && !reason.includes(code) ? `${code}: ${reason}` : reason;
|
|
35788
35791
|
console.error(`[SERVER] Warning: Godot Editor Bridge failed to start: ${this.bridgeStartupError}`);
|
|
35789
|
-
console.error("[SERVER] Continuing without bridge-backed editor tools.");
|
|
35792
|
+
console.error("[SERVER] Continuing without bridge-backed editor tools, and trying again.");
|
|
35793
|
+
this.keepTryingTheBridge();
|
|
35794
|
+
}
|
|
35795
|
+
}
|
|
35796
|
+
keepTryingTheBridge() {
|
|
35797
|
+
if (this.bridgeRetry !== null || this.shutdownInitiated) {
|
|
35798
|
+
return;
|
|
35799
|
+
}
|
|
35800
|
+
this.bridgeRetry = setInterval(() => {
|
|
35801
|
+
this.tryTheBridgeAgain();
|
|
35802
|
+
}, BRIDGE_RETRY_MS);
|
|
35803
|
+
this.bridgeRetry.unref();
|
|
35804
|
+
}
|
|
35805
|
+
async tryTheBridgeAgain() {
|
|
35806
|
+
if (this.shutdownInitiated) {
|
|
35807
|
+
this.stopTryingTheBridge();
|
|
35808
|
+
return;
|
|
35809
|
+
}
|
|
35810
|
+
try {
|
|
35811
|
+
await this.godotBridge.start();
|
|
35812
|
+
} catch {
|
|
35813
|
+
return;
|
|
35814
|
+
}
|
|
35815
|
+
this.bridgeStartupError = null;
|
|
35816
|
+
this.stopTryingTheBridge();
|
|
35817
|
+
const bridgeStatus = this.godotBridge.getStatus();
|
|
35818
|
+
console.error(`[SERVER] Godot Editor Bridge came up on ${bridgeStatus.host}:${bridgeStatus.port}; editor tools are live.`);
|
|
35819
|
+
}
|
|
35820
|
+
stopTryingTheBridge() {
|
|
35821
|
+
if (this.bridgeRetry !== null) {
|
|
35822
|
+
clearInterval(this.bridgeRetry);
|
|
35823
|
+
this.bridgeRetry = null;
|
|
35790
35824
|
}
|
|
35791
35825
|
}
|
|
35792
35826
|
async cleanup() {
|
|
35793
35827
|
this.logDebug("Cleaning up resources");
|
|
35828
|
+
this.stopTryingTheBridge();
|
|
35794
35829
|
if (this.activeProcess) {
|
|
35795
35830
|
this.activeProcess.process?.kill();
|
|
35796
35831
|
this.activeProcess = null;
|
|
@@ -36612,8 +36647,9 @@ class GodotServer {
|
|
|
36612
36647
|
bridgeAvailable: this.bridgeStartupError === null,
|
|
36613
36648
|
startupError: this.bridgeStartupError,
|
|
36614
36649
|
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
|
|
36615
|
-
|
|
36616
|
-
|
|
36650
|
+
retryingBridge: this.bridgeRetry === null ? undefined : true,
|
|
36651
|
+
note: isPortConflict ? "Bridge port is already in use. Another gdharness instance owns the editor bridge, so this server cannot reach the editor. Usually the server this one replaced, still on its way out." : undefined,
|
|
36652
|
+
suggestion: isPortConflict ? `This server is asking for the port again every ${BRIDGE_RETRY_MS / 1000}s and takes it the moment the other one lets go, so editor tools come back on their own. Ask again, or end the older gdharness process to have it now.` : undefined
|
|
36617
36653
|
};
|
|
36618
36654
|
}
|
|
36619
36655
|
async editorPlayingState() {
|
|
@@ -37091,7 +37127,7 @@ async function runGodotServer() {
|
|
|
37091
37127
|
const server = new GodotServer;
|
|
37092
37128
|
await server.run();
|
|
37093
37129
|
}
|
|
37094
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run5, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, HEADLESS_OPERATIONS, PROJECT_INFO_SECTIONS;
|
|
37130
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run5, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, BRIDGE_RETRY_MS = 2000, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, HEADLESS_OPERATIONS, PROJECT_INFO_SECTIONS;
|
|
37095
37131
|
var init_server3 = __esm(() => {
|
|
37096
37132
|
init_mcp();
|
|
37097
37133
|
init_stdio2();
|
|
@@ -37,6 +37,14 @@ var _server: TCPServer
|
|
|
37
37
|
var _clients: Array[StreamPeerTCP] = []
|
|
38
38
|
## Bytes received from each client that do not yet end in a newline, keyed by the peer.
|
|
39
39
|
var _pending: Dictionary = {}
|
|
40
|
+
## Bytes owed to each client that the socket has not taken yet, keyed by the peer.
|
|
41
|
+
##
|
|
42
|
+
## A reply is queued rather than written, because the write happens on the main thread and
|
|
43
|
+
## `put_data` blocks until every byte is gone. A client that asked a large question and then
|
|
44
|
+
## stopped reading, which is exactly what our own server does when a call times out, leaves that
|
|
45
|
+
## write with nowhere to go: the frame never ends, the game freezes, and the listener never
|
|
46
|
+
## accepts again, so every later request times out too. One timeout would take the game with it.
|
|
47
|
+
var _outgoing: Dictionary = {}
|
|
40
48
|
var _port: int = 0
|
|
41
49
|
var _enabled: bool = true
|
|
42
50
|
var _announcement: String = ""
|
|
@@ -102,10 +110,27 @@ func _process(_delta: float) -> void:
|
|
|
102
110
|
var received: Array = client.get_data(available)
|
|
103
111
|
var bytes: PackedByteArray = received[1]
|
|
104
112
|
_receive(client, bytes)
|
|
113
|
+
if not _drain(client):
|
|
114
|
+
gone.append(client)
|
|
105
115
|
|
|
106
116
|
for client: StreamPeerTCP in gone:
|
|
107
117
|
_clients.erase(client)
|
|
108
118
|
_pending.erase(client)
|
|
119
|
+
_outgoing.erase(client)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
## Hands the socket as much of what it is owed as it will take, and says whether it is still worth
|
|
123
|
+
## talking to. Reading comes first in the frame, so a client that asked and left is noticed here
|
|
124
|
+
## rather than blocking on a write that can never finish.
|
|
125
|
+
func _drain(client: StreamPeerTCP) -> bool:
|
|
126
|
+
var owed: PackedByteArray = _outgoing.get(client, PackedByteArray())
|
|
127
|
+
if owed.is_empty():
|
|
128
|
+
return true
|
|
129
|
+
var sent: Array = client.put_partial_data(owed)
|
|
130
|
+
if sent[0] != OK:
|
|
131
|
+
return false
|
|
132
|
+
_outgoing[client] = owed.slice(int(sent[1]))
|
|
133
|
+
return true
|
|
109
134
|
|
|
110
135
|
|
|
111
136
|
## Bytes arrive in whatever pieces the socket makes of them, so a request is only handled once
|
|
@@ -259,10 +284,15 @@ func _ping(_params: Dictionary) -> Dictionary:
|
|
|
259
284
|
return {"type": "pong", "timestamp": Time.get_unix_time_from_system()}
|
|
260
285
|
|
|
261
286
|
|
|
262
|
-
## One JSON object and a newline
|
|
263
|
-
##
|
|
287
|
+
## One JSON object and a newline, queued for [method _drain] rather than written here.
|
|
288
|
+
##
|
|
289
|
+
## Raw bytes rather than put_utf8_string, which would prefix them with a length the other side is
|
|
290
|
+
## not expecting, and queued rather than put_data, which blocks the frame until the socket has
|
|
291
|
+
## taken the lot: see [member _outgoing].
|
|
264
292
|
func _send_response(client: StreamPeerTCP, data: Dictionary) -> void:
|
|
265
|
-
|
|
293
|
+
var owed: PackedByteArray = _outgoing.get(client, PackedByteArray())
|
|
294
|
+
owed.append_array((JSON.stringify(data) + "\n").to_utf8_buffer())
|
|
295
|
+
_outgoing[client] = owed
|
|
266
296
|
|
|
267
297
|
|
|
268
298
|
## A request that could not be read far enough to find its id is answered with a null one.
|
|
@@ -280,6 +310,7 @@ func _cleanup() -> void:
|
|
|
280
310
|
client.disconnect_from_host()
|
|
281
311
|
_clients.clear()
|
|
282
312
|
_pending.clear()
|
|
313
|
+
_outgoing.clear()
|
|
283
314
|
|
|
284
315
|
if _server:
|
|
285
316
|
_server.stop()
|
package/build/index.js
CHANGED
|
@@ -25468,6 +25468,8 @@ class GodotBridge extends EventEmitter {
|
|
|
25468
25468
|
server.once("error", (error) => {
|
|
25469
25469
|
if (!settled) {
|
|
25470
25470
|
settled = true;
|
|
25471
|
+
server.close();
|
|
25472
|
+
godotWss.close();
|
|
25471
25473
|
reject(error);
|
|
25472
25474
|
return;
|
|
25473
25475
|
}
|
|
@@ -28405,6 +28407,7 @@ var FEEDBACK_NOTICE_EVERY = 250;
|
|
|
28405
28407
|
var run3 = promisify3(execFile3);
|
|
28406
28408
|
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
28407
28409
|
var EDITOR_RESTART_TIMEOUT_MS = 90000;
|
|
28410
|
+
var BRIDGE_RETRY_MS = 2000;
|
|
28408
28411
|
var PATH_SOLUTIONS = [
|
|
28409
28412
|
'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
|
|
28410
28413
|
"Point projectPath at the project the file belongs to"
|
|
@@ -28511,6 +28514,7 @@ class GodotServer {
|
|
|
28511
28514
|
lspClient = null;
|
|
28512
28515
|
dapClient = null;
|
|
28513
28516
|
bridgeStartupError = null;
|
|
28517
|
+
bridgeRetry = null;
|
|
28514
28518
|
lastProjectPath = null;
|
|
28515
28519
|
shutdownInitiated = false;
|
|
28516
28520
|
constructor() {
|
|
@@ -28567,11 +28571,43 @@ class GodotServer {
|
|
|
28567
28571
|
const reason = errorMessage(bridgeError);
|
|
28568
28572
|
this.bridgeStartupError = code && !reason.includes(code) ? `${code}: ${reason}` : reason;
|
|
28569
28573
|
console.error(`[SERVER] Warning: Godot Editor Bridge failed to start: ${this.bridgeStartupError}`);
|
|
28570
|
-
console.error("[SERVER] Continuing without bridge-backed editor tools.");
|
|
28574
|
+
console.error("[SERVER] Continuing without bridge-backed editor tools, and trying again.");
|
|
28575
|
+
this.keepTryingTheBridge();
|
|
28576
|
+
}
|
|
28577
|
+
}
|
|
28578
|
+
keepTryingTheBridge() {
|
|
28579
|
+
if (this.bridgeRetry !== null || this.shutdownInitiated) {
|
|
28580
|
+
return;
|
|
28581
|
+
}
|
|
28582
|
+
this.bridgeRetry = setInterval(() => {
|
|
28583
|
+
this.tryTheBridgeAgain();
|
|
28584
|
+
}, BRIDGE_RETRY_MS);
|
|
28585
|
+
this.bridgeRetry.unref();
|
|
28586
|
+
}
|
|
28587
|
+
async tryTheBridgeAgain() {
|
|
28588
|
+
if (this.shutdownInitiated) {
|
|
28589
|
+
this.stopTryingTheBridge();
|
|
28590
|
+
return;
|
|
28591
|
+
}
|
|
28592
|
+
try {
|
|
28593
|
+
await this.godotBridge.start();
|
|
28594
|
+
} catch {
|
|
28595
|
+
return;
|
|
28596
|
+
}
|
|
28597
|
+
this.bridgeStartupError = null;
|
|
28598
|
+
this.stopTryingTheBridge();
|
|
28599
|
+
const bridgeStatus = this.godotBridge.getStatus();
|
|
28600
|
+
console.error(`[SERVER] Godot Editor Bridge came up on ${bridgeStatus.host}:${bridgeStatus.port}; editor tools are live.`);
|
|
28601
|
+
}
|
|
28602
|
+
stopTryingTheBridge() {
|
|
28603
|
+
if (this.bridgeRetry !== null) {
|
|
28604
|
+
clearInterval(this.bridgeRetry);
|
|
28605
|
+
this.bridgeRetry = null;
|
|
28571
28606
|
}
|
|
28572
28607
|
}
|
|
28573
28608
|
async cleanup() {
|
|
28574
28609
|
this.logDebug("Cleaning up resources");
|
|
28610
|
+
this.stopTryingTheBridge();
|
|
28575
28611
|
if (this.activeProcess) {
|
|
28576
28612
|
this.activeProcess.process?.kill();
|
|
28577
28613
|
this.activeProcess = null;
|
|
@@ -29393,8 +29429,9 @@ class GodotServer {
|
|
|
29393
29429
|
bridgeAvailable: this.bridgeStartupError === null,
|
|
29394
29430
|
startupError: this.bridgeStartupError,
|
|
29395
29431
|
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
|
|
29396
|
-
|
|
29397
|
-
|
|
29432
|
+
retryingBridge: this.bridgeRetry === null ? undefined : true,
|
|
29433
|
+
note: isPortConflict ? "Bridge port is already in use. Another gdharness instance owns the editor bridge, so this server cannot reach the editor. Usually the server this one replaced, still on its way out." : undefined,
|
|
29434
|
+
suggestion: isPortConflict ? `This server is asking for the port again every ${BRIDGE_RETRY_MS / 1000}s and takes it the moment the other one lets go, so editor tools come back on their own. Ask again, or end the older gdharness process to have it now.` : undefined
|
|
29398
29435
|
};
|
|
29399
29436
|
}
|
|
29400
29437
|
async editorPlayingState() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.7",
|
|
4
4
|
"mcpName": "io.github.Aureliolo/gdharness",
|
|
5
5
|
"description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
|
|
6
6
|
"type": "module",
|