gdharness 1.0.2 → 1.0.4
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 +1000 -782
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +15 -2
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +83 -17
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +65 -37
- package/build/index.js +3757 -3541
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -16015,7 +16015,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16015
16015
|
runtimeWaitMs: {
|
|
16016
16016
|
type: "number",
|
|
16017
16017
|
ops: ["start"],
|
|
16018
|
-
description: "start: how long to wait for the game to announce its runtime before answering. Default 5000. What it waits for is the first frame, so everything the game does before drawing one is inside it, including work the args just asked for: a flag that simulates six years of game time before anything is drawn makes the announcement that late, on a project that announces promptly without it. Raise it for such a run rather than reading runtime listening false as a fault. 0 does not wait at all, which is the one to pass for a scene that announces nothing by construction, such as a bench that prints and quits."
|
|
16018
|
+
description: "start: how long to wait for the game to announce its runtime before answering. Default 5000, or half as long again as the last boot of this project took when that is more, up to 60000: the server notes how long each game took to announce, so a project that boots slowly is waited for from its second start on without being asked to. What it waits for is the first frame, so everything the game does before drawing one is inside it, including work the args just asked for: a flag that simulates six years of game time before anything is drawn makes the announcement that late, on a project that announces promptly without it. Raise it for such a run rather than reading runtime listening false as a fault. 0 does not wait at all, which is the one to pass for a scene that announces nothing by construction, such as a bench that prints and quits."
|
|
16019
16019
|
},
|
|
16020
16020
|
frames: {
|
|
16021
16021
|
type: "number",
|
|
@@ -16040,7 +16040,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16040
16040
|
requires: ["projectPath"]
|
|
16041
16041
|
},
|
|
16042
16042
|
stop: {
|
|
16043
|
-
summary: "end the run and answer with what it printed last, naming the process ended under endedPid: a game that started processes of its own keeps them unless andChildren says to end those too",
|
|
16043
|
+
summary: "end the run and answer with what it printed last, naming the process ended under endedPid: 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. A game that started processes of its own keeps them unless andChildren says to end those too",
|
|
16044
16044
|
requires: []
|
|
16045
16045
|
},
|
|
16046
16046
|
check: {
|
|
@@ -16155,7 +16155,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16155
16155
|
property: {
|
|
16156
16156
|
type: "string",
|
|
16157
16157
|
ops: ["property", "find"],
|
|
16158
|
-
description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A negative number counts from the end. A step that is not there is named, and says what was there instead.'
|
|
16158
|
+
description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that takes no arguments and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. A step that is not there is named, and says what was there instead.'
|
|
16159
16159
|
},
|
|
16160
16160
|
depth: { type: "number", ops: ["tree"], description: "tree: levels to descend. Default 3." },
|
|
16161
16161
|
includeProperties: {
|
|
@@ -16236,7 +16236,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16236
16236
|
property: {
|
|
16237
16237
|
type: "string",
|
|
16238
16238
|
ops: ["set"],
|
|
16239
|
-
description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end. The answer reads back off the same holder, so a write a typed container refused shows as an unchanged value.'
|
|
16239
|
+
description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end, and a step written as a call, "get_viewport():gui_embed_subwindows", walks through what a method taking no arguments returned. The answer reads back off the same holder, so a write a typed container refused shows as an unchanged value.'
|
|
16240
16240
|
},
|
|
16241
16241
|
value: {
|
|
16242
16242
|
ops: ["set"],
|
|
@@ -16246,7 +16246,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16246
16246
|
method: {
|
|
16247
16247
|
type: "string",
|
|
16248
16248
|
ops: ["call"],
|
|
16249
|
-
description: 'call: which one to call. Colons call through what a node holds, "_game:run:advance", the same way a property is written through them,
|
|
16249
|
+
description: 'call: which one to call. Colons call through what a node holds, "_game:run:advance", the same way a property is written through them, a list or a map is stepped into by index or key, "_game:run:roster:0:retire", and a step written as a call walks through what a method taking no arguments returned, "get_viewport():gui_get_focus_owner".'
|
|
16250
16250
|
},
|
|
16251
16251
|
args: {
|
|
16252
16252
|
type: "array",
|
|
@@ -16306,7 +16306,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16306
16306
|
pressed: {
|
|
16307
16307
|
type: "boolean",
|
|
16308
16308
|
ops: ["action", "key", "mouse_click"],
|
|
16309
|
-
description: "action, key: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. mouse_click is one raw event, so it is down unless you say false."
|
|
16309
|
+
description: "action, key: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. mouse_click is one raw event, so it is down unless you say false; a wheel button down is a whole step, since a wheel is never held."
|
|
16310
16310
|
},
|
|
16311
16311
|
strength: { type: "number", ops: ["action"], description: "action: 0 to 1. Default 1." },
|
|
16312
16312
|
keycode: {
|
|
@@ -16384,7 +16384,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16384
16384
|
requires: ["text"]
|
|
16385
16385
|
},
|
|
16386
16386
|
mouse_click: {
|
|
16387
|
-
summary: "one mouse button event at a position, carrying the buttons held once it has happened. A drag is a press held with pressed true, motions, and a release with pressed false",
|
|
16387
|
+
summary: "one mouse button event at a position, carrying the buttons held once it has happened. A drag is a press held with pressed true, motions, and a release with pressed false. A wheel button is a whole step, press and release together as a mouse sends one, and the answer says released: a lone wheel press holds the viewport on the control that took it, so every click after it lands there",
|
|
16388
16388
|
requires: ["x", "y"]
|
|
16389
16389
|
},
|
|
16390
16390
|
mouse_motion: {
|
|
@@ -16406,7 +16406,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
16406
16406
|
},
|
|
16407
16407
|
nodePath: { type: "string", ops: ["signal", "until"], description: "signal, until: the node." },
|
|
16408
16408
|
signal: { type: "string", ops: ["signal"], description: "signal: the signal name." },
|
|
16409
|
-
property: {
|
|
16409
|
+
property: {
|
|
16410
|
+
type: "string",
|
|
16411
|
+
ops: ["until"],
|
|
16412
|
+
description: 'until: the property name, or a colon path through what the node holds, "_game:run:day", read the way runtime_inspect property reads one, a step written as a call included. Walked again on every frame, so a holder the game replaces while the wait is on is followed.'
|
|
16413
|
+
},
|
|
16410
16414
|
value: { ops: ["until"], description: "until: the value to wait for, fitted to the property's type." },
|
|
16411
16415
|
says: {
|
|
16412
16416
|
type: "string",
|
|
@@ -31236,155 +31240,782 @@ var init_stdio2 = __esm(() => {
|
|
|
31236
31240
|
init_stdio();
|
|
31237
31241
|
});
|
|
31238
31242
|
|
|
31239
|
-
// src/
|
|
31240
|
-
import {
|
|
31241
|
-
import {
|
|
31242
|
-
|
|
31243
|
-
|
|
31243
|
+
// src/process-children.ts
|
|
31244
|
+
import { execFile as execFile4, execFileSync, spawnSync as spawnSync2 } from "node:child_process";
|
|
31245
|
+
import { basename as basename2 } from "node:path";
|
|
31246
|
+
import { promisify as promisify4 } from "node:util";
|
|
31247
|
+
function startTimesOf(pids) {
|
|
31248
|
+
const began = new Map;
|
|
31249
|
+
if (pids.length === 0) {
|
|
31250
|
+
return began;
|
|
31251
|
+
}
|
|
31252
|
+
try {
|
|
31253
|
+
if (process.platform === "win32") {
|
|
31254
|
+
const filter = pids.map((pid) => `ProcessId=${pid}`).join(" OR ");
|
|
31255
|
+
const said = execFileSync("powershell", [
|
|
31256
|
+
"-NoProfile",
|
|
31257
|
+
"-NonInteractive",
|
|
31258
|
+
"-Command",
|
|
31259
|
+
`Get-CimInstance Win32_Process -Filter "${filter}" | ForEach-Object { "$($_.ProcessId) $(([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds())" }`
|
|
31260
|
+
], { encoding: "utf8", timeout: ASK_TIMEOUT_MS, windowsHide: true });
|
|
31261
|
+
for (const line of said.split(`
|
|
31262
|
+
`)) {
|
|
31263
|
+
const found = /^\s*(\d+)\s+(\d+)\s*$/.exec(line);
|
|
31264
|
+
if (found?.[1] !== undefined && found[2] !== undefined) {
|
|
31265
|
+
began.set(Number(found[1]), Number(found[2]));
|
|
31266
|
+
}
|
|
31267
|
+
}
|
|
31268
|
+
return began;
|
|
31269
|
+
}
|
|
31270
|
+
const said = spawnSync2("ps", ["-p", pids.join(","), "-o", "pid=,lstart="], {
|
|
31271
|
+
encoding: "utf8",
|
|
31272
|
+
timeout: ASK_TIMEOUT_MS,
|
|
31273
|
+
env: { ...process.env, LC_ALL: "C" }
|
|
31274
|
+
}).stdout;
|
|
31275
|
+
for (const line of said.split(`
|
|
31276
|
+
`)) {
|
|
31277
|
+
const found = /^\s*(\d+)\s+(.+)$/.exec(line);
|
|
31278
|
+
const at = Date.parse(found?.[2]?.trim() ?? "");
|
|
31279
|
+
if (found?.[1] !== undefined && !Number.isNaN(at)) {
|
|
31280
|
+
began.set(Number(found[1]), at);
|
|
31281
|
+
}
|
|
31282
|
+
}
|
|
31283
|
+
} catch {}
|
|
31284
|
+
return began;
|
|
31244
31285
|
}
|
|
31245
|
-
function
|
|
31246
|
-
const path = breakpointNotePath(projectPath);
|
|
31247
|
-
const root = realPathOr(projectPath);
|
|
31248
|
-
const inside = held.map((one) => ({ scriptPath: realPathOr(one.scriptPath), lines: one.lines })).filter((one) => one.lines.length > 0 && withinProject(root, one.scriptPath));
|
|
31286
|
+
async function processTree() {
|
|
31249
31287
|
try {
|
|
31250
|
-
if (
|
|
31251
|
-
|
|
31252
|
-
return;
|
|
31288
|
+
if (process.platform === "win32") {
|
|
31289
|
+
return parseProcessTable(await askWindows(), true);
|
|
31253
31290
|
}
|
|
31254
|
-
|
|
31255
|
-
|
|
31256
|
-
|
|
31257
|
-
lines: [...one.lines]
|
|
31258
|
-
}))
|
|
31259
|
-
};
|
|
31260
|
-
mkdirSync6(dirname7(path), { recursive: true });
|
|
31261
|
-
writeFileSync8(path, `${JSON.stringify(note, null, 2)}
|
|
31262
|
-
`, "utf8");
|
|
31263
|
-
} catch (error) {
|
|
31264
|
-
console.error(`[SERVER] Could not record the breakpoints at ${path}: ${errorMessage(error)}`);
|
|
31291
|
+
return parseProcessTable(await askPosix());
|
|
31292
|
+
} catch {
|
|
31293
|
+
return;
|
|
31265
31294
|
}
|
|
31266
31295
|
}
|
|
31267
|
-
function
|
|
31268
|
-
|
|
31269
|
-
|
|
31270
|
-
|
|
31271
|
-
|
|
31296
|
+
function parseProcessTable(printed, started = false) {
|
|
31297
|
+
const tree = new Map;
|
|
31298
|
+
for (const line of printed.split(/\r?\n/)) {
|
|
31299
|
+
const fields = /^\s*(\d+)\s+(\d+)(?:\s+(.*))?$/.exec(line);
|
|
31300
|
+
if (fields === null) {
|
|
31301
|
+
continue;
|
|
31272
31302
|
}
|
|
31273
|
-
const
|
|
31274
|
-
|
|
31275
|
-
|
|
31276
|
-
|
|
31277
|
-
|
|
31278
|
-
const { script, lines: listed } = entry;
|
|
31279
|
-
if (typeof script !== "string" || isAbsolute2(script) || script.split("/").includes("..")) {
|
|
31280
|
-
continue;
|
|
31281
|
-
}
|
|
31282
|
-
const lines = Array.isArray(listed) ? listed.filter((line) => Number.isInteger(line) && line > 0) : [];
|
|
31283
|
-
if (lines.length > 0) {
|
|
31284
|
-
held.push({ scriptPath: realPathOr(join12(projectPath, script)), lines });
|
|
31285
|
-
}
|
|
31303
|
+
const pid = Number(fields[1]);
|
|
31304
|
+
const parent = Number(fields[2]);
|
|
31305
|
+
const rest = (fields[3] ?? "").trim();
|
|
31306
|
+
if (pid <= 0) {
|
|
31307
|
+
continue;
|
|
31286
31308
|
}
|
|
31287
|
-
|
|
31288
|
-
|
|
31289
|
-
|
|
31309
|
+
if (!started) {
|
|
31310
|
+
tree.set(pid, { parent, command: rest });
|
|
31311
|
+
continue;
|
|
31312
|
+
}
|
|
31313
|
+
const timed = /^(\d+)(?:\s+(.*))?$/.exec(rest);
|
|
31314
|
+
if (timed === null) {
|
|
31315
|
+
continue;
|
|
31316
|
+
}
|
|
31317
|
+
const began = Number(timed[1]);
|
|
31318
|
+
tree.set(pid, { parent, command: (timed[2] ?? "").trim(), ...began > 0 ? { startedAt: began } : {} });
|
|
31290
31319
|
}
|
|
31320
|
+
return tree;
|
|
31291
31321
|
}
|
|
31292
|
-
function
|
|
31293
|
-
const
|
|
31294
|
-
|
|
31322
|
+
function linked(tree, child, parent) {
|
|
31323
|
+
const from = tree.get(child);
|
|
31324
|
+
const to = tree.get(parent);
|
|
31325
|
+
if (from === undefined || from.parent !== parent) {
|
|
31326
|
+
return false;
|
|
31327
|
+
}
|
|
31328
|
+
if (from.startedAt === undefined || to?.startedAt === undefined) {
|
|
31329
|
+
return true;
|
|
31330
|
+
}
|
|
31331
|
+
return from.startedAt >= to.startedAt;
|
|
31295
31332
|
}
|
|
31296
|
-
|
|
31297
|
-
|
|
31298
|
-
|
|
31299
|
-
|
|
31300
|
-
|
|
31301
|
-
|
|
31302
|
-
|
|
31303
|
-
|
|
31304
|
-
function announcementPath(projectPath) {
|
|
31305
|
-
return join13(projectPath, ".godot", "gdharness-bridge.json");
|
|
31333
|
+
function childrenIn(tree, pid) {
|
|
31334
|
+
const children = [];
|
|
31335
|
+
for (const child of tree.keys()) {
|
|
31336
|
+
if (linked(tree, child, pid)) {
|
|
31337
|
+
children.push(child);
|
|
31338
|
+
}
|
|
31339
|
+
}
|
|
31340
|
+
return children;
|
|
31306
31341
|
}
|
|
31307
|
-
function
|
|
31308
|
-
|
|
31309
|
-
|
|
31310
|
-
|
|
31311
|
-
|
|
31342
|
+
function descendantsIn(tree, pid) {
|
|
31343
|
+
const found = [];
|
|
31344
|
+
const queue = [pid];
|
|
31345
|
+
const seen = new Set([pid]);
|
|
31346
|
+
while (queue.length > 0) {
|
|
31347
|
+
const next = queue.shift();
|
|
31348
|
+
if (next === undefined) {
|
|
31349
|
+
break;
|
|
31312
31350
|
}
|
|
31313
|
-
const
|
|
31314
|
-
|
|
31315
|
-
|
|
31351
|
+
for (const child of childrenIn(tree, next)) {
|
|
31352
|
+
if (!seen.has(child)) {
|
|
31353
|
+
seen.add(child);
|
|
31354
|
+
found.push(child);
|
|
31355
|
+
queue.push(child);
|
|
31356
|
+
}
|
|
31316
31357
|
}
|
|
31317
|
-
return said;
|
|
31318
|
-
} catch {
|
|
31319
|
-
return null;
|
|
31320
31358
|
}
|
|
31359
|
+
return found;
|
|
31321
31360
|
}
|
|
31322
|
-
function
|
|
31323
|
-
|
|
31361
|
+
function ancestorsIn(tree, pid, root) {
|
|
31362
|
+
const chain = [];
|
|
31363
|
+
const seen = new Set([pid]);
|
|
31364
|
+
let child = pid;
|
|
31365
|
+
let parent = tree.get(pid)?.parent;
|
|
31366
|
+
while (parent !== undefined && !seen.has(parent) && linked(tree, child, parent)) {
|
|
31367
|
+
chain.push(parent);
|
|
31368
|
+
if (parent === root) {
|
|
31369
|
+
break;
|
|
31370
|
+
}
|
|
31371
|
+
seen.add(parent);
|
|
31372
|
+
child = parent;
|
|
31373
|
+
parent = tree.get(parent)?.parent;
|
|
31374
|
+
}
|
|
31375
|
+
return chain;
|
|
31324
31376
|
}
|
|
31325
|
-
function
|
|
31326
|
-
const
|
|
31327
|
-
|
|
31328
|
-
|
|
31329
|
-
|
|
31330
|
-
|
|
31331
|
-
|
|
31332
|
-
|
|
31333
|
-
|
|
31334
|
-
|
|
31335
|
-
|
|
31336
|
-
|
|
31337
|
-
|
|
31338
|
-
|
|
31339
|
-
return path;
|
|
31340
|
-
} catch (error) {
|
|
31341
|
-
console.error(`[SERVER] Could not announce the editor bridge at ${path}: ${errorMessage(error)}`);
|
|
31342
|
-
return null;
|
|
31377
|
+
function readCommandLine(command) {
|
|
31378
|
+
const words = commandWords(command);
|
|
31379
|
+
const executable = words[0] === undefined ? "" : basename2(words[0].replaceAll("\\", "/"));
|
|
31380
|
+
const at = words.indexOf("--path");
|
|
31381
|
+
let projectPath = null;
|
|
31382
|
+
if (at !== -1 && at + 1 < words.length) {
|
|
31383
|
+
const taken = [];
|
|
31384
|
+
for (const word of words.slice(at + 1)) {
|
|
31385
|
+
if (taken.length > 0 && (word.startsWith("-") || /^[a-z]+:\/\//.test(word))) {
|
|
31386
|
+
break;
|
|
31387
|
+
}
|
|
31388
|
+
taken.push(word);
|
|
31389
|
+
}
|
|
31390
|
+
projectPath = taken.join(" ");
|
|
31343
31391
|
}
|
|
31392
|
+
return {
|
|
31393
|
+
executable,
|
|
31394
|
+
projectPath,
|
|
31395
|
+
editor: words.some((word) => word === "-e" || word === "--editor")
|
|
31396
|
+
};
|
|
31344
31397
|
}
|
|
31345
|
-
function
|
|
31346
|
-
|
|
31347
|
-
|
|
31398
|
+
function commandWords(command) {
|
|
31399
|
+
const words = [];
|
|
31400
|
+
let word = "";
|
|
31401
|
+
let quoted = false;
|
|
31402
|
+
let started = false;
|
|
31403
|
+
for (const char of command) {
|
|
31404
|
+
if (char === '"') {
|
|
31405
|
+
quoted = !quoted;
|
|
31406
|
+
started = true;
|
|
31407
|
+
} else if (!quoted && /\s/.test(char)) {
|
|
31408
|
+
if (started) {
|
|
31409
|
+
words.push(word);
|
|
31410
|
+
word = "";
|
|
31411
|
+
started = false;
|
|
31412
|
+
}
|
|
31413
|
+
} else {
|
|
31414
|
+
word += char;
|
|
31415
|
+
started = true;
|
|
31416
|
+
}
|
|
31348
31417
|
}
|
|
31349
|
-
|
|
31350
|
-
|
|
31351
|
-
} catch (error) {
|
|
31352
|
-
console.error(`[SERVER] Could not withdraw the editor bridge announcement: ${errorMessage(error)}`);
|
|
31418
|
+
if (started) {
|
|
31419
|
+
words.push(word);
|
|
31353
31420
|
}
|
|
31421
|
+
return words;
|
|
31354
31422
|
}
|
|
31355
|
-
|
|
31356
|
-
|
|
31357
|
-
|
|
31423
|
+
async function askWindows() {
|
|
31424
|
+
const { stdout } = await run4("powershell.exe", [
|
|
31425
|
+
"-NoProfile",
|
|
31426
|
+
"-NonInteractive",
|
|
31427
|
+
"-Command",
|
|
31428
|
+
'Get-CimInstance Win32_Process | ForEach-Object { $began = 0; if ($_.CreationDate) { $began = ([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds() }; "$($_.ProcessId) $($_.ParentProcessId) $began $($_.CommandLine)" }'
|
|
31429
|
+
], { timeout: ASK_TIMEOUT_MS, windowsHide: true, maxBuffer: 16 * 1024 * 1024 });
|
|
31430
|
+
return stdout;
|
|
31431
|
+
}
|
|
31432
|
+
async function askPosix() {
|
|
31433
|
+
const { stdout } = await run4("ps", ["-e", "-o", "pid=,ppid=,args="], {
|
|
31434
|
+
timeout: ASK_TIMEOUT_MS,
|
|
31435
|
+
maxBuffer: 16 * 1024 * 1024
|
|
31436
|
+
});
|
|
31437
|
+
return stdout;
|
|
31438
|
+
}
|
|
31439
|
+
var run4, ASK_TIMEOUT_MS = 15000;
|
|
31440
|
+
var init_process_children = __esm(() => {
|
|
31441
|
+
run4 = promisify4(execFile4);
|
|
31358
31442
|
});
|
|
31359
31443
|
|
|
31360
|
-
// src/
|
|
31361
|
-
import { existsSync as existsSync10, readFileSync as
|
|
31362
|
-
import {
|
|
31363
|
-
|
|
31364
|
-
|
|
31365
|
-
|
|
31366
|
-
|
|
31444
|
+
// src/runtime-client.ts
|
|
31445
|
+
import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync10, statSync as statSync3, unlinkSync } from "node:fs";
|
|
31446
|
+
import { createConnection } from "node:net";
|
|
31447
|
+
import { tmpdir as tmpdir4 } from "node:os";
|
|
31448
|
+
import { dirname as dirname7, join as join12, resolve as resolve3 } from "node:path";
|
|
31449
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
31450
|
+
function runtimeDirectory(variables = process.env) {
|
|
31451
|
+
const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
|
|
31452
|
+
if (explicit) {
|
|
31453
|
+
return explicit;
|
|
31367
31454
|
}
|
|
31368
|
-
|
|
31369
|
-
|
|
31370
|
-
|
|
31371
|
-
|
|
31372
|
-
|
|
31373
|
-
|
|
31455
|
+
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
31456
|
+
return join12(perUser ?? tmpdir4(), "gdharness");
|
|
31457
|
+
}
|
|
31458
|
+
function runtimeDirectories(variables = process.env) {
|
|
31459
|
+
const candidates = [runtimeDirectory(variables)];
|
|
31460
|
+
const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
|
|
31461
|
+
for (const base of fallbacks) {
|
|
31462
|
+
candidates.push(join12(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
|
|
31374
31463
|
}
|
|
31375
|
-
return [...new Set(
|
|
31464
|
+
return [...new Set(candidates.map((path) => resolve3(path)))];
|
|
31376
31465
|
}
|
|
31377
|
-
function
|
|
31378
|
-
const
|
|
31379
|
-
|
|
31380
|
-
|
|
31381
|
-
|
|
31466
|
+
function errorReportOf(pid, directories = runtimeDirectories()) {
|
|
31467
|
+
for (const directory of directories) {
|
|
31468
|
+
const path = join12(directory, `runtime-${pid}.log`);
|
|
31469
|
+
if (existsSync10(path)) {
|
|
31470
|
+
return path;
|
|
31471
|
+
}
|
|
31382
31472
|
}
|
|
31383
|
-
return
|
|
31473
|
+
return null;
|
|
31384
31474
|
}
|
|
31385
|
-
|
|
31386
|
-
|
|
31387
|
-
|
|
31475
|
+
function processAlive(pid) {
|
|
31476
|
+
try {
|
|
31477
|
+
process.kill(pid, 0);
|
|
31478
|
+
return true;
|
|
31479
|
+
} catch (error) {
|
|
31480
|
+
return error.code === "EPERM";
|
|
31481
|
+
}
|
|
31482
|
+
}
|
|
31483
|
+
function parseAnnouncement(file, pid) {
|
|
31484
|
+
let fields;
|
|
31485
|
+
try {
|
|
31486
|
+
fields = asParams(JSON.parse(readFileSync10(file, "utf8")));
|
|
31487
|
+
} catch {
|
|
31488
|
+
return { kind: "rubbish" };
|
|
31489
|
+
}
|
|
31490
|
+
const project = readParams(fields, "project");
|
|
31491
|
+
const port = readNumber(fields, "port");
|
|
31492
|
+
const address = readString(fields, "address");
|
|
31493
|
+
const protocol = readNumber(fields, "protocol");
|
|
31494
|
+
if (readNumber(fields, "pid") !== pid || port === undefined || !Number.isInteger(port) || port < 1 || port > 65535 || address === undefined || project === undefined) {
|
|
31495
|
+
return { kind: "rubbish" };
|
|
31496
|
+
}
|
|
31497
|
+
const named = {
|
|
31498
|
+
name: readString(project, "name") ?? "",
|
|
31499
|
+
path: readString(project, "path") ?? ""
|
|
31500
|
+
};
|
|
31501
|
+
if (protocol !== RUNTIME_PROTOCOL) {
|
|
31502
|
+
return { kind: "unspoken", unspoken: { pid, protocol: protocol ?? 0, project: named } };
|
|
31503
|
+
}
|
|
31504
|
+
const editorPid = readNumber(fields, "editor_pid");
|
|
31505
|
+
return {
|
|
31506
|
+
kind: "runtime",
|
|
31507
|
+
endpoint: {
|
|
31508
|
+
pid,
|
|
31509
|
+
port,
|
|
31510
|
+
address,
|
|
31511
|
+
project: named,
|
|
31512
|
+
file,
|
|
31513
|
+
...editorPid !== undefined && Number.isInteger(editorPid) && editorPid > 0 ? { editorPid } : {}
|
|
31514
|
+
}
|
|
31515
|
+
};
|
|
31516
|
+
}
|
|
31517
|
+
function runtimesAnnounced(directories = runtimeDirectories()) {
|
|
31518
|
+
const running = new Map;
|
|
31519
|
+
const unspoken = new Map;
|
|
31520
|
+
for (const directory of directories) {
|
|
31521
|
+
for (const found of announcedIn(directory)) {
|
|
31522
|
+
if (found.kind === "runtime" && !running.has(found.endpoint.pid)) {
|
|
31523
|
+
running.set(found.endpoint.pid, found.endpoint);
|
|
31524
|
+
} else if (found.kind === "unspoken" && !unspoken.has(found.unspoken.pid)) {
|
|
31525
|
+
unspoken.set(found.unspoken.pid, found.unspoken);
|
|
31526
|
+
}
|
|
31527
|
+
}
|
|
31528
|
+
}
|
|
31529
|
+
const newest = (a, b) => b.pid - a.pid;
|
|
31530
|
+
return { running: [...running.values()].sort(newest), unspoken: [...unspoken.values()].sort(newest) };
|
|
31531
|
+
}
|
|
31532
|
+
function discoverRuntimes(directories = runtimeDirectories()) {
|
|
31533
|
+
return runtimesAnnounced(directories).running;
|
|
31534
|
+
}
|
|
31535
|
+
function wentAway(file, pid) {
|
|
31536
|
+
let project = "";
|
|
31537
|
+
try {
|
|
31538
|
+
const fields = asParams(JSON.parse(readFileSync10(file, "utf8")));
|
|
31539
|
+
project = readString(readParams(fields, "project") ?? {}, "path") ?? "";
|
|
31540
|
+
} catch {
|
|
31541
|
+
return;
|
|
31542
|
+
}
|
|
31543
|
+
if (project === "") {
|
|
31544
|
+
return;
|
|
31545
|
+
}
|
|
31546
|
+
gone.unshift({ pid, project, noticedAt: Date.now() });
|
|
31547
|
+
gone.length = Math.min(gone.length, GONE_KEPT);
|
|
31548
|
+
}
|
|
31549
|
+
function announcedAndGone(projectPath) {
|
|
31550
|
+
if (projectPath === undefined) {
|
|
31551
|
+
return [];
|
|
31552
|
+
}
|
|
31553
|
+
const since = Date.now() - GONE_WINDOW_MS;
|
|
31554
|
+
const wanted = resolve3(projectPath);
|
|
31555
|
+
return gone.filter((one) => one.noticedAt >= since && resolve3(one.project) === wanted);
|
|
31556
|
+
}
|
|
31557
|
+
function strangersAmong(candidates, now, startTimes = startTimesOf) {
|
|
31558
|
+
const toJudge = candidates.filter((one) => {
|
|
31559
|
+
if (now - one.writtenAt < JUDGED_AFTER_MS) {
|
|
31560
|
+
return false;
|
|
31561
|
+
}
|
|
31562
|
+
const known = verdicts.get(one.file);
|
|
31563
|
+
return known === undefined || known.writtenAt !== one.writtenAt || !known.stranger && now - known.judgedAt > CONFIRMED_FOR_MS;
|
|
31564
|
+
});
|
|
31565
|
+
if (toJudge.length > 0) {
|
|
31566
|
+
const began = startTimes(toJudge.map((one) => one.pid));
|
|
31567
|
+
for (const one of toJudge) {
|
|
31568
|
+
const at = began.get(one.pid);
|
|
31569
|
+
const stranger = at !== undefined && at - one.writtenAt > STARTED_AFTER_ANNOUNCING_MS;
|
|
31570
|
+
verdicts.set(one.file, { writtenAt: one.writtenAt, judgedAt: now, stranger });
|
|
31571
|
+
}
|
|
31572
|
+
}
|
|
31573
|
+
return new Set(candidates.filter((one) => verdicts.get(one.file)?.stranger === true).map((one) => one.file));
|
|
31574
|
+
}
|
|
31575
|
+
function announcedIn(directory) {
|
|
31576
|
+
if (!existsSync10(directory)) {
|
|
31577
|
+
return [];
|
|
31578
|
+
}
|
|
31579
|
+
const found = [];
|
|
31580
|
+
const candidates = [];
|
|
31581
|
+
const entries = [];
|
|
31582
|
+
for (const entry of readdirSync4(directory)) {
|
|
31583
|
+
const report = ERROR_REPORT_PATTERN.exec(entry);
|
|
31584
|
+
if (report) {
|
|
31585
|
+
sweepErrorReport(join12(directory, entry), Number.parseInt(report[1] ?? "", 10));
|
|
31586
|
+
continue;
|
|
31587
|
+
}
|
|
31588
|
+
const match = ANNOUNCEMENT_PATTERN.exec(entry);
|
|
31589
|
+
if (!match) {
|
|
31590
|
+
continue;
|
|
31591
|
+
}
|
|
31592
|
+
const file = join12(directory, entry);
|
|
31593
|
+
const pid = Number.parseInt(match[1] ?? "", 10);
|
|
31594
|
+
const alive = processAlive(pid);
|
|
31595
|
+
entries.push({ file, pid, alive });
|
|
31596
|
+
if (alive) {
|
|
31597
|
+
try {
|
|
31598
|
+
candidates.push({ file, pid, writtenAt: statSync3(file).mtimeMs });
|
|
31599
|
+
} catch {}
|
|
31600
|
+
}
|
|
31601
|
+
}
|
|
31602
|
+
const strangers = strangersAmong(candidates, Date.now());
|
|
31603
|
+
for (const { file, pid, alive } of entries) {
|
|
31604
|
+
const announced = alive && !strangers.has(file) ? parseAnnouncement(file, pid) : { kind: "rubbish" };
|
|
31605
|
+
if (announced.kind === "rubbish") {
|
|
31606
|
+
if (!alive || strangers.has(file)) {
|
|
31607
|
+
wentAway(file, pid);
|
|
31608
|
+
}
|
|
31609
|
+
try {
|
|
31610
|
+
unlinkSync(file);
|
|
31611
|
+
} catch {}
|
|
31612
|
+
verdicts.delete(file);
|
|
31613
|
+
continue;
|
|
31614
|
+
}
|
|
31615
|
+
found.push(announced);
|
|
31616
|
+
}
|
|
31617
|
+
const held = new Set(entries.map((one) => one.file));
|
|
31618
|
+
const here = join12(directory, ".");
|
|
31619
|
+
for (const file of verdicts.keys()) {
|
|
31620
|
+
if (dirname7(file) === here && !held.has(file)) {
|
|
31621
|
+
verdicts.delete(file);
|
|
31622
|
+
}
|
|
31623
|
+
}
|
|
31624
|
+
return found;
|
|
31625
|
+
}
|
|
31626
|
+
async function announcementEnded(pid, directories = runtimeDirectories()) {
|
|
31627
|
+
const files = directories.map((directory) => join12(directory, `runtime-${pid}.json`)).filter((file) => existsSync10(file));
|
|
31628
|
+
const first = files[0];
|
|
31629
|
+
if (first === undefined) {
|
|
31630
|
+
return;
|
|
31631
|
+
}
|
|
31632
|
+
const until = Date.now() + ENDED_WITHIN_MS;
|
|
31633
|
+
while (processAlive(pid)) {
|
|
31634
|
+
const left = until - Date.now();
|
|
31635
|
+
if (left <= 0) {
|
|
31636
|
+
return;
|
|
31637
|
+
}
|
|
31638
|
+
await delay(Math.min(LOOK_EVERY_MS, left));
|
|
31639
|
+
}
|
|
31640
|
+
wentAway(first, pid);
|
|
31641
|
+
for (const file of files) {
|
|
31642
|
+
try {
|
|
31643
|
+
unlinkSync(file);
|
|
31644
|
+
} catch {}
|
|
31645
|
+
verdicts.delete(file);
|
|
31646
|
+
}
|
|
31647
|
+
}
|
|
31648
|
+
function sweepErrorReport(path, pid, now = Date.now()) {
|
|
31649
|
+
if (processAlive(pid)) {
|
|
31650
|
+
return;
|
|
31651
|
+
}
|
|
31652
|
+
try {
|
|
31653
|
+
if (now - statSync3(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
|
|
31654
|
+
unlinkSync(path);
|
|
31655
|
+
}
|
|
31656
|
+
} catch {}
|
|
31657
|
+
}
|
|
31658
|
+
async function announcedSince(projectPath, before, waiting = {}) {
|
|
31659
|
+
const wanted = resolve3(projectPath);
|
|
31660
|
+
const directories = waiting.directories ?? runtimeDirectories();
|
|
31661
|
+
const until = Date.now() + Math.max(waiting.budgetMs ?? ANNOUNCE_BUDGET_MS, 0);
|
|
31662
|
+
for (;; ) {
|
|
31663
|
+
const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && resolve3(endpoint.project.path) === wanted && (waiting.accept?.(endpoint) ?? true));
|
|
31664
|
+
if (fresh !== undefined) {
|
|
31665
|
+
return fresh;
|
|
31666
|
+
}
|
|
31667
|
+
const left = until - Date.now();
|
|
31668
|
+
if (left <= 0 || waiting.giveUp?.() === true) {
|
|
31669
|
+
return null;
|
|
31670
|
+
}
|
|
31671
|
+
await delay(Math.min(LOOK_EVERY_MS, left));
|
|
31672
|
+
}
|
|
31673
|
+
}
|
|
31674
|
+
function describe2(endpoint) {
|
|
31675
|
+
return `pid ${endpoint.pid} on ${endpoint.address}:${endpoint.port} (${endpoint.project.name || "unnamed"} at ${endpoint.project.path})`;
|
|
31676
|
+
}
|
|
31677
|
+
function tooNew(unspoken) {
|
|
31678
|
+
const named = unspoken.map((game) => `pid ${game.pid} speaks ${game.protocol} (${game.project.name || "unnamed"} at ${game.project.path})`).join("; ");
|
|
31679
|
+
const half = unspoken.some((game) => game.protocol > RUNTIME_PROTOCOL) ? "this server is the older half: reconnect it so it spawns the installed version" : "the addon is the older half: reinstall it and restart the game";
|
|
31680
|
+
return `A game is running, in a protocol this server does not speak ${named}. This server speaks ${RUNTIME_PROTOCOL}, so ${half}.`;
|
|
31681
|
+
}
|
|
31682
|
+
function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
|
|
31683
|
+
if (pid !== undefined) {
|
|
31684
|
+
const named = endpoints.find((endpoint) => endpoint.pid === pid);
|
|
31685
|
+
if (named !== undefined) {
|
|
31686
|
+
if (projectPath !== undefined && resolve3(named.project.path) !== resolve3(projectPath)) {
|
|
31687
|
+
return {
|
|
31688
|
+
problem: `The game with pid ${pid} is running ${named.project.path}, not ${resolve3(projectPath)}. Running: ${endpoints.map(describe2).join("; ")}.`
|
|
31689
|
+
};
|
|
31690
|
+
}
|
|
31691
|
+
return { endpoint: named };
|
|
31692
|
+
}
|
|
31693
|
+
const tooNewToo = unspoken.find((game) => game.pid === pid);
|
|
31694
|
+
if (tooNewToo !== undefined) {
|
|
31695
|
+
return { problem: tooNew([tooNewToo]) };
|
|
31696
|
+
}
|
|
31697
|
+
return {
|
|
31698
|
+
problem: endpoints.length === 0 ? `No game with the runtime addon is running, so there is none with pid ${pid}. Start one with editor_run, or play the project from the editor with the addon enabled.` : `No running game has pid ${pid}. Running: ${endpoints.map(describe2).join("; ")}. editor_status lists them under runtimes as they come and go.`
|
|
31699
|
+
};
|
|
31700
|
+
}
|
|
31701
|
+
if (endpoints.length === 0) {
|
|
31702
|
+
if (unspoken.length > 0) {
|
|
31703
|
+
return { problem: tooNew(unspoken) };
|
|
31704
|
+
}
|
|
31705
|
+
const went = announcedAndGone(projectPath);
|
|
31706
|
+
const first = went[0];
|
|
31707
|
+
if (first !== undefined) {
|
|
31708
|
+
const ago = Math.max(1, Math.round((Date.now() - first.noticedAt) / 1000));
|
|
31709
|
+
const others = went.length - 1;
|
|
31710
|
+
const earlier = others === 0 ? "" : ` ${others} earlier ${others === 1 ? "one" : "ones"} went the same way.`;
|
|
31711
|
+
return {
|
|
31712
|
+
problem: `A game with the runtime addon announced itself and its process is gone: pid ${first.pid}` + `${first.project === "" ? "" : ` for ${first.project}`}, noticed ${ago}s ago.${earlier}` + " It quit or was ended rather than never starting, so editor_output has what it printed" + " on the way, including whatever it broke on. Start another with editor_run once you have read it."
|
|
31713
|
+
};
|
|
31714
|
+
}
|
|
31715
|
+
return {
|
|
31716
|
+
problem: "No game with the runtime addon is running. Start one with editor_run, or play the project from the editor with the addon enabled."
|
|
31717
|
+
};
|
|
31718
|
+
}
|
|
31719
|
+
if (projectPath !== undefined) {
|
|
31720
|
+
const wanted = resolve3(projectPath);
|
|
31721
|
+
const matching = endpoints.filter((endpoint) => resolve3(endpoint.project.path) === wanted);
|
|
31722
|
+
if (matching.length === 0) {
|
|
31723
|
+
return {
|
|
31724
|
+
problem: `No running game is from ${wanted}. Running: ${endpoints.map(describe2).join("; ")}.`
|
|
31725
|
+
};
|
|
31726
|
+
}
|
|
31727
|
+
if (matching.length === 1 && matching[0]) {
|
|
31728
|
+
return { endpoint: matching[0] };
|
|
31729
|
+
}
|
|
31730
|
+
return {
|
|
31731
|
+
problem: `Several games are running from ${wanted}: ${matching.map(describe2).join("; ")}. Pass pid to choose one.`
|
|
31732
|
+
};
|
|
31733
|
+
}
|
|
31734
|
+
if (endpoints.length === 1 && endpoints[0]) {
|
|
31735
|
+
return { endpoint: endpoints[0] };
|
|
31736
|
+
}
|
|
31737
|
+
return {
|
|
31738
|
+
problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass projectPath to choose one, or pid when they are from one project.`
|
|
31739
|
+
};
|
|
31740
|
+
}
|
|
31741
|
+
function runtimeRequest(endpoint, command, params, timeoutMs) {
|
|
31742
|
+
const id = nextRequestId++;
|
|
31743
|
+
return new Promise((settle) => {
|
|
31744
|
+
let done = false;
|
|
31745
|
+
let buffered = "";
|
|
31746
|
+
let welcomed = false;
|
|
31747
|
+
const socket = createConnection({ port: endpoint.port, host: endpoint.address });
|
|
31748
|
+
const finish = (reply) => {
|
|
31749
|
+
if (done) {
|
|
31750
|
+
return;
|
|
31751
|
+
}
|
|
31752
|
+
done = true;
|
|
31753
|
+
clearTimeout(timer);
|
|
31754
|
+
socket.destroy();
|
|
31755
|
+
settle(reply);
|
|
31756
|
+
};
|
|
31757
|
+
const timer = setTimeout(() => {
|
|
31758
|
+
finish({
|
|
31759
|
+
ok: false,
|
|
31760
|
+
reason: "busy",
|
|
31761
|
+
message: `The game (${describe2(endpoint)}) accepted the connection but did not answer '${command}' within ${timeoutMs}ms. It may be paused at a breakpoint or stuck in a long frame.`
|
|
31762
|
+
});
|
|
31763
|
+
}, timeoutMs);
|
|
31764
|
+
socket.setEncoding("utf8");
|
|
31765
|
+
socket.on("connect", () => {
|
|
31766
|
+
socket.write(`${JSON.stringify({ id, command, params })}
|
|
31767
|
+
`);
|
|
31768
|
+
});
|
|
31769
|
+
socket.on("data", (chunk) => {
|
|
31770
|
+
buffered += chunk;
|
|
31771
|
+
let newline = buffered.indexOf(`
|
|
31772
|
+
`);
|
|
31773
|
+
while (newline !== -1 && !done) {
|
|
31774
|
+
const line = buffered.slice(0, newline).trim();
|
|
31775
|
+
buffered = buffered.slice(newline + 1);
|
|
31776
|
+
newline = buffered.indexOf(`
|
|
31777
|
+
`);
|
|
31778
|
+
if (line === "") {
|
|
31779
|
+
continue;
|
|
31780
|
+
}
|
|
31781
|
+
let message;
|
|
31782
|
+
try {
|
|
31783
|
+
message = asParams(JSON.parse(line));
|
|
31784
|
+
} catch {
|
|
31785
|
+
continue;
|
|
31786
|
+
}
|
|
31787
|
+
if (!welcomed) {
|
|
31788
|
+
welcomed = true;
|
|
31789
|
+
if (readString(message, "type") === "welcome") {
|
|
31790
|
+
const spoken = readNumber(message, "protocol");
|
|
31791
|
+
if (spoken !== RUNTIME_PROTOCOL) {
|
|
31792
|
+
finish({
|
|
31793
|
+
ok: false,
|
|
31794
|
+
reason: "protocol",
|
|
31795
|
+
message: `The game (${describe2(endpoint)}) speaks runtime protocol ${spoken ?? "unknown"} and this server speaks ${RUNTIME_PROTOCOL}. Reinstall the runtime addon into the project.`
|
|
31796
|
+
});
|
|
31797
|
+
return;
|
|
31798
|
+
}
|
|
31799
|
+
continue;
|
|
31800
|
+
}
|
|
31801
|
+
}
|
|
31802
|
+
if (message["id"] !== id) {
|
|
31803
|
+
continue;
|
|
31804
|
+
}
|
|
31805
|
+
if (readString(message, "type") === "error") {
|
|
31806
|
+
finish({ ok: false, reason: "error", message: readString(message, "message") ?? "unknown error" });
|
|
31807
|
+
return;
|
|
31808
|
+
}
|
|
31809
|
+
finish({ ok: true, payload: message, endpoint });
|
|
31810
|
+
}
|
|
31811
|
+
});
|
|
31812
|
+
socket.on("error", (error) => {
|
|
31813
|
+
finish({
|
|
31814
|
+
ok: false,
|
|
31815
|
+
reason: "refused",
|
|
31816
|
+
message: `The game (${describe2(endpoint)}) is running but nothing answered on its port: ${error.code ?? error.message}. It may still be starting, or be shutting down.`
|
|
31817
|
+
});
|
|
31818
|
+
});
|
|
31819
|
+
socket.on("close", () => {
|
|
31820
|
+
finish({
|
|
31821
|
+
ok: false,
|
|
31822
|
+
reason: "refused",
|
|
31823
|
+
message: `The game (${describe2(endpoint)}) closed the connection before answering '${command}'.`
|
|
31824
|
+
});
|
|
31825
|
+
});
|
|
31826
|
+
});
|
|
31827
|
+
}
|
|
31828
|
+
var RUNTIME_PROTOCOL = 2, ANNOUNCEMENT_PATTERN, ERROR_REPORT_PATTERN, ERROR_REPORT_KEEP_MS, GONE_WINDOW_MS = 120000, GONE_KEPT = 32, gone, STARTED_AFTER_ANNOUNCING_MS = 2000, CONFIRMED_FOR_MS = 60000, JUDGED_AFTER_MS = 60000, verdicts, ENDED_WITHIN_MS = 3000, LOOK_EVERY_MS = 50, ANNOUNCE_BUDGET_MS = 5000, nextRequestId = 1;
|
|
31829
|
+
var init_runtime_client = __esm(() => {
|
|
31830
|
+
init_process_children();
|
|
31831
|
+
ANNOUNCEMENT_PATTERN = /^runtime-(\d+)\.json$/;
|
|
31832
|
+
ERROR_REPORT_PATTERN = /^runtime-(\d+)\.log$/;
|
|
31833
|
+
ERROR_REPORT_KEEP_MS = 60 * 60 * 1000;
|
|
31834
|
+
gone = [];
|
|
31835
|
+
verdicts = new Map;
|
|
31836
|
+
});
|
|
31837
|
+
|
|
31838
|
+
// src/boot-note.ts
|
|
31839
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "node:fs";
|
|
31840
|
+
import { dirname as dirname8, join as join13 } from "node:path";
|
|
31841
|
+
function bootNotePath(projectPath) {
|
|
31842
|
+
return join13(projectPath, ".godot", "gdharness-boot.json");
|
|
31843
|
+
}
|
|
31844
|
+
function readBootNote(projectPath) {
|
|
31845
|
+
try {
|
|
31846
|
+
const noted = readNumber(asParams(JSON.parse(readFileSync11(bootNotePath(projectPath), "utf8"))), "announcedAfterMs");
|
|
31847
|
+
return noted !== undefined && Number.isFinite(noted) && noted > 0 ? noted : null;
|
|
31848
|
+
} catch {
|
|
31849
|
+
return null;
|
|
31850
|
+
}
|
|
31851
|
+
}
|
|
31852
|
+
function writeBootNote(projectPath, announcedAfterMs) {
|
|
31853
|
+
try {
|
|
31854
|
+
const path = bootNotePath(projectPath);
|
|
31855
|
+
mkdirSync6(dirname8(path), { recursive: true });
|
|
31856
|
+
writeFileSync8(path, JSON.stringify({ announcedAfterMs: Math.round(announcedAfterMs), at: new Date().toISOString() }, null, 2), "utf8");
|
|
31857
|
+
} catch {}
|
|
31858
|
+
}
|
|
31859
|
+
function waitSizedTo(lastBootMs, usual = ANNOUNCE_BUDGET_MS) {
|
|
31860
|
+
if (lastBootMs === null) {
|
|
31861
|
+
return usual;
|
|
31862
|
+
}
|
|
31863
|
+
return Math.min(LONGEST_SIZED_WAIT_MS, Math.max(usual, Math.round(lastBootMs * 1.5)));
|
|
31864
|
+
}
|
|
31865
|
+
var LONGEST_SIZED_WAIT_MS = 60000;
|
|
31866
|
+
var init_boot_note = __esm(() => {
|
|
31867
|
+
init_runtime_client();
|
|
31868
|
+
});
|
|
31869
|
+
|
|
31870
|
+
// src/breakpoint-note.ts
|
|
31871
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync12, rmSync as rmSync6, writeFileSync as writeFileSync9 } from "node:fs";
|
|
31872
|
+
import { dirname as dirname9, isAbsolute as isAbsolute2, join as join14, relative as relative2 } from "node:path";
|
|
31873
|
+
function breakpointNotePath(projectPath) {
|
|
31874
|
+
return join14(projectPath, ".godot", "gdharness-breakpoints.json");
|
|
31875
|
+
}
|
|
31876
|
+
function writeBreakpointNote(projectPath, held) {
|
|
31877
|
+
const path = breakpointNotePath(projectPath);
|
|
31878
|
+
const root = realPathOr(projectPath);
|
|
31879
|
+
const inside = held.map((one) => ({ scriptPath: realPathOr(one.scriptPath), lines: one.lines })).filter((one) => one.lines.length > 0 && withinProject(root, one.scriptPath));
|
|
31880
|
+
try {
|
|
31881
|
+
if (inside.length === 0) {
|
|
31882
|
+
rmSync6(path, { force: true });
|
|
31883
|
+
return;
|
|
31884
|
+
}
|
|
31885
|
+
const note = {
|
|
31886
|
+
breakpoints: inside.map((one) => ({
|
|
31887
|
+
script: relative2(root, one.scriptPath).replaceAll("\\", "/"),
|
|
31888
|
+
lines: [...one.lines]
|
|
31889
|
+
}))
|
|
31890
|
+
};
|
|
31891
|
+
mkdirSync7(dirname9(path), { recursive: true });
|
|
31892
|
+
writeFileSync9(path, `${JSON.stringify(note, null, 2)}
|
|
31893
|
+
`, "utf8");
|
|
31894
|
+
} catch (error) {
|
|
31895
|
+
console.error(`[SERVER] Could not record the breakpoints at ${path}: ${errorMessage(error)}`);
|
|
31896
|
+
}
|
|
31897
|
+
}
|
|
31898
|
+
function readBreakpointNote(projectPath) {
|
|
31899
|
+
try {
|
|
31900
|
+
const parsed = JSON.parse(readFileSync12(breakpointNotePath(projectPath), "utf8"));
|
|
31901
|
+
if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.breakpoints)) {
|
|
31902
|
+
return [];
|
|
31903
|
+
}
|
|
31904
|
+
const held = [];
|
|
31905
|
+
for (const entry of parsed.breakpoints) {
|
|
31906
|
+
if (typeof entry !== "object" || entry === null) {
|
|
31907
|
+
continue;
|
|
31908
|
+
}
|
|
31909
|
+
const { script, lines: listed } = entry;
|
|
31910
|
+
if (typeof script !== "string" || isAbsolute2(script) || script.split("/").includes("..")) {
|
|
31911
|
+
continue;
|
|
31912
|
+
}
|
|
31913
|
+
const lines = Array.isArray(listed) ? listed.filter((line) => Number.isInteger(line) && line > 0) : [];
|
|
31914
|
+
if (lines.length > 0) {
|
|
31915
|
+
held.push({ scriptPath: realPathOr(join14(projectPath, script)), lines });
|
|
31916
|
+
}
|
|
31917
|
+
}
|
|
31918
|
+
return held;
|
|
31919
|
+
} catch {
|
|
31920
|
+
return [];
|
|
31921
|
+
}
|
|
31922
|
+
}
|
|
31923
|
+
function withinProject(projectPath, scriptPath) {
|
|
31924
|
+
const rel = relative2(projectPath, scriptPath);
|
|
31925
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute2(rel);
|
|
31926
|
+
}
|
|
31927
|
+
var init_breakpoint_note = __esm(() => {
|
|
31928
|
+
init_errors();
|
|
31929
|
+
init_paths();
|
|
31930
|
+
});
|
|
31931
|
+
|
|
31932
|
+
// src/bridge-announce.ts
|
|
31933
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync13, rmSync as rmSync7, writeFileSync as writeFileSync10 } from "node:fs";
|
|
31934
|
+
import { dirname as dirname10, join as join15 } from "node:path";
|
|
31935
|
+
function announcementPath(projectPath) {
|
|
31936
|
+
return join15(projectPath, ".godot", "gdharness-bridge.json");
|
|
31937
|
+
}
|
|
31938
|
+
function readAnnouncement(path) {
|
|
31939
|
+
try {
|
|
31940
|
+
const parsed = JSON.parse(readFileSync13(path, "utf8"));
|
|
31941
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
31942
|
+
return null;
|
|
31943
|
+
}
|
|
31944
|
+
const said = parsed;
|
|
31945
|
+
if (typeof said.port !== "number" || typeof said.pid !== "number") {
|
|
31946
|
+
return null;
|
|
31947
|
+
}
|
|
31948
|
+
return said;
|
|
31949
|
+
} catch {
|
|
31950
|
+
return null;
|
|
31951
|
+
}
|
|
31952
|
+
}
|
|
31953
|
+
function announcementIsOurs(path) {
|
|
31954
|
+
return readAnnouncement(path)?.pid === process.pid;
|
|
31955
|
+
}
|
|
31956
|
+
function announceBridge(projectPath, bridge) {
|
|
31957
|
+
const path = announcementPath(projectPath);
|
|
31958
|
+
try {
|
|
31959
|
+
mkdirSync8(dirname10(path), { recursive: true });
|
|
31960
|
+
const announcement = {
|
|
31961
|
+
protocol: BRIDGE_ANNOUNCE_PROTOCOL,
|
|
31962
|
+
host: bridge.host,
|
|
31963
|
+
port: bridge.port,
|
|
31964
|
+
pid: process.pid,
|
|
31965
|
+
version: bridge.version,
|
|
31966
|
+
startedAt: new Date().toISOString()
|
|
31967
|
+
};
|
|
31968
|
+
writeFileSync10(path, `${JSON.stringify(announcement, null, 2)}
|
|
31969
|
+
`, "utf8");
|
|
31970
|
+
return path;
|
|
31971
|
+
} catch (error) {
|
|
31972
|
+
console.error(`[SERVER] Could not announce the editor bridge at ${path}: ${errorMessage(error)}`);
|
|
31973
|
+
return null;
|
|
31974
|
+
}
|
|
31975
|
+
}
|
|
31976
|
+
function withdrawBridge(path) {
|
|
31977
|
+
if (path === null || !announcementIsOurs(path)) {
|
|
31978
|
+
return;
|
|
31979
|
+
}
|
|
31980
|
+
try {
|
|
31981
|
+
rmSync7(path, { force: true });
|
|
31982
|
+
} catch (error) {
|
|
31983
|
+
console.error(`[SERVER] Could not withdraw the editor bridge announcement: ${errorMessage(error)}`);
|
|
31984
|
+
}
|
|
31985
|
+
}
|
|
31986
|
+
var BRIDGE_ANNOUNCE_PROTOCOL = 1;
|
|
31987
|
+
var init_bridge_announce = __esm(() => {
|
|
31988
|
+
init_errors();
|
|
31989
|
+
});
|
|
31990
|
+
|
|
31991
|
+
// src/config-pin.ts
|
|
31992
|
+
import { existsSync as existsSync11, readFileSync as readFileSync14 } from "node:fs";
|
|
31993
|
+
import { join as join16 } from "node:path";
|
|
31994
|
+
function versionsNamedInConfig(projectPath) {
|
|
31995
|
+
const config = join16(projectPath, ".mcp.json");
|
|
31996
|
+
if (!existsSync11(config)) {
|
|
31997
|
+
return [];
|
|
31998
|
+
}
|
|
31999
|
+
let text;
|
|
32000
|
+
try {
|
|
32001
|
+
text = readFileSync14(config, "utf8");
|
|
32002
|
+
JSON.parse(text);
|
|
32003
|
+
} catch {
|
|
32004
|
+
return [];
|
|
32005
|
+
}
|
|
32006
|
+
return [...new Set([...text.matchAll(PINNED)].map((found) => found[1] ?? ""))].filter((version) => version !== "");
|
|
32007
|
+
}
|
|
32008
|
+
function configDisagrees(projectPath, serverVersion) {
|
|
32009
|
+
const named = versionsNamedInConfig(projectPath);
|
|
32010
|
+
const others = named.filter((version) => version !== serverVersion);
|
|
32011
|
+
if (others.length === 0) {
|
|
32012
|
+
return null;
|
|
32013
|
+
}
|
|
32014
|
+
return `.mcp.json in this project names gdharness@${others.join(" and gdharness@")}, and this server ` + `is ${serverVersion}. Whichever is meant to be right, nothing will change version until the ` + "MCP server is reconnected, and a pin kept anywhere else does not reach this file.";
|
|
32015
|
+
}
|
|
32016
|
+
var PINNED;
|
|
32017
|
+
var init_config_pin = __esm(() => {
|
|
32018
|
+
PINNED = /\bgdharness@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\b/g;
|
|
31388
32019
|
});
|
|
31389
32020
|
|
|
31390
32021
|
// src/framing.ts
|
|
@@ -31514,8 +32145,8 @@ var init_ports = __esm(() => {
|
|
|
31514
32145
|
});
|
|
31515
32146
|
|
|
31516
32147
|
// src/dap_client.ts
|
|
31517
|
-
import { createConnection } from "node:net";
|
|
31518
|
-
import { setTimeout as
|
|
32148
|
+
import { createConnection as createConnection2 } from "node:net";
|
|
32149
|
+
import { setTimeout as delay2 } from "node:timers/promises";
|
|
31519
32150
|
function said(body, field) {
|
|
31520
32151
|
const value = body?.[field];
|
|
31521
32152
|
return typeof value === "string" ? value : "";
|
|
@@ -31550,7 +32181,7 @@ class GodotDAPClient {
|
|
|
31550
32181
|
return;
|
|
31551
32182
|
}
|
|
31552
32183
|
await new Promise((resolve, reject) => {
|
|
31553
|
-
const socket =
|
|
32184
|
+
const socket = createConnection2({ host: this.host, port: this.port });
|
|
31554
32185
|
this.socket = socket;
|
|
31555
32186
|
const connectTimeout = setTimeout(() => {
|
|
31556
32187
|
socket.destroy();
|
|
@@ -31959,7 +32590,7 @@ class GodotDAPClient {
|
|
|
31959
32590
|
return answered;
|
|
31960
32591
|
}
|
|
31961
32592
|
} catch {}
|
|
31962
|
-
await
|
|
32593
|
+
await delay2(200);
|
|
31963
32594
|
}
|
|
31964
32595
|
return [];
|
|
31965
32596
|
}
|
|
@@ -31980,7 +32611,7 @@ class GodotDAPClient {
|
|
|
31980
32611
|
break;
|
|
31981
32612
|
}
|
|
31982
32613
|
} catch {}
|
|
31983
|
-
await
|
|
32614
|
+
await delay2(200);
|
|
31984
32615
|
}
|
|
31985
32616
|
const named = [];
|
|
31986
32617
|
for (const scope of scopes) {
|
|
@@ -35496,10 +36127,10 @@ var init_godot_bridge = __esm(() => {
|
|
|
35496
36127
|
});
|
|
35497
36128
|
|
|
35498
36129
|
// src/godot-path.ts
|
|
35499
|
-
import { execFile as
|
|
35500
|
-
import { existsSync as
|
|
36130
|
+
import { execFile as execFile5 } from "node:child_process";
|
|
36131
|
+
import { existsSync as existsSync12 } from "node:fs";
|
|
35501
36132
|
import { normalize as normalize2 } from "node:path";
|
|
35502
|
-
import { promisify as
|
|
36133
|
+
import { promisify as promisify5 } from "node:util";
|
|
35503
36134
|
|
|
35504
36135
|
class GodotLocator2 {
|
|
35505
36136
|
answered = new Map;
|
|
@@ -35552,11 +36183,11 @@ class GodotLocator2 {
|
|
|
35552
36183
|
return known;
|
|
35553
36184
|
}
|
|
35554
36185
|
let reason;
|
|
35555
|
-
if (path !== "godot" && !
|
|
36186
|
+
if (path !== "godot" && !existsSync12(path)) {
|
|
35556
36187
|
reason = "does not exist";
|
|
35557
36188
|
} else {
|
|
35558
36189
|
try {
|
|
35559
|
-
await
|
|
36190
|
+
await run5(path, ["--version"]);
|
|
35560
36191
|
reason = null;
|
|
35561
36192
|
} catch (error) {
|
|
35562
36193
|
reason = `does not answer --version: ${errorMessage(error)}`;
|
|
@@ -35566,11 +36197,11 @@ class GodotLocator2 {
|
|
|
35566
36197
|
return reason;
|
|
35567
36198
|
}
|
|
35568
36199
|
}
|
|
35569
|
-
var
|
|
36200
|
+
var run5;
|
|
35570
36201
|
var init_godot_path = __esm(() => {
|
|
35571
36202
|
init_detection();
|
|
35572
36203
|
init_errors();
|
|
35573
|
-
|
|
36204
|
+
run5 = promisify5(execFile5);
|
|
35574
36205
|
});
|
|
35575
36206
|
|
|
35576
36207
|
// src/issues.ts
|
|
@@ -35927,8 +36558,8 @@ var init_junit = __esm(() => {
|
|
|
35927
36558
|
// src/lsp_client.ts
|
|
35928
36559
|
import { realpathSync as realpathSync2 } from "node:fs";
|
|
35929
36560
|
import { readFile, realpath } from "node:fs/promises";
|
|
35930
|
-
import { createConnection as
|
|
35931
|
-
import { dirname as
|
|
36561
|
+
import { createConnection as createConnection3 } from "node:net";
|
|
36562
|
+
import { dirname as dirname11, resolve as resolve4 } from "node:path";
|
|
35932
36563
|
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
|
|
35933
36564
|
function diagnosticsKey(uri) {
|
|
35934
36565
|
let decoded;
|
|
@@ -35974,7 +36605,7 @@ class GodotLSPClient {
|
|
|
35974
36605
|
}
|
|
35975
36606
|
this.connectPromise = new Promise((resolveConnect, rejectConnect) => {
|
|
35976
36607
|
let settled = false;
|
|
35977
|
-
const socket =
|
|
36608
|
+
const socket = createConnection3({ port: this.port, host: this.host }, () => {
|
|
35978
36609
|
this.socket = socket;
|
|
35979
36610
|
this.connected = true;
|
|
35980
36611
|
this.reader = new FrameReader;
|
|
@@ -36056,7 +36687,7 @@ class GodotLSPClient {
|
|
|
36056
36687
|
if (this.initialized) {
|
|
36057
36688
|
return;
|
|
36058
36689
|
}
|
|
36059
|
-
const rootPath = this.rootPath ??
|
|
36690
|
+
const rootPath = this.rootPath ?? dirname11(resolve4(filePath));
|
|
36060
36691
|
await this.initialize(rootPath);
|
|
36061
36692
|
}
|
|
36062
36693
|
async sendRequest(method, params) {
|
|
@@ -36194,7 +36825,7 @@ class GodotLSPClient {
|
|
|
36194
36825
|
});
|
|
36195
36826
|
}
|
|
36196
36827
|
toFileUri(filePath) {
|
|
36197
|
-
return pathToFileURL(
|
|
36828
|
+
return pathToFileURL(resolve4(filePath)).href;
|
|
36198
36829
|
}
|
|
36199
36830
|
syncDocument(filePath, content) {
|
|
36200
36831
|
const uri = this.toFileUri(filePath);
|
|
@@ -36235,7 +36866,7 @@ class GodotLSPClient {
|
|
|
36235
36866
|
}
|
|
36236
36867
|
async initialize(rootPath) {
|
|
36237
36868
|
await this.ensureConnected();
|
|
36238
|
-
const resolvedRootPath =
|
|
36869
|
+
const resolvedRootPath = resolve4(rootPath);
|
|
36239
36870
|
const rootUri = pathToFileURL(resolvedRootPath).href;
|
|
36240
36871
|
const result = await this.sendRequest("initialize", {
|
|
36241
36872
|
processId: process.pid,
|
|
@@ -36378,7 +37009,7 @@ function normalizeLSPError(error, port) {
|
|
|
36378
37009
|
return String(error);
|
|
36379
37010
|
}
|
|
36380
37011
|
async function resolveLSPPaths(projectPathValue, scriptPathValue) {
|
|
36381
|
-
const requestedProjectPath =
|
|
37012
|
+
const requestedProjectPath = resolve4(projectPathValue);
|
|
36382
37013
|
let projectPath;
|
|
36383
37014
|
try {
|
|
36384
37015
|
projectPath = await realpath(requestedProjectPath);
|
|
@@ -36432,169 +37063,33 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
36432
37063
|
return asToolResponse({ completions });
|
|
36433
37064
|
}
|
|
36434
37065
|
case "lsp_get_hover": {
|
|
36435
|
-
const line = Number(parsedArgs["line"]);
|
|
36436
|
-
const character = Number(parsedArgs["character"]);
|
|
36437
|
-
if (!Number.isFinite(line) || !Number.isFinite(character)) {
|
|
36438
|
-
throw new Refusal2("Arguments line and character must be numbers.");
|
|
36439
|
-
}
|
|
36440
|
-
const hover = await client.getHover(scriptPath, content, line, character);
|
|
36441
|
-
return asToolResponse({ hover });
|
|
36442
|
-
}
|
|
36443
|
-
case "lsp_get_symbols": {
|
|
36444
|
-
const symbols = await client.getDocumentSymbols(scriptPath, content);
|
|
36445
|
-
return asToolResponse({ symbols });
|
|
36446
|
-
}
|
|
36447
|
-
default:
|
|
36448
|
-
return asToolResponse({ error: `Unknown LSP tool: ${toolName}` });
|
|
36449
|
-
}
|
|
36450
|
-
} catch (error) {
|
|
36451
|
-
return asToolResponse({
|
|
36452
|
-
error: normalizeLSPError(error, client.port)
|
|
36453
|
-
});
|
|
36454
|
-
}
|
|
36455
|
-
}
|
|
36456
|
-
var DEFAULT_LSP_PORT = 6005, DIAGNOSTICS_TIMEOUT_MS = 5000;
|
|
36457
|
-
var init_lsp_client = __esm(() => {
|
|
36458
|
-
init_errors();
|
|
36459
|
-
init_framing();
|
|
36460
|
-
init_paths();
|
|
36461
|
-
init_ports();
|
|
36462
|
-
});
|
|
36463
|
-
|
|
36464
|
-
// src/process-children.ts
|
|
36465
|
-
import { execFile as execFile5 } from "node:child_process";
|
|
36466
|
-
import { basename as basename2 } from "node:path";
|
|
36467
|
-
import { promisify as promisify5 } from "node:util";
|
|
36468
|
-
async function processTree() {
|
|
36469
|
-
try {
|
|
36470
|
-
const printed = process.platform === "win32" ? await askWindows() : await askPosix();
|
|
36471
|
-
return parseProcessTable(printed);
|
|
36472
|
-
} catch {
|
|
36473
|
-
return;
|
|
36474
|
-
}
|
|
36475
|
-
}
|
|
36476
|
-
function parseProcessTable(printed) {
|
|
36477
|
-
const tree = new Map;
|
|
36478
|
-
for (const line of printed.split(/\r?\n/)) {
|
|
36479
|
-
const fields = /^\s*(\d+)\s+(\d+)(?:\s+(.*))?$/.exec(line);
|
|
36480
|
-
if (fields === null) {
|
|
36481
|
-
continue;
|
|
36482
|
-
}
|
|
36483
|
-
const pid = Number(fields[1]);
|
|
36484
|
-
const parent = Number(fields[2]);
|
|
36485
|
-
if (pid > 0) {
|
|
36486
|
-
tree.set(pid, { parent, command: (fields[3] ?? "").trim() });
|
|
36487
|
-
}
|
|
36488
|
-
}
|
|
36489
|
-
return tree;
|
|
36490
|
-
}
|
|
36491
|
-
function childrenIn(tree, pid) {
|
|
36492
|
-
const children = [];
|
|
36493
|
-
for (const [child, listed] of tree) {
|
|
36494
|
-
if (listed.parent === pid) {
|
|
36495
|
-
children.push(child);
|
|
36496
|
-
}
|
|
36497
|
-
}
|
|
36498
|
-
return children;
|
|
36499
|
-
}
|
|
36500
|
-
function descendantsIn(tree, pid) {
|
|
36501
|
-
const found = [];
|
|
36502
|
-
const queue = [pid];
|
|
36503
|
-
const seen = new Set([pid]);
|
|
36504
|
-
while (queue.length > 0) {
|
|
36505
|
-
const next = queue.shift();
|
|
36506
|
-
if (next === undefined) {
|
|
36507
|
-
break;
|
|
36508
|
-
}
|
|
36509
|
-
for (const child of childrenIn(tree, next)) {
|
|
36510
|
-
if (!seen.has(child)) {
|
|
36511
|
-
seen.add(child);
|
|
36512
|
-
found.push(child);
|
|
36513
|
-
queue.push(child);
|
|
36514
|
-
}
|
|
36515
|
-
}
|
|
36516
|
-
}
|
|
36517
|
-
return found;
|
|
36518
|
-
}
|
|
36519
|
-
function ancestorsIn(tree, pid, root) {
|
|
36520
|
-
const chain = [];
|
|
36521
|
-
const seen = new Set([pid]);
|
|
36522
|
-
let parent = tree.get(pid)?.parent;
|
|
36523
|
-
while (parent !== undefined && !seen.has(parent)) {
|
|
36524
|
-
chain.push(parent);
|
|
36525
|
-
if (parent === root) {
|
|
36526
|
-
break;
|
|
36527
|
-
}
|
|
36528
|
-
seen.add(parent);
|
|
36529
|
-
parent = tree.get(parent)?.parent;
|
|
36530
|
-
}
|
|
36531
|
-
return chain;
|
|
36532
|
-
}
|
|
36533
|
-
function readCommandLine(command) {
|
|
36534
|
-
const words = commandWords(command);
|
|
36535
|
-
const executable = words[0] === undefined ? "" : basename2(words[0].replaceAll("\\", "/"));
|
|
36536
|
-
const at = words.indexOf("--path");
|
|
36537
|
-
let projectPath = null;
|
|
36538
|
-
if (at !== -1 && at + 1 < words.length) {
|
|
36539
|
-
const taken = [];
|
|
36540
|
-
for (const word of words.slice(at + 1)) {
|
|
36541
|
-
if (taken.length > 0 && (word.startsWith("-") || /^[a-z]+:\/\//.test(word))) {
|
|
36542
|
-
break;
|
|
37066
|
+
const line = Number(parsedArgs["line"]);
|
|
37067
|
+
const character = Number(parsedArgs["character"]);
|
|
37068
|
+
if (!Number.isFinite(line) || !Number.isFinite(character)) {
|
|
37069
|
+
throw new Refusal2("Arguments line and character must be numbers.");
|
|
37070
|
+
}
|
|
37071
|
+
const hover = await client.getHover(scriptPath, content, line, character);
|
|
37072
|
+
return asToolResponse({ hover });
|
|
36543
37073
|
}
|
|
36544
|
-
|
|
36545
|
-
|
|
36546
|
-
|
|
36547
|
-
}
|
|
36548
|
-
return {
|
|
36549
|
-
executable,
|
|
36550
|
-
projectPath,
|
|
36551
|
-
editor: words.some((word) => word === "-e" || word === "--editor")
|
|
36552
|
-
};
|
|
36553
|
-
}
|
|
36554
|
-
function commandWords(command) {
|
|
36555
|
-
const words = [];
|
|
36556
|
-
let word = "";
|
|
36557
|
-
let quoted = false;
|
|
36558
|
-
let started = false;
|
|
36559
|
-
for (const char of command) {
|
|
36560
|
-
if (char === '"') {
|
|
36561
|
-
quoted = !quoted;
|
|
36562
|
-
started = true;
|
|
36563
|
-
} else if (!quoted && /\s/.test(char)) {
|
|
36564
|
-
if (started) {
|
|
36565
|
-
words.push(word);
|
|
36566
|
-
word = "";
|
|
36567
|
-
started = false;
|
|
37074
|
+
case "lsp_get_symbols": {
|
|
37075
|
+
const symbols = await client.getDocumentSymbols(scriptPath, content);
|
|
37076
|
+
return asToolResponse({ symbols });
|
|
36568
37077
|
}
|
|
36569
|
-
|
|
36570
|
-
|
|
36571
|
-
started = true;
|
|
37078
|
+
default:
|
|
37079
|
+
return asToolResponse({ error: `Unknown LSP tool: ${toolName}` });
|
|
36572
37080
|
}
|
|
37081
|
+
} catch (error) {
|
|
37082
|
+
return asToolResponse({
|
|
37083
|
+
error: normalizeLSPError(error, client.port)
|
|
37084
|
+
});
|
|
36573
37085
|
}
|
|
36574
|
-
if (started) {
|
|
36575
|
-
words.push(word);
|
|
36576
|
-
}
|
|
36577
|
-
return words;
|
|
36578
|
-
}
|
|
36579
|
-
async function askWindows() {
|
|
36580
|
-
const { stdout } = await run5("powershell.exe", [
|
|
36581
|
-
"-NoProfile",
|
|
36582
|
-
"-NonInteractive",
|
|
36583
|
-
"-Command",
|
|
36584
|
-
'Get-CimInstance Win32_Process | ForEach-Object { "$($_.ProcessId) $($_.ParentProcessId) $($_.CommandLine)" }'
|
|
36585
|
-
], { timeout: ASK_TIMEOUT_MS, windowsHide: true, maxBuffer: 16 * 1024 * 1024 });
|
|
36586
|
-
return stdout;
|
|
36587
|
-
}
|
|
36588
|
-
async function askPosix() {
|
|
36589
|
-
const { stdout } = await run5("ps", ["-e", "-o", "pid=,ppid=,args="], {
|
|
36590
|
-
timeout: ASK_TIMEOUT_MS,
|
|
36591
|
-
maxBuffer: 16 * 1024 * 1024
|
|
36592
|
-
});
|
|
36593
|
-
return stdout;
|
|
36594
37086
|
}
|
|
36595
|
-
var
|
|
36596
|
-
var
|
|
36597
|
-
|
|
37087
|
+
var DEFAULT_LSP_PORT = 6005, DIAGNOSTICS_TIMEOUT_MS = 5000;
|
|
37088
|
+
var init_lsp_client = __esm(() => {
|
|
37089
|
+
init_errors();
|
|
37090
|
+
init_framing();
|
|
37091
|
+
init_paths();
|
|
37092
|
+
init_ports();
|
|
36598
37093
|
});
|
|
36599
37094
|
|
|
36600
37095
|
// src/process-time.ts
|
|
@@ -36637,13 +37132,13 @@ var init_process_time = __esm(() => {
|
|
|
36637
37132
|
});
|
|
36638
37133
|
|
|
36639
37134
|
// src/project-scan.ts
|
|
36640
|
-
import { existsSync as
|
|
36641
|
-
import { join as
|
|
37135
|
+
import { existsSync as existsSync13, readdirSync as readdirSync5, readFileSync as readFileSync15 } from "node:fs";
|
|
37136
|
+
import { join as join17 } from "node:path";
|
|
36642
37137
|
function skipped(name) {
|
|
36643
37138
|
return name.startsWith(".") || SKIPPED.has(name);
|
|
36644
37139
|
}
|
|
36645
37140
|
function steppedOver(directory) {
|
|
36646
|
-
return
|
|
37141
|
+
return existsSync13(join17(directory, ".gdignore"));
|
|
36647
37142
|
}
|
|
36648
37143
|
function projectStructure(projectPath) {
|
|
36649
37144
|
const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
|
|
@@ -36651,12 +37146,12 @@ function projectStructure(projectPath) {
|
|
|
36651
37146
|
if (steppedOver(directory)) {
|
|
36652
37147
|
return;
|
|
36653
37148
|
}
|
|
36654
|
-
for (const entry of
|
|
37149
|
+
for (const entry of readdirSync5(directory, { withFileTypes: true })) {
|
|
36655
37150
|
if (skipped(entry.name)) {
|
|
36656
37151
|
continue;
|
|
36657
37152
|
}
|
|
36658
37153
|
if (entry.isDirectory()) {
|
|
36659
|
-
visit(
|
|
37154
|
+
visit(join17(directory, entry.name));
|
|
36660
37155
|
} else if (entry.isFile()) {
|
|
36661
37156
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
36662
37157
|
if (extension === "tscn") {
|
|
@@ -36680,20 +37175,20 @@ function scriptsWithoutUid(projectPath) {
|
|
|
36680
37175
|
if (steppedOver(directory)) {
|
|
36681
37176
|
return;
|
|
36682
37177
|
}
|
|
36683
|
-
for (const entry of
|
|
37178
|
+
for (const entry of readdirSync5(directory, { withFileTypes: true })) {
|
|
36684
37179
|
if (skipped(entry.name)) {
|
|
36685
37180
|
continue;
|
|
36686
37181
|
}
|
|
36687
37182
|
const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
36688
37183
|
if (entry.isDirectory()) {
|
|
36689
|
-
visit(
|
|
37184
|
+
visit(join17(directory, entry.name), spelled);
|
|
36690
37185
|
continue;
|
|
36691
37186
|
}
|
|
36692
37187
|
if (!entry.isFile()) {
|
|
36693
37188
|
continue;
|
|
36694
37189
|
}
|
|
36695
37190
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
36696
|
-
if (CARRIES_A_UID.has(extension) && !
|
|
37191
|
+
if (CARRIES_A_UID.has(extension) && !existsSync13(join17(directory, `${entry.name}.uid`))) {
|
|
36697
37192
|
found.push(spelled);
|
|
36698
37193
|
}
|
|
36699
37194
|
}
|
|
@@ -36721,459 +37216,126 @@ function searchProject(projectPath, options) {
|
|
|
36721
37216
|
if (steppedOver(directory)) {
|
|
36722
37217
|
return;
|
|
36723
37218
|
}
|
|
36724
|
-
for (const entry of
|
|
37219
|
+
for (const entry of readdirSync5(directory, { withFileTypes: true })) {
|
|
36725
37220
|
if (full()) {
|
|
36726
37221
|
return;
|
|
36727
37222
|
}
|
|
36728
37223
|
if (skipped(entry.name)) {
|
|
36729
37224
|
continue;
|
|
36730
37225
|
}
|
|
36731
|
-
const entryPath =
|
|
37226
|
+
const entryPath = join17(directory, entry.name);
|
|
36732
37227
|
if (entry.isDirectory()) {
|
|
36733
37228
|
visit(entryPath);
|
|
36734
37229
|
continue;
|
|
36735
37230
|
}
|
|
36736
|
-
const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
|
|
36737
|
-
if (!entry.isFile() || !extensions.has(extension)) {
|
|
36738
|
-
continue;
|
|
36739
|
-
}
|
|
36740
|
-
result.summary.files_searched += 1;
|
|
36741
|
-
const matches = [];
|
|
36742
|
-
for (const [index, line] of
|
|
36743
|
-
`).entries()) {
|
|
36744
|
-
if (full()) {
|
|
36745
|
-
break;
|
|
36746
|
-
}
|
|
36747
|
-
const match = regex ? regex.exec(line)?.[0] : (options.caseSensitive ? line : line.toLowerCase()).includes(needle) ? options.query : "";
|
|
36748
|
-
if (match) {
|
|
36749
|
-
matches.push({ line: index + 1, content: line.trim(), match });
|
|
36750
|
-
result.summary.total_matches += 1;
|
|
36751
|
-
}
|
|
36752
|
-
}
|
|
36753
|
-
if (matches.length > 0) {
|
|
36754
|
-
const relativePath = entryPath.slice(projectPath.length + 1).replace(/\\/g, "/");
|
|
36755
|
-
result.results.push({ file: `res://${relativePath}`, matches });
|
|
36756
|
-
result.summary.files_with_matches += 1;
|
|
36757
|
-
}
|
|
36758
|
-
}
|
|
36759
|
-
};
|
|
36760
|
-
visit(projectPath);
|
|
36761
|
-
return result;
|
|
36762
|
-
}
|
|
36763
|
-
var SKIPPED, ASSET_EXTENSIONS, CARRIES_A_UID;
|
|
36764
|
-
var init_project_scan = __esm(() => {
|
|
36765
|
-
SKIPPED = new Set(["node_modules"]);
|
|
36766
|
-
ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
|
|
36767
|
-
CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
|
|
36768
|
-
});
|
|
36769
|
-
|
|
36770
|
-
// src/restart-note.ts
|
|
36771
|
-
import { mkdirSync as mkdirSync8, readFileSync as readFileSync14, rmSync as rmSync8, writeFileSync as writeFileSync10 } from "node:fs";
|
|
36772
|
-
import { dirname as dirname10, join as join16 } from "node:path";
|
|
36773
|
-
function restartNotePath(projectPath) {
|
|
36774
|
-
return join16(projectPath, ".godot", "gdharness-restart.json");
|
|
36775
|
-
}
|
|
36776
|
-
function noteRestartBegun(note) {
|
|
36777
|
-
const path = restartNotePath(note.projectPath);
|
|
36778
|
-
try {
|
|
36779
|
-
mkdirSync8(dirname10(path), { recursive: true });
|
|
36780
|
-
writeFileSync10(path, `${JSON.stringify(note, null, 2)}
|
|
36781
|
-
`, "utf8");
|
|
36782
|
-
} catch (error) {
|
|
36783
|
-
console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
|
|
36784
|
-
}
|
|
36785
|
-
}
|
|
36786
|
-
function restartOwed(projectPath) {
|
|
36787
|
-
try {
|
|
36788
|
-
const parsed = JSON.parse(readFileSync14(restartNotePath(projectPath), "utf8"));
|
|
36789
|
-
if (typeof parsed !== "object" || parsed === null) {
|
|
36790
|
-
return null;
|
|
36791
|
-
}
|
|
36792
|
-
const said = parsed;
|
|
36793
|
-
if (typeof said.quitAt !== "string" || typeof said.byPid !== "number" || typeof said.ports !== "object") {
|
|
36794
|
-
return null;
|
|
36795
|
-
}
|
|
36796
|
-
return said;
|
|
36797
|
-
} catch {
|
|
36798
|
-
return null;
|
|
36799
|
-
}
|
|
36800
|
-
}
|
|
36801
|
-
function restartSettled(projectPath) {
|
|
36802
|
-
try {
|
|
36803
|
-
rmSync8(restartNotePath(projectPath), { force: true });
|
|
36804
|
-
} catch (error) {
|
|
36805
|
-
console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
|
|
36806
|
-
}
|
|
36807
|
-
}
|
|
36808
|
-
var init_restart_note = __esm(() => {
|
|
36809
|
-
init_errors();
|
|
36810
|
-
});
|
|
36811
|
-
|
|
36812
|
-
// src/runtime-client.ts
|
|
36813
|
-
import { existsSync as existsSync13, readdirSync as readdirSync5, readFileSync as readFileSync15, statSync as statSync3, unlinkSync } from "node:fs";
|
|
36814
|
-
import { createConnection as createConnection3 } from "node:net";
|
|
36815
|
-
import { tmpdir as tmpdir4 } from "node:os";
|
|
36816
|
-
import { join as join17, resolve as resolve4 } from "node:path";
|
|
36817
|
-
import { setTimeout as delay2 } from "node:timers/promises";
|
|
36818
|
-
function runtimeDirectory(variables = process.env) {
|
|
36819
|
-
const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
|
|
36820
|
-
if (explicit) {
|
|
36821
|
-
return explicit;
|
|
36822
|
-
}
|
|
36823
|
-
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
36824
|
-
return join17(perUser ?? tmpdir4(), "gdharness");
|
|
36825
|
-
}
|
|
36826
|
-
function runtimeDirectories(variables = process.env) {
|
|
36827
|
-
const candidates = [runtimeDirectory(variables)];
|
|
36828
|
-
const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
|
|
36829
|
-
for (const base of fallbacks) {
|
|
36830
|
-
candidates.push(join17(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
|
|
36831
|
-
}
|
|
36832
|
-
return [...new Set(candidates.map((path) => resolve4(path)))];
|
|
36833
|
-
}
|
|
36834
|
-
function errorReportOf(pid, directories = runtimeDirectories()) {
|
|
36835
|
-
for (const directory of directories) {
|
|
36836
|
-
const path = join17(directory, `runtime-${pid}.log`);
|
|
36837
|
-
if (existsSync13(path)) {
|
|
36838
|
-
return path;
|
|
36839
|
-
}
|
|
36840
|
-
}
|
|
36841
|
-
return null;
|
|
36842
|
-
}
|
|
36843
|
-
function processAlive(pid) {
|
|
36844
|
-
try {
|
|
36845
|
-
process.kill(pid, 0);
|
|
36846
|
-
return true;
|
|
36847
|
-
} catch (error) {
|
|
36848
|
-
return error.code === "EPERM";
|
|
36849
|
-
}
|
|
36850
|
-
}
|
|
36851
|
-
function parseAnnouncement(file, pid) {
|
|
36852
|
-
let fields;
|
|
36853
|
-
try {
|
|
36854
|
-
fields = asParams(JSON.parse(readFileSync15(file, "utf8")));
|
|
36855
|
-
} catch {
|
|
36856
|
-
return { kind: "rubbish" };
|
|
36857
|
-
}
|
|
36858
|
-
const project = readParams(fields, "project");
|
|
36859
|
-
const port = readNumber(fields, "port");
|
|
36860
|
-
const address = readString(fields, "address");
|
|
36861
|
-
const protocol = readNumber(fields, "protocol");
|
|
36862
|
-
if (readNumber(fields, "pid") !== pid || port === undefined || !Number.isInteger(port) || port < 1 || port > 65535 || address === undefined || project === undefined) {
|
|
36863
|
-
return { kind: "rubbish" };
|
|
36864
|
-
}
|
|
36865
|
-
const named = {
|
|
36866
|
-
name: readString(project, "name") ?? "",
|
|
36867
|
-
path: readString(project, "path") ?? ""
|
|
36868
|
-
};
|
|
36869
|
-
if (protocol !== RUNTIME_PROTOCOL) {
|
|
36870
|
-
return { kind: "unspoken", unspoken: { pid, protocol: protocol ?? 0, project: named } };
|
|
36871
|
-
}
|
|
36872
|
-
const editorPid = readNumber(fields, "editor_pid");
|
|
36873
|
-
return {
|
|
36874
|
-
kind: "runtime",
|
|
36875
|
-
endpoint: {
|
|
36876
|
-
pid,
|
|
36877
|
-
port,
|
|
36878
|
-
address,
|
|
36879
|
-
project: named,
|
|
36880
|
-
file,
|
|
36881
|
-
...editorPid !== undefined && Number.isInteger(editorPid) && editorPid > 0 ? { editorPid } : {}
|
|
36882
|
-
}
|
|
36883
|
-
};
|
|
36884
|
-
}
|
|
36885
|
-
function runtimesAnnounced(directories = runtimeDirectories()) {
|
|
36886
|
-
const running = new Map;
|
|
36887
|
-
const unspoken = new Map;
|
|
36888
|
-
for (const directory of directories) {
|
|
36889
|
-
for (const found of announcedIn(directory)) {
|
|
36890
|
-
if (found.kind === "runtime" && !running.has(found.endpoint.pid)) {
|
|
36891
|
-
running.set(found.endpoint.pid, found.endpoint);
|
|
36892
|
-
} else if (found.kind === "unspoken" && !unspoken.has(found.unspoken.pid)) {
|
|
36893
|
-
unspoken.set(found.unspoken.pid, found.unspoken);
|
|
36894
|
-
}
|
|
36895
|
-
}
|
|
36896
|
-
}
|
|
36897
|
-
const newest = (a, b) => b.pid - a.pid;
|
|
36898
|
-
return { running: [...running.values()].sort(newest), unspoken: [...unspoken.values()].sort(newest) };
|
|
36899
|
-
}
|
|
36900
|
-
function discoverRuntimes(directories = runtimeDirectories()) {
|
|
36901
|
-
return runtimesAnnounced(directories).running;
|
|
36902
|
-
}
|
|
36903
|
-
function wentAway(file, pid) {
|
|
36904
|
-
let project = "";
|
|
36905
|
-
try {
|
|
36906
|
-
const fields = asParams(JSON.parse(readFileSync15(file, "utf8")));
|
|
36907
|
-
project = readString(readParams(fields, "project") ?? {}, "path") ?? "";
|
|
36908
|
-
} catch {
|
|
36909
|
-
return;
|
|
36910
|
-
}
|
|
36911
|
-
if (project === "") {
|
|
36912
|
-
return;
|
|
36913
|
-
}
|
|
36914
|
-
gone.unshift({ pid, project, noticedAt: Date.now() });
|
|
36915
|
-
gone.length = Math.min(gone.length, GONE_KEPT);
|
|
36916
|
-
}
|
|
36917
|
-
function announcedAndGone(projectPath) {
|
|
36918
|
-
if (projectPath === undefined) {
|
|
36919
|
-
return [];
|
|
36920
|
-
}
|
|
36921
|
-
const since = Date.now() - GONE_WINDOW_MS;
|
|
36922
|
-
const wanted = resolve4(projectPath);
|
|
36923
|
-
return gone.filter((one) => one.noticedAt >= since && resolve4(one.project) === wanted);
|
|
36924
|
-
}
|
|
36925
|
-
function announcedIn(directory) {
|
|
36926
|
-
if (!existsSync13(directory)) {
|
|
36927
|
-
return [];
|
|
36928
|
-
}
|
|
36929
|
-
const found = [];
|
|
36930
|
-
for (const entry of readdirSync5(directory)) {
|
|
36931
|
-
const report = ERROR_REPORT_PATTERN.exec(entry);
|
|
36932
|
-
if (report) {
|
|
36933
|
-
sweepErrorReport(join17(directory, entry), Number.parseInt(report[1] ?? "", 10));
|
|
36934
|
-
continue;
|
|
36935
|
-
}
|
|
36936
|
-
const match = ANNOUNCEMENT_PATTERN.exec(entry);
|
|
36937
|
-
if (!match) {
|
|
36938
|
-
continue;
|
|
36939
|
-
}
|
|
36940
|
-
const file = join17(directory, entry);
|
|
36941
|
-
const pid = Number.parseInt(match[1] ?? "", 10);
|
|
36942
|
-
const alive = processAlive(pid);
|
|
36943
|
-
const announced = alive ? parseAnnouncement(file, pid) : { kind: "rubbish" };
|
|
36944
|
-
if (announced.kind === "rubbish") {
|
|
36945
|
-
if (!alive) {
|
|
36946
|
-
wentAway(file, pid);
|
|
37231
|
+
const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
|
|
37232
|
+
if (!entry.isFile() || !extensions.has(extension)) {
|
|
37233
|
+
continue;
|
|
37234
|
+
}
|
|
37235
|
+
result.summary.files_searched += 1;
|
|
37236
|
+
const matches = [];
|
|
37237
|
+
for (const [index, line] of readFileSync15(entryPath, "utf8").split(`
|
|
37238
|
+
`).entries()) {
|
|
37239
|
+
if (full()) {
|
|
37240
|
+
break;
|
|
37241
|
+
}
|
|
37242
|
+
const match = regex ? regex.exec(line)?.[0] : (options.caseSensitive ? line : line.toLowerCase()).includes(needle) ? options.query : "";
|
|
37243
|
+
if (match) {
|
|
37244
|
+
matches.push({ line: index + 1, content: line.trim(), match });
|
|
37245
|
+
result.summary.total_matches += 1;
|
|
37246
|
+
}
|
|
37247
|
+
}
|
|
37248
|
+
if (matches.length > 0) {
|
|
37249
|
+
const relativePath = entryPath.slice(projectPath.length + 1).replace(/\\/g, "/");
|
|
37250
|
+
result.results.push({ file: `res://${relativePath}`, matches });
|
|
37251
|
+
result.summary.files_with_matches += 1;
|
|
36947
37252
|
}
|
|
36948
|
-
try {
|
|
36949
|
-
unlinkSync(file);
|
|
36950
|
-
} catch {}
|
|
36951
|
-
continue;
|
|
36952
37253
|
}
|
|
36953
|
-
|
|
36954
|
-
|
|
36955
|
-
return
|
|
37254
|
+
};
|
|
37255
|
+
visit(projectPath);
|
|
37256
|
+
return result;
|
|
36956
37257
|
}
|
|
36957
|
-
|
|
36958
|
-
|
|
36959
|
-
|
|
36960
|
-
|
|
37258
|
+
var SKIPPED, ASSET_EXTENSIONS, CARRIES_A_UID;
|
|
37259
|
+
var init_project_scan = __esm(() => {
|
|
37260
|
+
SKIPPED = new Set(["node_modules"]);
|
|
37261
|
+
ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
|
|
37262
|
+
CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
|
|
37263
|
+
});
|
|
37264
|
+
|
|
37265
|
+
// src/restart-note.ts
|
|
37266
|
+
import { mkdirSync as mkdirSync9, readFileSync as readFileSync16, rmSync as rmSync8, writeFileSync as writeFileSync11 } from "node:fs";
|
|
37267
|
+
import { dirname as dirname12, join as join18 } from "node:path";
|
|
37268
|
+
function restartNotePath(projectPath) {
|
|
37269
|
+
return join18(projectPath, ".godot", "gdharness-restart.json");
|
|
37270
|
+
}
|
|
37271
|
+
function noteRestartBegun(note) {
|
|
37272
|
+
const path = restartNotePath(note.projectPath);
|
|
36961
37273
|
try {
|
|
36962
|
-
|
|
36963
|
-
|
|
36964
|
-
|
|
36965
|
-
} catch {
|
|
37274
|
+
mkdirSync9(dirname12(path), { recursive: true });
|
|
37275
|
+
writeFileSync11(path, `${JSON.stringify(note, null, 2)}
|
|
37276
|
+
`, "utf8");
|
|
37277
|
+
} catch (error) {
|
|
37278
|
+
console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
|
|
37279
|
+
}
|
|
36966
37280
|
}
|
|
36967
|
-
|
|
36968
|
-
|
|
36969
|
-
|
|
36970
|
-
|
|
36971
|
-
|
|
36972
|
-
const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && resolve4(endpoint.project.path) === wanted && (waiting.accept?.(endpoint) ?? true));
|
|
36973
|
-
if (fresh !== undefined) {
|
|
36974
|
-
return fresh;
|
|
37281
|
+
function restartOwed(projectPath) {
|
|
37282
|
+
try {
|
|
37283
|
+
const parsed = JSON.parse(readFileSync16(restartNotePath(projectPath), "utf8"));
|
|
37284
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
37285
|
+
return null;
|
|
36975
37286
|
}
|
|
36976
|
-
const
|
|
36977
|
-
if (
|
|
37287
|
+
const said = parsed;
|
|
37288
|
+
if (typeof said.quitAt !== "string" || typeof said.byPid !== "number" || typeof said.ports !== "object") {
|
|
36978
37289
|
return null;
|
|
36979
37290
|
}
|
|
36980
|
-
|
|
37291
|
+
return said;
|
|
37292
|
+
} catch {
|
|
37293
|
+
return null;
|
|
36981
37294
|
}
|
|
36982
37295
|
}
|
|
36983
|
-
function
|
|
36984
|
-
|
|
36985
|
-
}
|
|
36986
|
-
|
|
36987
|
-
|
|
36988
|
-
const half = unspoken.some((game) => game.protocol > RUNTIME_PROTOCOL) ? "this server is the older half: reconnect it so it spawns the installed version" : "the addon is the older half: reinstall it and restart the game";
|
|
36989
|
-
return `A game is running, in a protocol this server does not speak ${named}. This server speaks ${RUNTIME_PROTOCOL}, so ${half}.`;
|
|
36990
|
-
}
|
|
36991
|
-
function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
|
|
36992
|
-
if (pid !== undefined) {
|
|
36993
|
-
const named = endpoints.find((endpoint) => endpoint.pid === pid);
|
|
36994
|
-
if (named !== undefined) {
|
|
36995
|
-
if (projectPath !== undefined && resolve4(named.project.path) !== resolve4(projectPath)) {
|
|
36996
|
-
return {
|
|
36997
|
-
problem: `The game with pid ${pid} is running ${named.project.path}, not ${resolve4(projectPath)}. Running: ${endpoints.map(describe2).join("; ")}.`
|
|
36998
|
-
};
|
|
36999
|
-
}
|
|
37000
|
-
return { endpoint: named };
|
|
37001
|
-
}
|
|
37002
|
-
const tooNewToo = unspoken.find((game) => game.pid === pid);
|
|
37003
|
-
if (tooNewToo !== undefined) {
|
|
37004
|
-
return { problem: tooNew([tooNewToo]) };
|
|
37005
|
-
}
|
|
37006
|
-
return {
|
|
37007
|
-
problem: endpoints.length === 0 ? `No game with the runtime addon is running, so there is none with pid ${pid}. Start one with editor_run, or play the project from the editor with the addon enabled.` : `No running game has pid ${pid}. Running: ${endpoints.map(describe2).join("; ")}. editor_status lists them under runtimes as they come and go.`
|
|
37008
|
-
};
|
|
37009
|
-
}
|
|
37010
|
-
if (endpoints.length === 0) {
|
|
37011
|
-
if (unspoken.length > 0) {
|
|
37012
|
-
return { problem: tooNew(unspoken) };
|
|
37013
|
-
}
|
|
37014
|
-
const went = announcedAndGone(projectPath);
|
|
37015
|
-
const first = went[0];
|
|
37016
|
-
if (first !== undefined) {
|
|
37017
|
-
const ago = Math.max(1, Math.round((Date.now() - first.noticedAt) / 1000));
|
|
37018
|
-
const others = went.length - 1;
|
|
37019
|
-
const earlier = others === 0 ? "" : ` ${others} earlier ${others === 1 ? "one" : "ones"} went the same way.`;
|
|
37020
|
-
return {
|
|
37021
|
-
problem: `A game with the runtime addon announced itself and its process is gone: pid ${first.pid}` + `${first.project === "" ? "" : ` for ${first.project}`}, noticed ${ago}s ago.${earlier}` + " It quit or was ended rather than never starting, so editor_output has what it printed" + " on the way, including whatever it broke on. Start another with editor_run once you have read it."
|
|
37022
|
-
};
|
|
37023
|
-
}
|
|
37024
|
-
return {
|
|
37025
|
-
problem: "No game with the runtime addon is running. Start one with editor_run, or play the project from the editor with the addon enabled."
|
|
37026
|
-
};
|
|
37027
|
-
}
|
|
37028
|
-
if (projectPath !== undefined) {
|
|
37029
|
-
const wanted = resolve4(projectPath);
|
|
37030
|
-
const matching = endpoints.filter((endpoint) => resolve4(endpoint.project.path) === wanted);
|
|
37031
|
-
if (matching.length === 0) {
|
|
37032
|
-
return {
|
|
37033
|
-
problem: `No running game is from ${wanted}. Running: ${endpoints.map(describe2).join("; ")}.`
|
|
37034
|
-
};
|
|
37035
|
-
}
|
|
37036
|
-
if (matching.length === 1 && matching[0]) {
|
|
37037
|
-
return { endpoint: matching[0] };
|
|
37038
|
-
}
|
|
37039
|
-
return {
|
|
37040
|
-
problem: `Several games are running from ${wanted}: ${matching.map(describe2).join("; ")}. Pass pid to choose one.`
|
|
37041
|
-
};
|
|
37042
|
-
}
|
|
37043
|
-
if (endpoints.length === 1 && endpoints[0]) {
|
|
37044
|
-
return { endpoint: endpoints[0] };
|
|
37296
|
+
function restartSettled(projectPath) {
|
|
37297
|
+
try {
|
|
37298
|
+
rmSync8(restartNotePath(projectPath), { force: true });
|
|
37299
|
+
} catch (error) {
|
|
37300
|
+
console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
|
|
37045
37301
|
}
|
|
37046
|
-
return {
|
|
37047
|
-
problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass projectPath to choose one, or pid when they are from one project.`
|
|
37048
|
-
};
|
|
37049
|
-
}
|
|
37050
|
-
function runtimeRequest(endpoint, command, params, timeoutMs) {
|
|
37051
|
-
const id = nextRequestId++;
|
|
37052
|
-
return new Promise((settle) => {
|
|
37053
|
-
let done = false;
|
|
37054
|
-
let buffered = "";
|
|
37055
|
-
let welcomed = false;
|
|
37056
|
-
const socket = createConnection3({ port: endpoint.port, host: endpoint.address });
|
|
37057
|
-
const finish = (reply) => {
|
|
37058
|
-
if (done) {
|
|
37059
|
-
return;
|
|
37060
|
-
}
|
|
37061
|
-
done = true;
|
|
37062
|
-
clearTimeout(timer);
|
|
37063
|
-
socket.destroy();
|
|
37064
|
-
settle(reply);
|
|
37065
|
-
};
|
|
37066
|
-
const timer = setTimeout(() => {
|
|
37067
|
-
finish({
|
|
37068
|
-
ok: false,
|
|
37069
|
-
reason: "busy",
|
|
37070
|
-
message: `The game (${describe2(endpoint)}) accepted the connection but did not answer '${command}' within ${timeoutMs}ms. It may be paused at a breakpoint or stuck in a long frame.`
|
|
37071
|
-
});
|
|
37072
|
-
}, timeoutMs);
|
|
37073
|
-
socket.setEncoding("utf8");
|
|
37074
|
-
socket.on("connect", () => {
|
|
37075
|
-
socket.write(`${JSON.stringify({ id, command, params })}
|
|
37076
|
-
`);
|
|
37077
|
-
});
|
|
37078
|
-
socket.on("data", (chunk) => {
|
|
37079
|
-
buffered += chunk;
|
|
37080
|
-
let newline = buffered.indexOf(`
|
|
37081
|
-
`);
|
|
37082
|
-
while (newline !== -1 && !done) {
|
|
37083
|
-
const line = buffered.slice(0, newline).trim();
|
|
37084
|
-
buffered = buffered.slice(newline + 1);
|
|
37085
|
-
newline = buffered.indexOf(`
|
|
37086
|
-
`);
|
|
37087
|
-
if (line === "") {
|
|
37088
|
-
continue;
|
|
37089
|
-
}
|
|
37090
|
-
let message;
|
|
37091
|
-
try {
|
|
37092
|
-
message = asParams(JSON.parse(line));
|
|
37093
|
-
} catch {
|
|
37094
|
-
continue;
|
|
37095
|
-
}
|
|
37096
|
-
if (!welcomed) {
|
|
37097
|
-
welcomed = true;
|
|
37098
|
-
if (readString(message, "type") === "welcome") {
|
|
37099
|
-
const spoken = readNumber(message, "protocol");
|
|
37100
|
-
if (spoken !== RUNTIME_PROTOCOL) {
|
|
37101
|
-
finish({
|
|
37102
|
-
ok: false,
|
|
37103
|
-
reason: "protocol",
|
|
37104
|
-
message: `The game (${describe2(endpoint)}) speaks runtime protocol ${spoken ?? "unknown"} and this server speaks ${RUNTIME_PROTOCOL}. Reinstall the runtime addon into the project.`
|
|
37105
|
-
});
|
|
37106
|
-
return;
|
|
37107
|
-
}
|
|
37108
|
-
continue;
|
|
37109
|
-
}
|
|
37110
|
-
}
|
|
37111
|
-
if (message["id"] !== id) {
|
|
37112
|
-
continue;
|
|
37113
|
-
}
|
|
37114
|
-
if (readString(message, "type") === "error") {
|
|
37115
|
-
finish({ ok: false, reason: "error", message: readString(message, "message") ?? "unknown error" });
|
|
37116
|
-
return;
|
|
37117
|
-
}
|
|
37118
|
-
finish({ ok: true, payload: message, endpoint });
|
|
37119
|
-
}
|
|
37120
|
-
});
|
|
37121
|
-
socket.on("error", (error) => {
|
|
37122
|
-
finish({
|
|
37123
|
-
ok: false,
|
|
37124
|
-
reason: "refused",
|
|
37125
|
-
message: `The game (${describe2(endpoint)}) is running but nothing answered on its port: ${error.code ?? error.message}. It may still be starting, or be shutting down.`
|
|
37126
|
-
});
|
|
37127
|
-
});
|
|
37128
|
-
socket.on("close", () => {
|
|
37129
|
-
finish({
|
|
37130
|
-
ok: false,
|
|
37131
|
-
reason: "refused",
|
|
37132
|
-
message: `The game (${describe2(endpoint)}) closed the connection before answering '${command}'.`
|
|
37133
|
-
});
|
|
37134
|
-
});
|
|
37135
|
-
});
|
|
37136
37302
|
}
|
|
37137
|
-
var
|
|
37138
|
-
|
|
37139
|
-
ANNOUNCEMENT_PATTERN = /^runtime-(\d+)\.json$/;
|
|
37140
|
-
ERROR_REPORT_PATTERN = /^runtime-(\d+)\.log$/;
|
|
37141
|
-
ERROR_REPORT_KEEP_MS = 60 * 60 * 1000;
|
|
37142
|
-
gone = [];
|
|
37303
|
+
var init_restart_note = __esm(() => {
|
|
37304
|
+
init_errors();
|
|
37143
37305
|
});
|
|
37144
37306
|
|
|
37145
37307
|
// src/run-record.ts
|
|
37146
|
-
import { execFileSync } from "node:child_process";
|
|
37147
|
-
import { mkdirSync as
|
|
37148
|
-
import { basename as basename3, join as
|
|
37308
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
37309
|
+
import { mkdirSync as mkdirSync10, openSync, readdirSync as readdirSync6, readFileSync as readFileSync17, rmSync as rmSync9, statSync as statSync4, writeFileSync as writeFileSync12 } from "node:fs";
|
|
37310
|
+
import { basename as basename3, join as join19 } from "node:path";
|
|
37149
37311
|
function runsDirectory() {
|
|
37150
|
-
return
|
|
37312
|
+
return join19(runtimeDirectory(), "runs");
|
|
37151
37313
|
}
|
|
37152
37314
|
function recordPath() {
|
|
37153
|
-
return
|
|
37315
|
+
return join19(runsDirectory(), "run.json");
|
|
37154
37316
|
}
|
|
37155
37317
|
function recordPaths() {
|
|
37156
|
-
return runtimeDirectories().map((directory) =>
|
|
37318
|
+
return runtimeDirectories().map((directory) => join19(directory, "runs", "run.json"));
|
|
37157
37319
|
}
|
|
37158
37320
|
function openTranscript(startedAt) {
|
|
37159
|
-
|
|
37160
|
-
const path =
|
|
37321
|
+
mkdirSync10(runsDirectory(), { recursive: true });
|
|
37322
|
+
const path = join19(runsDirectory(), `run-${startedAt}.log`);
|
|
37161
37323
|
return { path, fd: openSync(path, "a") };
|
|
37162
37324
|
}
|
|
37163
37325
|
function writeRunRecord(record) {
|
|
37164
|
-
|
|
37165
|
-
|
|
37326
|
+
mkdirSync10(runsDirectory(), { recursive: true });
|
|
37327
|
+
writeFileSync12(recordPath(), JSON.stringify(record, null, 2), "utf8");
|
|
37166
37328
|
}
|
|
37167
37329
|
function editorNotePath() {
|
|
37168
|
-
return
|
|
37330
|
+
return join19(runsDirectory(), "editor-run.json");
|
|
37169
37331
|
}
|
|
37170
37332
|
function writeEditorRunNote(note) {
|
|
37171
|
-
|
|
37172
|
-
|
|
37333
|
+
mkdirSync10(runsDirectory(), { recursive: true });
|
|
37334
|
+
writeFileSync12(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
|
|
37173
37335
|
}
|
|
37174
37336
|
function readEditorRunNote() {
|
|
37175
37337
|
for (const directory of runtimeDirectories()) {
|
|
37176
|
-
const note = editorNoteAt(
|
|
37338
|
+
const note = editorNoteAt(join19(directory, "runs", "editor-run.json"));
|
|
37177
37339
|
if (note !== null) {
|
|
37178
37340
|
return note;
|
|
37179
37341
|
}
|
|
@@ -37183,7 +37345,7 @@ function readEditorRunNote() {
|
|
|
37183
37345
|
function editorNoteAt(path) {
|
|
37184
37346
|
let parsed;
|
|
37185
37347
|
try {
|
|
37186
|
-
parsed = JSON.parse(
|
|
37348
|
+
parsed = JSON.parse(readFileSync17(path, "utf8"));
|
|
37187
37349
|
} catch {
|
|
37188
37350
|
return null;
|
|
37189
37351
|
}
|
|
@@ -37206,7 +37368,7 @@ function recordRunEnded(pid, exitCode) {
|
|
|
37206
37368
|
return;
|
|
37207
37369
|
}
|
|
37208
37370
|
try {
|
|
37209
|
-
|
|
37371
|
+
writeFileSync12(path, JSON.stringify({ ...record, exitCode }, null, 2), "utf8");
|
|
37210
37372
|
} catch {}
|
|
37211
37373
|
}
|
|
37212
37374
|
function readRunRecord() {
|
|
@@ -37221,7 +37383,7 @@ function readRunRecord() {
|
|
|
37221
37383
|
function recordAt(path) {
|
|
37222
37384
|
let parsed;
|
|
37223
37385
|
try {
|
|
37224
|
-
parsed = JSON.parse(
|
|
37386
|
+
parsed = JSON.parse(readFileSync17(path, "utf8"));
|
|
37225
37387
|
} catch {
|
|
37226
37388
|
return null;
|
|
37227
37389
|
}
|
|
@@ -37266,10 +37428,10 @@ function runningAs(pid) {
|
|
|
37266
37428
|
const began = startedAt(pid, null);
|
|
37267
37429
|
const when = began === null ? {} : { startedAt: began };
|
|
37268
37430
|
if (process.platform === "linux") {
|
|
37269
|
-
const raw =
|
|
37431
|
+
const raw = readFileSync17(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
37270
37432
|
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
37271
37433
|
}
|
|
37272
|
-
const args =
|
|
37434
|
+
const args = execFileSync2("ps", ["-p", String(pid), "-o", "args="], {
|
|
37273
37435
|
encoding: "utf8",
|
|
37274
37436
|
timeout: 15000
|
|
37275
37437
|
}).trim();
|
|
@@ -37281,20 +37443,14 @@ function runningAs(pid) {
|
|
|
37281
37443
|
function listeningPid(port) {
|
|
37282
37444
|
try {
|
|
37283
37445
|
if (process.platform === "win32") {
|
|
37284
|
-
|
|
37285
|
-
"-NoProfile",
|
|
37286
|
-
"-NonInteractive",
|
|
37287
|
-
"-Command",
|
|
37288
|
-
`(Get-NetTCPConnection -State Listen -LocalPort ${port} -ErrorAction SilentlyContinue | Select-Object -First 1).OwningProcess`
|
|
37289
|
-
], { encoding: "utf8", timeout: 15000 }).trim();
|
|
37290
|
-
return /^\d+$/.test(said) ? Number(said) : null;
|
|
37446
|
+
return listeningPidInNetstat(execFileSync2("netstat", ["-ano"], { encoding: "utf8", timeout: 15000 }), port);
|
|
37291
37447
|
}
|
|
37292
37448
|
for (const [command, args, pattern] of [
|
|
37293
37449
|
["lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], /^(\d+)/],
|
|
37294
37450
|
["ss", ["-ltnpH", `sport = :${port}`], /pid=(\d+)/]
|
|
37295
37451
|
]) {
|
|
37296
37452
|
try {
|
|
37297
|
-
const said =
|
|
37453
|
+
const said = execFileSync2(command, [...args], { encoding: "utf8", timeout: 15000 }).trim();
|
|
37298
37454
|
const found = pattern.exec(said);
|
|
37299
37455
|
if (found?.[1] !== undefined) {
|
|
37300
37456
|
return Number(found[1]);
|
|
@@ -37306,9 +37462,22 @@ function listeningPid(port) {
|
|
|
37306
37462
|
return null;
|
|
37307
37463
|
}
|
|
37308
37464
|
}
|
|
37465
|
+
function listeningPidInNetstat(printed, port) {
|
|
37466
|
+
for (const line of printed.split(/\r?\n/)) {
|
|
37467
|
+
const fields = /^\s*TCP\s+(\S+)\s+\S+\s+LISTENING\s+(\d+)\s*$/i.exec(line);
|
|
37468
|
+
if (fields === null) {
|
|
37469
|
+
continue;
|
|
37470
|
+
}
|
|
37471
|
+
const local = fields[1] ?? "";
|
|
37472
|
+
if (Number(local.slice(local.lastIndexOf(":") + 1)) === port) {
|
|
37473
|
+
return Number(fields[2]);
|
|
37474
|
+
}
|
|
37475
|
+
}
|
|
37476
|
+
return null;
|
|
37477
|
+
}
|
|
37309
37478
|
function windowsCommandLine(pid) {
|
|
37310
37479
|
try {
|
|
37311
|
-
const answer =
|
|
37480
|
+
const answer = execFileSync2("powershell", [
|
|
37312
37481
|
"-NoProfile",
|
|
37313
37482
|
"-NonInteractive",
|
|
37314
37483
|
"-Command",
|
|
@@ -37326,7 +37495,7 @@ function startedAt(pid, windowsAnswer) {
|
|
|
37326
37495
|
return Number.isNaN(when) ? null : when;
|
|
37327
37496
|
}
|
|
37328
37497
|
try {
|
|
37329
|
-
const said =
|
|
37498
|
+
const said = execFileSync2("ps", ["-p", String(pid), "-o", "lstart="], {
|
|
37330
37499
|
encoding: "utf8",
|
|
37331
37500
|
timeout: 15000
|
|
37332
37501
|
}).trim();
|
|
@@ -37338,7 +37507,7 @@ function startedAt(pid, windowsAnswer) {
|
|
|
37338
37507
|
}
|
|
37339
37508
|
function windowsImage(pid) {
|
|
37340
37509
|
try {
|
|
37341
|
-
const csv =
|
|
37510
|
+
const csv = execFileSync2("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
|
|
37342
37511
|
encoding: "utf8",
|
|
37343
37512
|
timeout: 15000,
|
|
37344
37513
|
windowsHide: true
|
|
@@ -37410,7 +37579,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
37410
37579
|
if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
|
|
37411
37580
|
continue;
|
|
37412
37581
|
}
|
|
37413
|
-
const path =
|
|
37582
|
+
const path = join19(directory, entry);
|
|
37414
37583
|
if (path === keep || ageMs(path, now) < keepMs) {
|
|
37415
37584
|
continue;
|
|
37416
37585
|
}
|
|
@@ -37433,15 +37602,16 @@ import {
|
|
|
37433
37602
|
closeSync,
|
|
37434
37603
|
existsSync as existsSync14,
|
|
37435
37604
|
fstatSync,
|
|
37436
|
-
mkdirSync as
|
|
37605
|
+
mkdirSync as mkdirSync11,
|
|
37437
37606
|
mkdtempSync as mkdtempSync3,
|
|
37438
37607
|
openSync as openSync2,
|
|
37439
37608
|
readdirSync as readdirSync7,
|
|
37440
|
-
readFileSync as
|
|
37441
|
-
readSync
|
|
37609
|
+
readFileSync as readFileSync18,
|
|
37610
|
+
readSync,
|
|
37611
|
+
statSync as statSync5
|
|
37442
37612
|
} from "node:fs";
|
|
37443
37613
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
37444
|
-
import { basename as basename4, dirname as
|
|
37614
|
+
import { basename as basename4, dirname as dirname13, join as join20, normalize as normalize3, relative as relative3 } from "node:path";
|
|
37445
37615
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
37446
37616
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
37447
37617
|
import { promisify as promisify7 } from "node:util";
|
|
@@ -37564,10 +37734,11 @@ function runtimeVerdict(endpoint, after) {
|
|
|
37564
37734
|
};
|
|
37565
37735
|
}
|
|
37566
37736
|
const state = after.playingSeen === false ? "the editor has not yet reported the game as playing, which a restarted editor does not until its scan is over" : "the game is still running";
|
|
37737
|
+
const sized = after.sizedToMs === undefined ? "" : `, half as long again as the ${after.sizedToMs}ms its last game took,`;
|
|
37567
37738
|
return {
|
|
37568
37739
|
listening: false,
|
|
37569
37740
|
mayYetAnnounce: after.running,
|
|
37570
|
-
note: after.running ? `nothing announced itself within ${after.budgetMs}ms and ${state}, so it may announce a moment from now: editor_status says whether it has, and editor_run start takes runtimeWaitMs to wait longer than this${after.withArgs ? ", which a run carrying its own arguments may well need, since whatever they ask the game to do before its first frame is inside this wait" : ""}` : `nothing announced itself within ${after.budgetMs}ms and the game is no longer running, so nothing is going to: editor_output has what it printed on the way down`,
|
|
37741
|
+
note: after.running ? `nothing announced itself within ${after.budgetMs}ms${sized} and ${state}, so it may announce a moment from now: editor_status says whether it has, and editor_run start takes runtimeWaitMs to wait longer than this${after.withArgs ? ", which a run carrying its own arguments may well need, since whatever they ask the game to do before its first frame is inside this wait" : ""}` : `nothing announced itself within ${after.budgetMs}ms${sized} and the game is no longer running, so nothing is going to: editor_output has what it printed on the way down`,
|
|
37571
37742
|
heldAt: null
|
|
37572
37743
|
};
|
|
37573
37744
|
}
|
|
@@ -37672,7 +37843,7 @@ function aboveTheRunner(entry) {
|
|
|
37672
37843
|
}
|
|
37673
37844
|
function elsewhereIn(projectPath, asked) {
|
|
37674
37845
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
37675
|
-
if (wanted === "" || existsSync14(
|
|
37846
|
+
if (wanted === "" || existsSync14(join20(projectPath, wanted))) {
|
|
37676
37847
|
return "";
|
|
37677
37848
|
}
|
|
37678
37849
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
@@ -37686,7 +37857,7 @@ function elsewhereIn(projectPath, asked) {
|
|
|
37686
37857
|
return near.length === 0 ? "" : ` The project does have ${there}.`;
|
|
37687
37858
|
}
|
|
37688
37859
|
function hasMainScene(projectFile) {
|
|
37689
|
-
const scene = parseProjectGodot(
|
|
37860
|
+
const scene = parseProjectGodot(readFileSync18(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
37690
37861
|
return typeof scene === "string" && scene !== "";
|
|
37691
37862
|
}
|
|
37692
37863
|
function runtimeAddonAnnouncesEditors(projectPath) {
|
|
@@ -37694,13 +37865,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
|
|
|
37694
37865
|
return false;
|
|
37695
37866
|
}
|
|
37696
37867
|
try {
|
|
37697
|
-
return
|
|
37868
|
+
return readFileSync18(join20(projectPath, RUNTIME_AUTOLOAD2.path), "utf8").includes('"editor_pid"');
|
|
37698
37869
|
} catch {
|
|
37699
37870
|
return false;
|
|
37700
37871
|
}
|
|
37701
37872
|
}
|
|
37702
37873
|
function editorPlaysHeadless(projectFile) {
|
|
37703
|
-
const runArgs = parseProjectGodot(
|
|
37874
|
+
const runArgs = parseProjectGodot(readFileSync18(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
37704
37875
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
37705
37876
|
}
|
|
37706
37877
|
function camelCased(params) {
|
|
@@ -37716,7 +37887,7 @@ function camelCased(params) {
|
|
|
37716
37887
|
class GodotServer {
|
|
37717
37888
|
mcp;
|
|
37718
37889
|
locator = new GodotLocator2;
|
|
37719
|
-
operationsScript =
|
|
37890
|
+
operationsScript = join20(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
37720
37891
|
godotBridge;
|
|
37721
37892
|
tools = buildToolDefinitions();
|
|
37722
37893
|
activeProcess = null;
|
|
@@ -38374,7 +38545,7 @@ class GodotServer {
|
|
|
38374
38545
|
if (path === undefined) {
|
|
38375
38546
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
38376
38547
|
}
|
|
38377
|
-
const file =
|
|
38548
|
+
const file = join20(path, "project.godot");
|
|
38378
38549
|
if (!existsSync14(file)) {
|
|
38379
38550
|
return {
|
|
38380
38551
|
ok: false,
|
|
@@ -38598,7 +38769,7 @@ class GodotServer {
|
|
|
38598
38769
|
if (!engine.ok) {
|
|
38599
38770
|
return engine.response;
|
|
38600
38771
|
}
|
|
38601
|
-
const application = parseProjectGodot(
|
|
38772
|
+
const application = parseProjectGodot(readFileSync18(project.value.file, "utf8"))["application"] ?? {};
|
|
38602
38773
|
const name = application["config/name"];
|
|
38603
38774
|
const mainScene = application["run/main_scene"];
|
|
38604
38775
|
const info = {
|
|
@@ -38659,18 +38830,18 @@ class GodotServer {
|
|
|
38659
38830
|
if (!engine.ok) {
|
|
38660
38831
|
return engine.response;
|
|
38661
38832
|
}
|
|
38662
|
-
const folder =
|
|
38833
|
+
const folder = dirname13(output.absolutePath);
|
|
38663
38834
|
try {
|
|
38664
|
-
|
|
38835
|
+
mkdirSync11(folder, { recursive: true });
|
|
38665
38836
|
} catch (error) {
|
|
38666
|
-
return this.createErrorResponse(`Cannot create ${
|
|
38837
|
+
return this.createErrorResponse(`Cannot create ${dirname13(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
38667
38838
|
}
|
|
38668
38839
|
const debug = readBoolean(args, "debug") ?? false;
|
|
38669
|
-
const exportLogs = mkdtempSync3(
|
|
38840
|
+
const exportLogs = mkdtempSync3(join20(tmpdir5(), "gdharness-export-"));
|
|
38670
38841
|
const exportArgs = [
|
|
38671
38842
|
"--headless",
|
|
38672
38843
|
"--log-file",
|
|
38673
|
-
|
|
38844
|
+
join20(exportLogs, "engine.log"),
|
|
38674
38845
|
"--path",
|
|
38675
38846
|
project.value.path,
|
|
38676
38847
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -38728,7 +38899,7 @@ class GodotServer {
|
|
|
38728
38899
|
return contained.response;
|
|
38729
38900
|
}
|
|
38730
38901
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
38731
|
-
if (!existsSync14(
|
|
38902
|
+
if (!existsSync14(join20(project.value.path, runner))) {
|
|
38732
38903
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
38733
38904
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
38734
38905
|
]);
|
|
@@ -38763,7 +38934,7 @@ class GodotServer {
|
|
|
38763
38934
|
];
|
|
38764
38935
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
38765
38936
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
38766
|
-
const userData = mkdtempSync3(
|
|
38937
|
+
const userData = mkdtempSync3(join20(tmpdir5(), "gdharness-tests-"));
|
|
38767
38938
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
38768
38939
|
const hung = await new Promise((resolve) => {
|
|
38769
38940
|
const timer = setTimeout(() => {
|
|
@@ -38779,14 +38950,14 @@ class GodotServer {
|
|
|
38779
38950
|
resolve(false);
|
|
38780
38951
|
});
|
|
38781
38952
|
});
|
|
38782
|
-
const reportsDir =
|
|
38953
|
+
const reportsDir = join20(project.value.path, ".godot", ...ours.split("/"));
|
|
38783
38954
|
let report = null;
|
|
38784
38955
|
let reportProblem = null;
|
|
38785
38956
|
try {
|
|
38786
38957
|
const written = existsSync14(reportsDir) ? readdirSync7(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
38787
38958
|
const newest = written.at(-1);
|
|
38788
38959
|
if (newest !== undefined) {
|
|
38789
|
-
report = parseJUnit(
|
|
38960
|
+
report = parseJUnit(readFileSync18(join20(reportsDir, newest, "results.xml"), "utf8"));
|
|
38790
38961
|
}
|
|
38791
38962
|
} catch (error) {
|
|
38792
38963
|
reportProblem = errorMessage(error);
|
|
@@ -38926,7 +39097,7 @@ class GodotServer {
|
|
|
38926
39097
|
return null;
|
|
38927
39098
|
}
|
|
38928
39099
|
try {
|
|
38929
|
-
return
|
|
39100
|
+
return readFileSync18(contained.absolutePath, "utf8");
|
|
38930
39101
|
} catch {
|
|
38931
39102
|
return null;
|
|
38932
39103
|
}
|
|
@@ -38968,7 +39139,7 @@ class GodotServer {
|
|
|
38968
39139
|
return null;
|
|
38969
39140
|
}
|
|
38970
39141
|
try {
|
|
38971
|
-
return
|
|
39142
|
+
return readFileSync18(contained.absolutePath, "utf8");
|
|
38972
39143
|
} catch {
|
|
38973
39144
|
return null;
|
|
38974
39145
|
}
|
|
@@ -39117,11 +39288,14 @@ class GodotServer {
|
|
|
39117
39288
|
return null;
|
|
39118
39289
|
}
|
|
39119
39290
|
try {
|
|
39120
|
-
const
|
|
39121
|
-
|
|
39122
|
-
|
|
39123
|
-
]);
|
|
39291
|
+
const askedAt = Date.now();
|
|
39292
|
+
const asked = this.godotBridge.invokeTool("playing_status", {});
|
|
39293
|
+
const answer = await Promise.race([asked, delay3(PLAYING_STATUS_MS).then(() => null)]);
|
|
39124
39294
|
if (answer === null) {
|
|
39295
|
+
this.logDebug(`playing_status did not answer within ${PLAYING_STATUS_MS}ms`);
|
|
39296
|
+
asked.then(() => {
|
|
39297
|
+
this.logDebug(`playing_status answered after ${Date.now() - askedAt}ms`);
|
|
39298
|
+
}).catch(() => {});
|
|
39125
39299
|
return null;
|
|
39126
39300
|
}
|
|
39127
39301
|
return {
|
|
@@ -39194,7 +39368,7 @@ class GodotServer {
|
|
|
39194
39368
|
return null;
|
|
39195
39369
|
}
|
|
39196
39370
|
try {
|
|
39197
|
-
return settingKeys(
|
|
39371
|
+
return settingKeys(readFileSync18(join20(projectPath, "project.godot"), "utf8"));
|
|
39198
39372
|
} catch {
|
|
39199
39373
|
return null;
|
|
39200
39374
|
}
|
|
@@ -39401,7 +39575,7 @@ class GodotServer {
|
|
|
39401
39575
|
platform: process.platform,
|
|
39402
39576
|
variables: process.env
|
|
39403
39577
|
});
|
|
39404
|
-
const runtimeWaitMs =
|
|
39578
|
+
const runtimeWaitMs = this.announceBudget(args, project.value.path);
|
|
39405
39579
|
const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
|
|
39406
39580
|
if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0) {
|
|
39407
39581
|
return await this.playThroughEditor(sceneArgument, refreshed.value, project.value.path, alreadyPlaying, runtimeWaitMs, ended);
|
|
@@ -39434,13 +39608,17 @@ class GodotServer {
|
|
|
39434
39608
|
message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}${alsoRunning}`
|
|
39435
39609
|
});
|
|
39436
39610
|
}
|
|
39437
|
-
async runtimeUp(projectPath, before,
|
|
39438
|
-
|
|
39611
|
+
async runtimeUp(projectPath, before, budget, withArgs) {
|
|
39612
|
+
const budgetMs = budget.ms;
|
|
39613
|
+
if (!existsSync14(join20(projectPath, RUNTIME_AUTOLOAD2.path))) {
|
|
39439
39614
|
return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
|
|
39440
39615
|
}
|
|
39441
39616
|
let asking = null;
|
|
39442
39617
|
let editorAskedAt = 0;
|
|
39443
39618
|
let editorSaysGoing = true;
|
|
39619
|
+
let editorAsks = 0;
|
|
39620
|
+
const waitBegan = Date.now();
|
|
39621
|
+
const lastSaid = () => editorSaysGoing ? "playing" : "not playing";
|
|
39444
39622
|
const endpoint = await announcedSince(projectPath, before, {
|
|
39445
39623
|
budgetMs,
|
|
39446
39624
|
accept: (one) => this.isTheGameOf(this.currentRun(), one, projectPath),
|
|
@@ -39454,6 +39632,7 @@ class GodotServer {
|
|
|
39454
39632
|
}
|
|
39455
39633
|
if (asking === null && Date.now() - editorAskedAt >= RUN_POLL_MS) {
|
|
39456
39634
|
editorAskedAt = Date.now();
|
|
39635
|
+
editorAsks += 1;
|
|
39457
39636
|
asking = this.runStillGoing(run).then((going) => {
|
|
39458
39637
|
editorSaysGoing = going;
|
|
39459
39638
|
}).finally(() => {
|
|
@@ -39463,19 +39642,46 @@ class GodotServer {
|
|
|
39463
39642
|
return !editorSaysGoing;
|
|
39464
39643
|
}
|
|
39465
39644
|
});
|
|
39645
|
+
this.logDebug(`announce wait ended after ${Date.now() - waitBegan}ms: ${endpoint === null ? "nothing announced" : `pid ${endpoint.pid} announced`}, editor asked ${editorAsks} times, last said ${lastSaid()}`);
|
|
39466
39646
|
const going = this.currentRun();
|
|
39467
39647
|
if (endpoint !== null && going !== null && going.announcedPid === undefined) {
|
|
39468
39648
|
going.announcedPid = endpoint.pid;
|
|
39649
|
+
this.noteBootOf(going, endpoint);
|
|
39469
39650
|
}
|
|
39470
39651
|
return runtimeVerdict(endpoint, {
|
|
39471
39652
|
addon: true,
|
|
39472
39653
|
budgetMs,
|
|
39654
|
+
...budget.sizedToMs === undefined ? {} : { sizedToMs: budget.sizedToMs },
|
|
39473
39655
|
heldAt: this.dapClient?.whereItStopped() ?? null,
|
|
39474
39656
|
running: endpoint !== null || going !== null && await this.runStillGoing(going),
|
|
39475
39657
|
withArgs,
|
|
39476
39658
|
...going?.throughEditor === true ? { playingSeen: going.seenPlaying === true } : {}
|
|
39477
39659
|
});
|
|
39478
39660
|
}
|
|
39661
|
+
announceBudget(args, projectPath) {
|
|
39662
|
+
const asked = readNonNegativeNumber(args, "runtimeWaitMs");
|
|
39663
|
+
if (asked !== undefined) {
|
|
39664
|
+
return { ms: asked };
|
|
39665
|
+
}
|
|
39666
|
+
const lastBoot = readBootNote(projectPath);
|
|
39667
|
+
const ms = waitSizedTo(lastBoot);
|
|
39668
|
+
return lastBoot !== null && ms > ANNOUNCE_BUDGET_MS ? { ms, sizedToMs: lastBoot } : { ms };
|
|
39669
|
+
}
|
|
39670
|
+
noteBootOf(run, endpoint) {
|
|
39671
|
+
if (run.projectPath === null) {
|
|
39672
|
+
return;
|
|
39673
|
+
}
|
|
39674
|
+
let announcedAt;
|
|
39675
|
+
try {
|
|
39676
|
+
announcedAt = statSync5(endpoint.file).mtimeMs;
|
|
39677
|
+
} catch {
|
|
39678
|
+
return;
|
|
39679
|
+
}
|
|
39680
|
+
const took = announcedAt - run.startedAt;
|
|
39681
|
+
if (took > 0 && took <= LONGEST_BOOT_NOTED_MS) {
|
|
39682
|
+
writeBootNote(run.projectPath, took);
|
|
39683
|
+
}
|
|
39684
|
+
}
|
|
39479
39685
|
async playThroughEditor(scene, refreshedClasses, projectPath, alreadyPlaying, runtimeWaitMs, ended) {
|
|
39480
39686
|
const log = new GameLog;
|
|
39481
39687
|
try {
|
|
@@ -39805,6 +40011,7 @@ class GodotServer {
|
|
|
39805
40011
|
const theOne = candidates.length === 1 ? candidates[0] : undefined;
|
|
39806
40012
|
if (theOne !== undefined) {
|
|
39807
40013
|
run.announcedPid = theOne.pid;
|
|
40014
|
+
this.noteBootOf(run, theOne);
|
|
39808
40015
|
}
|
|
39809
40016
|
return run.announcedPid;
|
|
39810
40017
|
}
|
|
@@ -40139,11 +40346,19 @@ class GodotServer {
|
|
|
40139
40346
|
this.drainEditorOutput(stopped);
|
|
40140
40347
|
this.drainTranscript(stopped);
|
|
40141
40348
|
const wasRunning = await this.runStillGoing(stopped);
|
|
40142
|
-
const
|
|
40349
|
+
const announcedPid = this.announcedPidOf(stopped) ?? null;
|
|
40350
|
+
const endedPid = stopped.pid ?? announcedPid;
|
|
40143
40351
|
const children = readBoolean(args, "andChildren") === true && wasRunning ? await this.whatTheRunStarted(stopped) : null;
|
|
40144
40352
|
this.logDebug("Stopping the running game");
|
|
40145
40353
|
await this.endActiveGame("editor_run stop");
|
|
40146
40354
|
const ended = children === null ? null : endChildrenAmong(children);
|
|
40355
|
+
if (wasRunning) {
|
|
40356
|
+
for (const pid of new Set([endedPid, announcedPid])) {
|
|
40357
|
+
if (pid !== null) {
|
|
40358
|
+
await announcementEnded(pid);
|
|
40359
|
+
}
|
|
40360
|
+
}
|
|
40361
|
+
}
|
|
40147
40362
|
return this.jsonTextResponse({
|
|
40148
40363
|
stopped: true,
|
|
40149
40364
|
through: stopped.throughEditor ? "editor" : "gdharness",
|
|
@@ -40465,8 +40680,8 @@ class GodotServer {
|
|
|
40465
40680
|
]);
|
|
40466
40681
|
}
|
|
40467
40682
|
const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
|
|
40468
|
-
const screenshotDir = expectsScreenshot ? mkdtempSync3(
|
|
40469
|
-
const screenshotPath = screenshotDir ?
|
|
40683
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync3(join20(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
|
|
40684
|
+
const screenshotPath = screenshotDir ? join20(screenshotDir, "capture.png") : null;
|
|
40470
40685
|
try {
|
|
40471
40686
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
|
|
40472
40687
|
if (!reply.ok) {
|
|
@@ -40490,7 +40705,7 @@ class GodotServer {
|
|
|
40490
40705
|
type: "text",
|
|
40491
40706
|
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}`
|
|
40492
40707
|
},
|
|
40493
|
-
{ type: "image", data:
|
|
40708
|
+
{ type: "image", data: readFileSync18(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
40494
40709
|
]
|
|
40495
40710
|
};
|
|
40496
40711
|
} finally {
|
|
@@ -40557,11 +40772,12 @@ async function runGodotServer() {
|
|
|
40557
40772
|
const server = new GodotServer;
|
|
40558
40773
|
await server.run();
|
|
40559
40774
|
}
|
|
40560
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run7, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, CONSOLE_WRAPPER, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CACHE_WRITE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
40775
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run7, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CACHE_WRITE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
40561
40776
|
var init_server3 = __esm(() => {
|
|
40562
40777
|
init_mcp();
|
|
40563
40778
|
init_stdio2();
|
|
40564
40779
|
init_types();
|
|
40780
|
+
init_boot_note();
|
|
40565
40781
|
init_breakpoint_note();
|
|
40566
40782
|
init_bridge_announce();
|
|
40567
40783
|
init_class_cache();
|
|
@@ -40591,8 +40807,9 @@ var init_server3 = __esm(() => {
|
|
|
40591
40807
|
init_tool_definitions();
|
|
40592
40808
|
init_update_check();
|
|
40593
40809
|
run7 = promisify7(execFile7);
|
|
40594
|
-
__dirname2 =
|
|
40810
|
+
__dirname2 = dirname13(fileURLToPath4(import.meta.url));
|
|
40595
40811
|
CONSOLE_WRAPPER = /_console(\.exe)?$/i;
|
|
40812
|
+
LONGEST_BOOT_NOTED_MS = 5 * 60000;
|
|
40596
40813
|
VALUE_NAMES = {
|
|
40597
40814
|
string: "a string",
|
|
40598
40815
|
number: "a number",
|
|
@@ -40631,7 +40848,7 @@ var init_server3 = __esm(() => {
|
|
|
40631
40848
|
|
|
40632
40849
|
// src/cli.ts
|
|
40633
40850
|
import { existsSync as existsSync15 } from "fs";
|
|
40634
|
-
import { join as
|
|
40851
|
+
import { join as join21, resolve as resolve5 } from "path";
|
|
40635
40852
|
|
|
40636
40853
|
// src/errors.ts
|
|
40637
40854
|
class Refusal extends Error {
|
|
@@ -42142,6 +42359,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
42142
42359
|
| Where a control is, and whether it is visible | \`runtime_inspect\` \`find\`, \`rect\` |
|
|
42143
42360
|
| What a property reads right now, on a node whose path you already hold | \`runtime_inspect\` \`property\`, \`runtime_invoke\` |
|
|
42144
42361
|
| What a property on an object a node holds reads | the same two, with colons: \`_game:clock:speed\` |
|
|
42362
|
+
| Which control holds the focus, or anything only a method answers | the same path with a step written as a call, \`get_viewport():gui_get_focus_owner\`, which calls a method taking no arguments and walks into what it returned |
|
|
42145
42363
|
| Press a button | \`runtime_input click\`, which says what was under the pointer |
|
|
42146
42364
|
| Press the next thing after a click that changed screen | \`runtime_wait until\` with \`says\` and a word the new screen shows, then the click. A click sent straight after a screen change lands on a control the new screen is about to free, answers \`landed: true\`, and does nothing; a frame count is a number nobody can pick correctly across a cold start and a warm one, and waiting for the words answered in about 130ms where twenty frames had not been enough |
|
|
42147
42365
|
| Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\` with \`replace\`; a submitted field has to be clicked again |
|
|
@@ -42335,7 +42553,7 @@ class UsageError extends Refusal {
|
|
|
42335
42553
|
function projectArgument(at) {
|
|
42336
42554
|
const named = args.slice(at).find((value) => !value.startsWith("--"));
|
|
42337
42555
|
const projectPath = resolve5(named ?? ".");
|
|
42338
|
-
if (!existsSync15(
|
|
42556
|
+
if (!existsSync15(join21(projectPath, "project.godot"))) {
|
|
42339
42557
|
throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
|
|
42340
42558
|
}
|
|
42341
42559
|
return projectPath;
|
|
@@ -42374,7 +42592,7 @@ async function registerRuntime(godot, projectPath) {
|
|
|
42374
42592
|
return;
|
|
42375
42593
|
}
|
|
42376
42594
|
console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
|
|
42377
|
-
if (!existsSync15(
|
|
42595
|
+
if (!existsSync15(join21(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
|
|
42378
42596
|
console.log(` that file is not in this project, so it boots with a missing script: restore it, or point the entry at res://${RUNTIME_AUTOLOAD.path}`);
|
|
42379
42597
|
}
|
|
42380
42598
|
}
|