gdharness 1.0.29 → 1.0.31
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 +444 -186
- package/build/index.js +441 -188
- package/build/keeper.js +272 -0
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -10183,7 +10183,7 @@ function defectReport(where, error, context = {}) {
|
|
|
10183
10183
|
}
|
|
10184
10184
|
|
|
10185
10185
|
// src/server.ts
|
|
10186
|
-
import { execFile as execFile5, spawn } from "node:child_process";
|
|
10186
|
+
import { execFile as execFile5, spawn as spawn2 } from "node:child_process";
|
|
10187
10187
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
10188
10188
|
import {
|
|
10189
10189
|
appendFileSync,
|
|
@@ -10202,9 +10202,9 @@ import {
|
|
|
10202
10202
|
statSync as statSync7
|
|
10203
10203
|
} from "node:fs";
|
|
10204
10204
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
10205
|
-
import { basename as basename3, dirname as
|
|
10205
|
+
import { basename as basename3, dirname as dirname11, extname as extname2, isAbsolute as isAbsolute3, join as join16, normalize as normalize2, relative as relative3 } from "node:path";
|
|
10206
10206
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
10207
|
-
import { fileURLToPath as
|
|
10207
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
10208
10208
|
import { promisify as promisify5 } from "node:util";
|
|
10209
10209
|
|
|
10210
10210
|
// node_modules/zod/v3/helpers/util.js
|
|
@@ -25042,7 +25042,9 @@ function ancestorsIn(tree, pid, root) {
|
|
|
25042
25042
|
return chain;
|
|
25043
25043
|
}
|
|
25044
25044
|
function readCommandLine(command) {
|
|
25045
|
-
const
|
|
25045
|
+
const all = commandWords(command);
|
|
25046
|
+
const separator = all.findIndex((word) => word === "--" || word === "++");
|
|
25047
|
+
const words = separator === -1 ? all : all.slice(0, separator);
|
|
25046
25048
|
const executable = words[0] === undefined ? "" : basename(words[0].replaceAll("\\", "/"));
|
|
25047
25049
|
const at = words.indexOf("--path");
|
|
25048
25050
|
let projectPath = null;
|
|
@@ -25054,7 +25056,7 @@ function readCommandLine(command) {
|
|
|
25054
25056
|
}
|
|
25055
25057
|
taken.push(word);
|
|
25056
25058
|
}
|
|
25057
|
-
projectPath = taken.join(" ");
|
|
25059
|
+
projectPath = taken.join(" ").replaceAll("%20", " ");
|
|
25058
25060
|
}
|
|
25059
25061
|
return {
|
|
25060
25062
|
executable,
|
|
@@ -25216,8 +25218,13 @@ function parseAnnouncement(file, pid) {
|
|
|
25216
25218
|
name: readString(project, "name") ?? "",
|
|
25217
25219
|
path: readString(project, "path") ?? ""
|
|
25218
25220
|
};
|
|
25221
|
+
let announcedAt;
|
|
25222
|
+
try {
|
|
25223
|
+
announcedAt = statSync(file).mtimeMs;
|
|
25224
|
+
} catch {}
|
|
25225
|
+
const when = announcedAt === undefined ? {} : { announcedAt };
|
|
25219
25226
|
if (protocol !== RUNTIME_PROTOCOL) {
|
|
25220
|
-
return { kind: "unspoken", unspoken: { pid, protocol: protocol ?? 0, project: named } };
|
|
25227
|
+
return { kind: "unspoken", unspoken: { pid, protocol: protocol ?? 0, project: named, ...when } };
|
|
25221
25228
|
}
|
|
25222
25229
|
const editorPid = readNumber(fields, "editor_pid");
|
|
25223
25230
|
return {
|
|
@@ -25228,7 +25235,8 @@ function parseAnnouncement(file, pid) {
|
|
|
25228
25235
|
address,
|
|
25229
25236
|
project: named,
|
|
25230
25237
|
file,
|
|
25231
|
-
...editorPid !== undefined && Number.isInteger(editorPid) && editorPid > 0 ? { editorPid } : {}
|
|
25238
|
+
...editorPid !== undefined && Number.isInteger(editorPid) && editorPid > 0 ? { editorPid } : {},
|
|
25239
|
+
...when
|
|
25232
25240
|
}
|
|
25233
25241
|
};
|
|
25234
25242
|
}
|
|
@@ -28884,6 +28892,69 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
28884
28892
|
}
|
|
28885
28893
|
}
|
|
28886
28894
|
|
|
28895
|
+
// src/outside.ts
|
|
28896
|
+
import { spawn } from "node:child_process";
|
|
28897
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
28898
|
+
function changesFrom(env, base) {
|
|
28899
|
+
const changes = {};
|
|
28900
|
+
if (env === undefined) {
|
|
28901
|
+
return changes;
|
|
28902
|
+
}
|
|
28903
|
+
for (const [name, value] of Object.entries(env)) {
|
|
28904
|
+
if (value !== undefined && base[name] !== value) {
|
|
28905
|
+
changes[name] = value;
|
|
28906
|
+
}
|
|
28907
|
+
}
|
|
28908
|
+
for (const name of Object.keys(base)) {
|
|
28909
|
+
if (env[name] === undefined) {
|
|
28910
|
+
changes[name] = null;
|
|
28911
|
+
}
|
|
28912
|
+
}
|
|
28913
|
+
return changes;
|
|
28914
|
+
}
|
|
28915
|
+
function sendSpec(child, spec) {
|
|
28916
|
+
child.stdin?.end(JSON.stringify(spec), "utf8");
|
|
28917
|
+
}
|
|
28918
|
+
var KEEPER_SCRIPT = fileURLToPath2(new URL("./keeper.js", import.meta.url));
|
|
28919
|
+
function readLaunched(printed) {
|
|
28920
|
+
const line = printed.split(`
|
|
28921
|
+
`).find((one) => one.trim() !== "")?.trim() ?? "";
|
|
28922
|
+
const pid = /^pid (\d+)$/.exec(line);
|
|
28923
|
+
if (pid?.[1] !== undefined) {
|
|
28924
|
+
return { pid: Number(pid[1]) };
|
|
28925
|
+
}
|
|
28926
|
+
return { error: line.replace(/^error /, "") || "the launcher said nothing" };
|
|
28927
|
+
}
|
|
28928
|
+
async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = KEEPER_SCRIPT) {
|
|
28929
|
+
return await new Promise((resolve) => {
|
|
28930
|
+
const launcher = spawn(runtime, [keeper, "launch"], {
|
|
28931
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
28932
|
+
windowsHide: true
|
|
28933
|
+
});
|
|
28934
|
+
sendSpec(launcher, {
|
|
28935
|
+
command: spec.command,
|
|
28936
|
+
args: spec.args,
|
|
28937
|
+
envChanges: changesFrom(spec.env, process.env),
|
|
28938
|
+
...spec.run === undefined ? {} : { run: spec.run }
|
|
28939
|
+
});
|
|
28940
|
+
let printed = "";
|
|
28941
|
+
let complained = "";
|
|
28942
|
+
launcher.stdout.on("data", (chunk) => {
|
|
28943
|
+
printed += chunk.toString();
|
|
28944
|
+
});
|
|
28945
|
+
launcher.stderr.on("data", (chunk) => {
|
|
28946
|
+
complained += chunk.toString();
|
|
28947
|
+
});
|
|
28948
|
+
launcher.once("error", (error) => {
|
|
28949
|
+
resolve({ error: `the launcher did not start: ${error.message}` });
|
|
28950
|
+
});
|
|
28951
|
+
launcher.once("close", () => {
|
|
28952
|
+
const launched = readLaunched(printed);
|
|
28953
|
+
resolve("error" in launched && complained.trim() !== "" ? { error: `${launched.error}: ${complained.trim()}` } : launched);
|
|
28954
|
+
});
|
|
28955
|
+
});
|
|
28956
|
+
}
|
|
28957
|
+
|
|
28887
28958
|
// src/process-time.ts
|
|
28888
28959
|
import { execFile as execFile4 } from "node:child_process";
|
|
28889
28960
|
import { promisify as promisify4 } from "node:util";
|
|
@@ -29331,37 +29402,62 @@ function restartSettled(projectPath) {
|
|
|
29331
29402
|
|
|
29332
29403
|
// src/run-record.ts
|
|
29333
29404
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
29334
|
-
import {
|
|
29335
|
-
import {
|
|
29405
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
29406
|
+
import {
|
|
29407
|
+
mkdirSync as mkdirSync8,
|
|
29408
|
+
openSync as openSync2,
|
|
29409
|
+
readdirSync as readdirSync5,
|
|
29410
|
+
readFileSync as readFileSync14,
|
|
29411
|
+
realpathSync as realpathSync3,
|
|
29412
|
+
rmSync as rmSync6,
|
|
29413
|
+
statSync as statSync5,
|
|
29414
|
+
writeFileSync as writeFileSync9
|
|
29415
|
+
} from "node:fs";
|
|
29416
|
+
import { basename as basename2, dirname as dirname9, join as join14, resolve as resolve5 } from "node:path";
|
|
29336
29417
|
function runsDirectory() {
|
|
29337
29418
|
return join14(runtimeDirectory(), "runs");
|
|
29338
29419
|
}
|
|
29339
|
-
function
|
|
29340
|
-
|
|
29420
|
+
function realAsFarAsItExists(path) {
|
|
29421
|
+
const missing = [];
|
|
29422
|
+
let at = path;
|
|
29423
|
+
for (;; ) {
|
|
29424
|
+
try {
|
|
29425
|
+
return join14(realpathSync3.native(at), ...missing);
|
|
29426
|
+
} catch {
|
|
29427
|
+
const parent = dirname9(at);
|
|
29428
|
+
if (parent === at) {
|
|
29429
|
+
return path;
|
|
29430
|
+
}
|
|
29431
|
+
missing.unshift(basename2(at));
|
|
29432
|
+
at = parent;
|
|
29433
|
+
}
|
|
29434
|
+
}
|
|
29435
|
+
}
|
|
29436
|
+
function projectKey(projectPath) {
|
|
29437
|
+
const real = realAsFarAsItExists(resolve5(projectPath)).replace(/[\\/]+$/, "");
|
|
29438
|
+
const folded = process.platform === "win32" || process.platform === "darwin" ? real.toLowerCase() : real;
|
|
29439
|
+
return createHash3("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
|
|
29341
29440
|
}
|
|
29342
|
-
function
|
|
29343
|
-
return
|
|
29441
|
+
function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
29442
|
+
return join14(directory, "runs", `note-${projectKey(projectPath)}.json`);
|
|
29344
29443
|
}
|
|
29444
|
+
var SHARED_NOTE = "run.json";
|
|
29345
29445
|
function openTranscript(startedAt) {
|
|
29346
29446
|
mkdirSync8(runsDirectory(), { recursive: true });
|
|
29347
29447
|
const path = join14(runsDirectory(), `run-${startedAt}.log`);
|
|
29348
29448
|
return { path, fd: openSync2(path, "a") };
|
|
29349
29449
|
}
|
|
29350
|
-
function
|
|
29351
|
-
|
|
29352
|
-
writeFileSync9(recordPath(), JSON.stringify(record, null, 2), "utf8");
|
|
29353
|
-
}
|
|
29354
|
-
function editorNotePath() {
|
|
29355
|
-
return join14(runsDirectory(), "editor-run.json");
|
|
29450
|
+
function editorNotePath(projectPath, directory = runtimeDirectory()) {
|
|
29451
|
+
return join14(directory, "runs", `editor-${projectKey(projectPath)}.json`);
|
|
29356
29452
|
}
|
|
29357
29453
|
function writeEditorRunNote(note) {
|
|
29358
29454
|
mkdirSync8(runsDirectory(), { recursive: true });
|
|
29359
|
-
writeFileSync9(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
|
|
29455
|
+
writeFileSync9(editorNotePath(note.projectPath), JSON.stringify(note, null, 2), "utf8");
|
|
29360
29456
|
}
|
|
29361
|
-
function readEditorRunNote() {
|
|
29457
|
+
function readEditorRunNote(projectPath) {
|
|
29362
29458
|
for (const directory of runtimeDirectories()) {
|
|
29363
|
-
const note = editorNoteAt(join14(directory, "runs", "editor-run.json"));
|
|
29364
|
-
if (note !== null) {
|
|
29459
|
+
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join14(directory, "runs", "editor-run.json"));
|
|
29460
|
+
if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
|
|
29365
29461
|
return note;
|
|
29366
29462
|
}
|
|
29367
29463
|
}
|
|
@@ -29386,29 +29482,38 @@ function editorNoteAt(path) {
|
|
|
29386
29482
|
}
|
|
29387
29483
|
return { projectPath, transcript, startedAt };
|
|
29388
29484
|
}
|
|
29389
|
-
function
|
|
29390
|
-
const
|
|
29391
|
-
|
|
29392
|
-
|
|
29393
|
-
|
|
29485
|
+
function readRunRecord(projectPath) {
|
|
29486
|
+
for (const directory of runtimeDirectories()) {
|
|
29487
|
+
const own = recordAt(runRecordPath(projectPath, directory));
|
|
29488
|
+
if (own !== null) {
|
|
29489
|
+
return own;
|
|
29490
|
+
}
|
|
29491
|
+
const shared = recordAt(join14(directory, "runs", SHARED_NOTE));
|
|
29492
|
+
if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
|
|
29493
|
+
return shared;
|
|
29494
|
+
}
|
|
29394
29495
|
}
|
|
29395
|
-
|
|
29396
|
-
...record,
|
|
29397
|
-
...ending.exitCode === null ? {} : { exitCode: ending.exitCode },
|
|
29398
|
-
...ending.exitSignal === null ? {} : { exitSignal: ending.exitSignal }
|
|
29399
|
-
};
|
|
29400
|
-
try {
|
|
29401
|
-
writeFileSync9(path, JSON.stringify(written, null, 2), "utf8");
|
|
29402
|
-
} catch {}
|
|
29496
|
+
return null;
|
|
29403
29497
|
}
|
|
29404
|
-
function
|
|
29405
|
-
|
|
29406
|
-
|
|
29407
|
-
|
|
29408
|
-
|
|
29498
|
+
function everyRunRecord() {
|
|
29499
|
+
const found = [];
|
|
29500
|
+
for (const directory of runtimeDirectories()) {
|
|
29501
|
+
let entries;
|
|
29502
|
+
try {
|
|
29503
|
+
entries = readdirSync5(join14(directory, "runs"));
|
|
29504
|
+
} catch {
|
|
29505
|
+
continue;
|
|
29506
|
+
}
|
|
29507
|
+
for (const entry of entries) {
|
|
29508
|
+
if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
|
|
29509
|
+
const record = recordAt(join14(directory, "runs", entry));
|
|
29510
|
+
if (record !== null) {
|
|
29511
|
+
found.push(record);
|
|
29512
|
+
}
|
|
29513
|
+
}
|
|
29409
29514
|
}
|
|
29410
29515
|
}
|
|
29411
|
-
return
|
|
29516
|
+
return found.sort((one, other) => other.startedAt - one.startedAt);
|
|
29412
29517
|
}
|
|
29413
29518
|
function recordAt(path) {
|
|
29414
29519
|
let parsed;
|
|
@@ -29554,7 +29659,6 @@ function stillTheRecordedRun(record) {
|
|
|
29554
29659
|
function couldStillBeTheRecordedRun(record) {
|
|
29555
29660
|
return judgeRun(record, runningAs(record.pid), "possible");
|
|
29556
29661
|
}
|
|
29557
|
-
var AN_EDITOR = /(?:^|\s)(?:-e|--editor)(?:\s|$)/;
|
|
29558
29662
|
var SAME_RUN_WINDOW_MS = 1e4;
|
|
29559
29663
|
function judgeRun(record, running, needed) {
|
|
29560
29664
|
if (running === null) {
|
|
@@ -29569,7 +29673,7 @@ function judgeRun(record, running, needed) {
|
|
|
29569
29673
|
if (wanted !== null && !said.includes(wanted)) {
|
|
29570
29674
|
return false;
|
|
29571
29675
|
}
|
|
29572
|
-
if (running.kind === "commandLine" &&
|
|
29676
|
+
if (running.kind === "commandLine" && readCommandLine(running.text).editor) {
|
|
29573
29677
|
return false;
|
|
29574
29678
|
}
|
|
29575
29679
|
if (record.projectPath === "") {
|
|
@@ -29581,15 +29685,32 @@ function judgeRun(record, running, needed) {
|
|
|
29581
29685
|
const project = process.platform === "win32" ? record.projectPath.toLowerCase() : record.projectPath;
|
|
29582
29686
|
return said.includes(project);
|
|
29583
29687
|
}
|
|
29584
|
-
function
|
|
29585
|
-
|
|
29586
|
-
|
|
29587
|
-
|
|
29588
|
-
|
|
29688
|
+
function stillTheAnnouncedGame(pid, projectPath, announcedAt) {
|
|
29689
|
+
return judgeAnnouncedGame(runningAs(pid), projectPath, announcedAt);
|
|
29690
|
+
}
|
|
29691
|
+
function judgeAnnouncedGame(running, projectPath, announcedAt) {
|
|
29692
|
+
if (running?.kind !== "commandLine" || running.startedAt === undefined) {
|
|
29693
|
+
return null;
|
|
29694
|
+
}
|
|
29695
|
+
if (running.startedAt - announcedAt > STARTED_AFTER_ANNOUNCING_MS) {
|
|
29696
|
+
return null;
|
|
29697
|
+
}
|
|
29698
|
+
const read = readCommandLine(running.text);
|
|
29699
|
+
if (read.editor || read.projectPath === null || !isSameDirectory(read.projectPath, projectPath)) {
|
|
29700
|
+
return null;
|
|
29701
|
+
}
|
|
29702
|
+
return read;
|
|
29703
|
+
}
|
|
29704
|
+
function clearRunRecord(projectPath) {
|
|
29705
|
+
for (const directory of runtimeDirectories()) {
|
|
29706
|
+
const shared = join14(directory, "runs", SHARED_NOTE);
|
|
29707
|
+
const sharedRecord = recordAt(shared);
|
|
29708
|
+
const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
|
|
29709
|
+
for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
|
|
29710
|
+
try {
|
|
29711
|
+
rmSync6(path, { force: true });
|
|
29712
|
+
} catch {}
|
|
29589
29713
|
}
|
|
29590
|
-
try {
|
|
29591
|
-
rmSync6(path);
|
|
29592
|
-
} catch {}
|
|
29593
29714
|
}
|
|
29594
29715
|
}
|
|
29595
29716
|
function ageMs(path, now) {
|
|
@@ -29601,7 +29722,7 @@ function ageMs(path, now) {
|
|
|
29601
29722
|
}
|
|
29602
29723
|
function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
29603
29724
|
const directory = runsDirectory();
|
|
29604
|
-
const
|
|
29725
|
+
const kept = new Set(everyRunRecord().map((record) => record.transcript));
|
|
29605
29726
|
let entries;
|
|
29606
29727
|
try {
|
|
29607
29728
|
entries = readdirSync5(directory);
|
|
@@ -29613,7 +29734,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
29613
29734
|
continue;
|
|
29614
29735
|
}
|
|
29615
29736
|
const path = join14(directory, entry);
|
|
29616
|
-
if (path
|
|
29737
|
+
if (kept.has(path) || ageMs(path, now) < keepMs) {
|
|
29617
29738
|
continue;
|
|
29618
29739
|
}
|
|
29619
29740
|
try {
|
|
@@ -29623,7 +29744,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
29623
29744
|
}
|
|
29624
29745
|
|
|
29625
29746
|
// src/setup.ts
|
|
29626
|
-
import { createHash as
|
|
29747
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
29627
29748
|
import {
|
|
29628
29749
|
cpSync,
|
|
29629
29750
|
existsSync as existsSync9,
|
|
@@ -29634,8 +29755,8 @@ import {
|
|
|
29634
29755
|
statSync as statSync6,
|
|
29635
29756
|
writeFileSync as writeFileSync10
|
|
29636
29757
|
} from "node:fs";
|
|
29637
|
-
import { dirname as
|
|
29638
|
-
import { fileURLToPath as
|
|
29758
|
+
import { dirname as dirname10, join as join15 } from "node:path";
|
|
29759
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
29639
29760
|
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
29640
29761
|
var EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
|
|
29641
29762
|
var RUNTIME_AUTOLOAD = {
|
|
@@ -29645,7 +29766,7 @@ var RUNTIME_AUTOLOAD = {
|
|
|
29645
29766
|
var VERSION_MARKER = ".gdharness-version";
|
|
29646
29767
|
var DIGEST_MARKER = ".gdharness-digest";
|
|
29647
29768
|
function editorAddonDigest(from = shippedAddonsDirectory()) {
|
|
29648
|
-
const hash =
|
|
29769
|
+
const hash = createHash4("sha256");
|
|
29649
29770
|
const walk = (directory, relative) => {
|
|
29650
29771
|
for (const name of readdirSync6(directory).sort()) {
|
|
29651
29772
|
if (name === VERSION_MARKER || name === DIGEST_MARKER) {
|
|
@@ -29681,7 +29802,7 @@ function shippedEditorDigest() {
|
|
|
29681
29802
|
return shippedDigest ?? undefined;
|
|
29682
29803
|
}
|
|
29683
29804
|
function shippedAddonsDirectory() {
|
|
29684
|
-
return join15(
|
|
29805
|
+
return join15(dirname10(fileURLToPath3(import.meta.url)), "godot", "addons");
|
|
29685
29806
|
}
|
|
29686
29807
|
function installedAddonVersion(projectPath) {
|
|
29687
29808
|
const marker = join15(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
@@ -30363,6 +30484,12 @@ var TOOL_SPECS = [
|
|
|
30363
30484
|
type: "boolean",
|
|
30364
30485
|
ops: ["stop"],
|
|
30365
30486
|
description: "stop: also end what the game started for itself, with OS.create_process or otherwise, such as a bench's workers and whatever those started in turn. Only the processes under the run's, to any depth, that are games of this project are ended: announced as one, or this project's engine run with --path on this project by its command line, which is how a bench whose workers deliberately carry no runtime is reached. They are named under endedChildren, with what identified each under identifiedBy; any other process under the run is left and named under childrenLeft, and childrenUnknown is true when nothing could be listed, with the note saying whether the platform would not or the run has no process to list under. Default false, and the run's process is ended either way."
|
|
30487
|
+
},
|
|
30488
|
+
pid: {
|
|
30489
|
+
type: "integer",
|
|
30490
|
+
minimum: 1,
|
|
30491
|
+
ops: ["stop"],
|
|
30492
|
+
description: "stop: a game of this project that no server here is holding, by its process id as editor_status lists it under runtimes: one a server before a reconnect started and left no note for, or one started some other way. It is ended only when it announced itself for this project and the operating system describes it as this project's engine run, --path on the project and no editor flag, started before it announced; otherwise nothing is signalled and the answer says which of those failed. The run this server holds is ended the ordinary way whether or not its number is given."
|
|
30366
30493
|
}
|
|
30367
30494
|
},
|
|
30368
30495
|
requires: [],
|
|
@@ -30372,7 +30499,7 @@ var TOOL_SPECS = [
|
|
|
30372
30499
|
requires: ["projectPath"]
|
|
30373
30500
|
},
|
|
30374
30501
|
stop: {
|
|
30375
|
-
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",
|
|
30502
|
+
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. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled",
|
|
30376
30503
|
requires: []
|
|
30377
30504
|
},
|
|
30378
30505
|
check: {
|
|
@@ -30948,7 +31075,7 @@ function buildToolDefinitions() {
|
|
|
30948
31075
|
var UPDATE_NOTICE_EVERY = 500;
|
|
30949
31076
|
var FEEDBACK_NOTICE_EVERY = 250;
|
|
30950
31077
|
var run5 = promisify5(execFile5);
|
|
30951
|
-
var __dirname2 =
|
|
31078
|
+
var __dirname2 = dirname11(fileURLToPath4(import.meta.url));
|
|
30952
31079
|
var EDITOR_RESTART_TIMEOUT_MS = 90000;
|
|
30953
31080
|
var HOLD_PING_MS = 2000;
|
|
30954
31081
|
var SLOWEST_FRAME_RATE = 20;
|
|
@@ -30962,9 +31089,9 @@ function captureDestinationRefusal(outputPath, projects) {
|
|
|
30962
31089
|
if (extname2(outputPath).toLowerCase() !== ".png") {
|
|
30963
31090
|
return `outputPath ${outputPath} does not end in .png, which is what a capture is.`;
|
|
30964
31091
|
}
|
|
30965
|
-
const directory = realPathOr(
|
|
31092
|
+
const directory = realPathOr(dirname11(outputPath));
|
|
30966
31093
|
if (!existsSync10(directory)) {
|
|
30967
|
-
return `outputPath's directory ${
|
|
31094
|
+
return `outputPath's directory ${dirname11(outputPath)} does not exist; a capture creates no directories.`;
|
|
30968
31095
|
}
|
|
30969
31096
|
const where = join16(directory, basename3(outputPath));
|
|
30970
31097
|
const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
@@ -31019,7 +31146,7 @@ function stillRunning(run) {
|
|
|
31019
31146
|
if (typeof run.endedHere === "string") {
|
|
31020
31147
|
return false;
|
|
31021
31148
|
}
|
|
31022
|
-
if (!run.throughEditor && run.
|
|
31149
|
+
if (!run.throughEditor && run.pid !== null) {
|
|
31023
31150
|
return alive(run.pid);
|
|
31024
31151
|
}
|
|
31025
31152
|
if (run.announcedPid !== undefined && !alive(run.announcedPid)) {
|
|
@@ -31044,12 +31171,12 @@ function runIsUp(run, editorSays, now = Date.now()) {
|
|
|
31044
31171
|
return editorSays ?? stillRunning(run);
|
|
31045
31172
|
}
|
|
31046
31173
|
function noCodeWillCome(run) {
|
|
31047
|
-
return !exited(run)
|
|
31174
|
+
return !exited(run);
|
|
31048
31175
|
}
|
|
31049
31176
|
var STOP_WAIT_MS = 1e4;
|
|
31050
31177
|
var EXIT_REPORTED_WITHIN_MS = 2000;
|
|
31051
|
-
async function untilGone(
|
|
31052
|
-
const gone = () =>
|
|
31178
|
+
async function untilGone(pids, withinMs = STOP_WAIT_MS) {
|
|
31179
|
+
const gone = () => pids.every((pid) => !alive(pid));
|
|
31053
31180
|
const deadline = Date.now() + withinMs;
|
|
31054
31181
|
while (!gone() && Date.now() < deadline) {
|
|
31055
31182
|
await delay3(50);
|
|
@@ -31057,6 +31184,9 @@ async function untilGone(handle, pids, withinMs = STOP_WAIT_MS) {
|
|
|
31057
31184
|
return gone();
|
|
31058
31185
|
}
|
|
31059
31186
|
function endedWithoutACode(run) {
|
|
31187
|
+
if (!run.throughEditor && !run.pickedUp) {
|
|
31188
|
+
return "This run was started here, and the keeper holding it wrote no exit code into its note: the keeper was ended before the game, or another run replaced the note. Everything it printed is below, read back from its transcript.";
|
|
31189
|
+
}
|
|
31060
31190
|
if (!run.throughEditor) {
|
|
31061
31191
|
return run.endedHere ? "This run was started by another server, so nothing here held it and its exit code was never collected. Everything it printed is below, read back from its transcript." : "This run was started by another server, the one this server replaced or one running beside it, and it ended with nothing here waiting on it, so its exit code was never collected. Everything it printed is below, read back from its transcript.";
|
|
31062
31192
|
}
|
|
@@ -31069,12 +31199,41 @@ function endedWithoutACode(run) {
|
|
|
31069
31199
|
}
|
|
31070
31200
|
return `The editor stopped playing this run. ${kept} What it printed is below.`;
|
|
31071
31201
|
}
|
|
31202
|
+
function stopVerdict(stop) {
|
|
31203
|
+
if (!stop.wasRunning) {
|
|
31204
|
+
return {
|
|
31205
|
+
stopped: true,
|
|
31206
|
+
endedPid: stop.endedPid,
|
|
31207
|
+
clean: stop.errors === 0,
|
|
31208
|
+
note: "This run was over before the stop, so nothing was ended here: editor_output has what it printed and how it ended. Anything that game started for itself, with OS.create_process or otherwise, is a separate process and may still be running.",
|
|
31209
|
+
withChildren: false
|
|
31210
|
+
};
|
|
31211
|
+
}
|
|
31212
|
+
if (stop.ending === "refused") {
|
|
31213
|
+
return {
|
|
31214
|
+
stopped: false,
|
|
31215
|
+
notSignalled: stop.endedPid,
|
|
31216
|
+
note: `Nothing was ended: pid ${stop.endedPid ?? "unknown"} no longer answers as the process this run was started as (its engine, its project and when it started), so it was not signalled. If that process is still the game, end it yourself, for instance with runtime_invoke calling get_tree().quit() when the game has the runtime addon; if it is not, it belongs to something else.`,
|
|
31217
|
+
withChildren: false
|
|
31218
|
+
};
|
|
31219
|
+
}
|
|
31220
|
+
return {
|
|
31221
|
+
stopped: true,
|
|
31222
|
+
endedPid: stop.endedPid,
|
|
31223
|
+
clean: stop.errors === 0,
|
|
31224
|
+
note: `${stop.throughEditor ? stop.endedPid === null ? "The editor was asked to stop the scene it is playing. Its game had not announced a runtime, so no process was named under endedPid, and its exit code stays with the editor." : "The editor was asked to stop the scene it is playing." : "The process named under endedPid was ended."}${stop.exitSignal === null ? "" : ` It was ended by ${stop.exitSignal} and so has no exit code.`}${stop.ending === "gone" ? "" : ` It had not exited ${STOP_WAIT_MS / 1000} seconds after being told to, so it may still be going: editor_status says whether it is.`}`,
|
|
31225
|
+
withChildren: true
|
|
31226
|
+
};
|
|
31227
|
+
}
|
|
31072
31228
|
function endedPreviousRun(ended) {
|
|
31073
|
-
if (ended === null) {
|
|
31229
|
+
if (ended === null || ended.ending === "refused") {
|
|
31074
31230
|
return;
|
|
31075
31231
|
}
|
|
31076
31232
|
return ended.pid ?? true;
|
|
31077
31233
|
}
|
|
31234
|
+
function previousRunLeft(ended) {
|
|
31235
|
+
return ended?.ending === "refused" && ended.pid !== null ? ended.pid : undefined;
|
|
31236
|
+
}
|
|
31078
31237
|
var CONSOLE_WRAPPER = /_console(\.exe)?$/i;
|
|
31079
31238
|
function engineNamesOf(executable) {
|
|
31080
31239
|
return CONSOLE_WRAPPER.test(executable) ? [executable, executable.replace(CONSOLE_WRAPPER, "$1")] : [executable];
|
|
@@ -31115,6 +31274,17 @@ function aboutTheChildren(ended, throughEditor) {
|
|
|
31115
31274
|
const leftToo = ended.left.length === 0 ? "" : ` ${counted(ended.left.length)} it had started ${ended.left.length === 1 ? "was" : "were"} left, named under childrenLeft: neither announced as a game of this project nor this project's engine run with --path on it, so what ${ended.left.length === 1 ? "it is" : "they are"} cannot be told from here.`;
|
|
31116
31275
|
return `${endedToo}${leftToo}`;
|
|
31117
31276
|
}
|
|
31277
|
+
function childrenFields(ended) {
|
|
31278
|
+
if (ended === null) {
|
|
31279
|
+
return {};
|
|
31280
|
+
}
|
|
31281
|
+
return {
|
|
31282
|
+
endedChildren: ended.ended.map((one) => one.pid),
|
|
31283
|
+
identifiedBy: ended.ended.length === 0 ? undefined : Object.fromEntries(ended.ended.map((one) => [String(one.pid), one.identifiedBy])),
|
|
31284
|
+
childrenLeft: ended.left.length === 0 ? undefined : ended.left,
|
|
31285
|
+
childrenUnknown: ended.unknown === false ? undefined : true
|
|
31286
|
+
};
|
|
31287
|
+
}
|
|
31118
31288
|
function uidsLeftNote(left) {
|
|
31119
31289
|
if (left === 0) {
|
|
31120
31290
|
return "No scene was written: this reads the project and imports it, and does not resave anything.";
|
|
@@ -31122,12 +31292,24 @@ function uidsLeftNote(left) {
|
|
|
31122
31292
|
const them = left === 1 ? "it" : "them";
|
|
31123
31293
|
return `${left === 1 ? "One file" : `${left} files`} still ${left === 1 ? "has" : "have"} no .uid, named under stillWithoutUid, which is the engine declining to import ${them} rather than this op skipping ${them}. No scene was written.`;
|
|
31124
31294
|
}
|
|
31295
|
+
function leftRunningNote(stranded) {
|
|
31296
|
+
if (stranded.length === 0) {
|
|
31297
|
+
return "";
|
|
31298
|
+
}
|
|
31299
|
+
const named = stranded.map((one) => one.protocol === null ? `pid ${one.pid}` : `pid ${one.pid} on protocol ${one.protocol}`).join(", ");
|
|
31300
|
+
const [only] = stranded;
|
|
31301
|
+
return stranded.length === 1 && only !== undefined ? ` A game of this project was already running and still is: ${named}. Starting this one did not end it, because this server did not start it. editor_run stop with pid ${only.pid} ends it and leaves this run going.` : ` ${stranded.length} games of this project were already running and still are: ${named}. Starting this one did not end them, because this server did not start them. editor_run stop with the pid of each ends it and leaves this run going.`;
|
|
31302
|
+
}
|
|
31125
31303
|
function endedToStartThis(ended) {
|
|
31126
31304
|
if (ended === null) {
|
|
31127
31305
|
return "";
|
|
31128
31306
|
}
|
|
31129
31307
|
const which = ended.pid === null ? "The game the editor was playing" : `The run that was going, pid ${ended.pid},`;
|
|
31130
|
-
|
|
31308
|
+
if (ended.ending === "refused") {
|
|
31309
|
+
return ` ${which} was not ended: that pid no longer answers as the process the run was started as, so nothing was signalled. If it is still the game, it is running beside this one and you have to end it yourself; if it is not, it belongs to something else.`;
|
|
31310
|
+
}
|
|
31311
|
+
const after = ended.ending === "lingering" ? ` It had not exited ${STOP_WAIT_MS / 1000} seconds after being told to, so it may still be running beside this one: editor_status says whether it is.` : "";
|
|
31312
|
+
return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.${after}`;
|
|
31131
31313
|
}
|
|
31132
31314
|
var LONGEST_BOOT_NOTED_MS = 5 * 60000;
|
|
31133
31315
|
var MOST_GROUPS = 20;
|
|
@@ -32479,11 +32661,11 @@ class GodotServer {
|
|
|
32479
32661
|
if (!engine.ok) {
|
|
32480
32662
|
return engine.response;
|
|
32481
32663
|
}
|
|
32482
|
-
const folder =
|
|
32664
|
+
const folder = dirname11(output.absolutePath);
|
|
32483
32665
|
try {
|
|
32484
32666
|
mkdirSync10(folder, { recursive: true });
|
|
32485
32667
|
} catch (error) {
|
|
32486
|
-
return this.createErrorResponse(`Cannot create ${
|
|
32668
|
+
return this.createErrorResponse(`Cannot create ${dirname11(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
32487
32669
|
}
|
|
32488
32670
|
const debug = readBoolean(args, "debug") ?? false;
|
|
32489
32671
|
const exportLogs = mkdtempSync2(join16(tmpdir4(), "gdharness-export-"));
|
|
@@ -32628,7 +32810,7 @@ class GodotServer {
|
|
|
32628
32810
|
} finally {
|
|
32629
32811
|
discard(reportsDir, userData);
|
|
32630
32812
|
try {
|
|
32631
|
-
rmdirSync(
|
|
32813
|
+
rmdirSync(dirname11(reportsDir));
|
|
32632
32814
|
} catch {}
|
|
32633
32815
|
}
|
|
32634
32816
|
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
|
|
@@ -32856,7 +33038,12 @@ class GodotServer {
|
|
|
32856
33038
|
return anEditorIsStillComing(listeningSince, this.launchedEditorIsUp());
|
|
32857
33039
|
}
|
|
32858
33040
|
launchedEditorIsUp() {
|
|
32859
|
-
|
|
33041
|
+
const launched = this.launchedEditor;
|
|
33042
|
+
if (launched === null || !alive(launched.pid)) {
|
|
33043
|
+
return false;
|
|
33044
|
+
}
|
|
33045
|
+
const now = startTimesOf([launched.pid]).get(launched.pid);
|
|
33046
|
+
return launched.startedAt === null || now === undefined || now === launched.startedAt;
|
|
32860
33047
|
}
|
|
32861
33048
|
dap() {
|
|
32862
33049
|
const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
|
|
@@ -33217,9 +33404,9 @@ class GodotServer {
|
|
|
33217
33404
|
}
|
|
33218
33405
|
async openAnEditor(engine, projectPath, ports) {
|
|
33219
33406
|
clearEditorLog(projectPath);
|
|
33220
|
-
const
|
|
33221
|
-
|
|
33222
|
-
|
|
33407
|
+
const launched = await launchOutsideTheTree({
|
|
33408
|
+
command: engine,
|
|
33409
|
+
args: editorArguments(projectPath, ports, editorLogPath(projectPath)),
|
|
33223
33410
|
env: {
|
|
33224
33411
|
...process.env,
|
|
33225
33412
|
GDHARNESS_RUNTIME_DIR: runtimeDirectory(),
|
|
@@ -33228,23 +33415,15 @@ class GodotServer {
|
|
|
33228
33415
|
[OPENED_BY_A_SERVER]: "1"
|
|
33229
33416
|
}
|
|
33230
33417
|
});
|
|
33231
|
-
if (
|
|
33232
|
-
|
|
33233
|
-
}
|
|
33234
|
-
const started = await new Promise((resolve) => {
|
|
33235
|
-
editor.once("spawn", () => {
|
|
33236
|
-
resolve(null);
|
|
33237
|
-
});
|
|
33238
|
-
editor.once("error", (error) => {
|
|
33239
|
-
resolve(error.message);
|
|
33240
|
-
});
|
|
33241
|
-
});
|
|
33242
|
-
if (started !== null) {
|
|
33243
|
-
return { pid: null, error: started };
|
|
33418
|
+
if ("error" in launched) {
|
|
33419
|
+
return { pid: null, error: launched.error };
|
|
33244
33420
|
}
|
|
33245
|
-
|
|
33246
|
-
this.launchedEditor =
|
|
33247
|
-
|
|
33421
|
+
writeEditorLogNote(projectPath, launched.pid);
|
|
33422
|
+
this.launchedEditor = {
|
|
33423
|
+
pid: launched.pid,
|
|
33424
|
+
startedAt: startTimesOf([launched.pid]).get(launched.pid) ?? null
|
|
33425
|
+
};
|
|
33426
|
+
return { pid: launched.pid, error: null };
|
|
33248
33427
|
}
|
|
33249
33428
|
async portsForAnEditor() {
|
|
33250
33429
|
const lsp = portFromEnvOrNull("GDHARNESS_LSP_PORT") ?? await freePort(DEFAULT_LSP_PORT);
|
|
@@ -33293,8 +33472,10 @@ class GodotServer {
|
|
|
33293
33472
|
const before = this.currentRun();
|
|
33294
33473
|
if (before !== null && await this.runStillGoing(before)) {
|
|
33295
33474
|
this.logDebug("Ending the running game before starting another");
|
|
33296
|
-
ended = {
|
|
33297
|
-
|
|
33475
|
+
ended = {
|
|
33476
|
+
pid: before.pid ?? this.announcedPidOf(before) ?? null,
|
|
33477
|
+
ending: await this.endActiveGame("editor_run start, which ends the run that was going before it starts another", true)
|
|
33478
|
+
};
|
|
33298
33479
|
}
|
|
33299
33480
|
const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
|
|
33300
33481
|
const wereHereFirst = this.everyRuntimeOfOurs();
|
|
@@ -33314,25 +33495,30 @@ class GodotServer {
|
|
|
33314
33495
|
userArgs: given.value
|
|
33315
33496
|
});
|
|
33316
33497
|
this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
33317
|
-
const started = this.
|
|
33498
|
+
const started = await this.launchKeptGame(engine.value, cmdArgs, project.value.path, needsItsOwnProcess(asked.value) ? environmentFor({ ...asked.value, runtimeDirectory: runtimeDirectory() }) : undefined);
|
|
33499
|
+
if ("error" in started) {
|
|
33500
|
+
return this.createErrorResponse(`The game could not be started: ${started.error}`, [
|
|
33501
|
+
"editor_status says which Godot this server runs and whether it answers"
|
|
33502
|
+
]);
|
|
33503
|
+
}
|
|
33318
33504
|
started.announcedBefore = alreadyPlaying;
|
|
33319
33505
|
this.activeProcess = started;
|
|
33320
33506
|
const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && given.value.length > 0 ? " The editor cannot be handed arguments for a game it plays, so this one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && needsItsOwnProcess(asked.value) ? " The editor plays a game with its own environment, so a run given one was started " + "here: the debug_* tools answer only for a game the editor is playing." : "";
|
|
33321
33507
|
const endedForThis = endedToStartThis(ended);
|
|
33322
33508
|
const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
|
|
33323
|
-
const
|
|
33324
|
-
const alsoRunning = stranded.length === 0 ? "" : ` A game of this project was already running and still is: ${named}. Starting this one did not end it and nothing here can, because this server did not start it. End it yourself, or bring the halves to one version where a protocol is named.`;
|
|
33509
|
+
const alsoRunning = leftRunningNote(stranded);
|
|
33325
33510
|
return this.jsonTextResponse({
|
|
33326
33511
|
started: true,
|
|
33327
33512
|
through: "gdharness",
|
|
33328
33513
|
alsoRunning: stranded.length === 0 ? undefined : stranded.map((one) => one.pid),
|
|
33329
|
-
pid: started.
|
|
33514
|
+
pid: started.pid,
|
|
33330
33515
|
arguments: cmdArgs,
|
|
33331
33516
|
transcript: started.transcript,
|
|
33332
33517
|
refreshedClasses: refreshed.value,
|
|
33333
33518
|
...scanWaitAnswer(scanned),
|
|
33334
33519
|
savesNote: asked.value.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
|
|
33335
33520
|
endedPreviousRun: endedPreviousRun(ended),
|
|
33521
|
+
previousRunLeft: previousRunLeft(ended),
|
|
33336
33522
|
runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
|
|
33337
33523
|
message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}${alsoRunning}`
|
|
33338
33524
|
});
|
|
@@ -33441,7 +33627,6 @@ class GodotServer {
|
|
|
33441
33627
|
closeSync2(transcript.fd);
|
|
33442
33628
|
const playAnswer = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
|
|
33443
33629
|
const played = {
|
|
33444
|
-
process: null,
|
|
33445
33630
|
pid: null,
|
|
33446
33631
|
log,
|
|
33447
33632
|
transcript: transcript.path,
|
|
@@ -33466,6 +33651,7 @@ class GodotServer {
|
|
|
33466
33651
|
...scanWaitAnswer(scanned),
|
|
33467
33652
|
debugPort: readNumber(playAnswer, "debugPort"),
|
|
33468
33653
|
endedPreviousRun: endedPreviousRun(ended),
|
|
33654
|
+
previousRunLeft: previousRunLeft(ended),
|
|
33469
33655
|
breakpoints: breakpoints.applied.length === 0 ? undefined : this.breakpointsAsProjectSpells(projectPath, breakpoints.applied),
|
|
33470
33656
|
breakpointsRefused: breakpoints.refused.length === 0 ? undefined : breakpoints.refused.map((one) => ({
|
|
33471
33657
|
scriptPath: projectSpelling(projectPath, one.scriptPath),
|
|
@@ -33540,28 +33726,41 @@ class GodotServer {
|
|
|
33540
33726
|
}
|
|
33541
33727
|
async endActiveGame(reason, going) {
|
|
33542
33728
|
const running = this.activeProcess;
|
|
33543
|
-
const
|
|
33544
|
-
|
|
33729
|
+
const project = running?.projectPath ?? this.ourProject();
|
|
33730
|
+
const clearTheRecord = () => {
|
|
33731
|
+
if (project !== null) {
|
|
33732
|
+
clearRunRecord(project);
|
|
33733
|
+
}
|
|
33734
|
+
};
|
|
33735
|
+
const aKeeperHoldsIt = running !== null && going && !running.throughEditor && running.pid !== null;
|
|
33736
|
+
if (!aKeeperHoldsIt) {
|
|
33737
|
+
clearTheRecord();
|
|
33738
|
+
}
|
|
33545
33739
|
if (!running || !going) {
|
|
33546
|
-
return
|
|
33740
|
+
return "gone";
|
|
33547
33741
|
}
|
|
33548
33742
|
running.endedHere = reason;
|
|
33549
33743
|
const announced = this.announcedPidOf(running);
|
|
33550
33744
|
if (running.throughEditor) {
|
|
33551
33745
|
await this.handleViaBridge("stop_playing", {});
|
|
33552
|
-
return await untilGone(
|
|
33553
|
-
}
|
|
33554
|
-
if (running.process !== null) {
|
|
33555
|
-
running.process.kill();
|
|
33556
|
-
return await untilGone(running.process, announced === undefined ? [] : [announced]);
|
|
33746
|
+
return await untilGone(announced === undefined ? [] : [announced]) ? "gone" : "lingering";
|
|
33557
33747
|
}
|
|
33558
33748
|
if (running.pid === null) {
|
|
33559
|
-
return
|
|
33560
|
-
}
|
|
33561
|
-
|
|
33749
|
+
return "gone";
|
|
33750
|
+
}
|
|
33751
|
+
const startedAs = {
|
|
33752
|
+
pid: running.pid,
|
|
33753
|
+
transcript: running.transcript ?? "",
|
|
33754
|
+
startedAt: running.startedAt,
|
|
33755
|
+
projectPath: running.projectPath ?? "",
|
|
33756
|
+
arguments: [],
|
|
33757
|
+
...running.command === undefined ? {} : { command: running.command }
|
|
33758
|
+
};
|
|
33759
|
+
if (!stillTheRecordedRun(startedAs)) {
|
|
33760
|
+
clearTheRecord();
|
|
33562
33761
|
running.endedHere = null;
|
|
33563
|
-
running.log.record("warning", `This run was not ended here: pid ${running.pid} no longer answers as the run
|
|
33564
|
-
return
|
|
33762
|
+
running.log.record("warning", `This run was not ended here: pid ${running.pid} no longer answers as the process the run was started as, so nothing was signalled. If that process is still the game, end it yourself; if it is not, it belongs to something else.`);
|
|
33763
|
+
return "refused";
|
|
33565
33764
|
}
|
|
33566
33765
|
const signalled = runningAs(running.pid);
|
|
33567
33766
|
const described = signalled === null ? "nothing it would name" : `${signalled.kind}: ${signalled.text}`;
|
|
@@ -33571,12 +33770,20 @@ class GodotServer {
|
|
|
33571
33770
|
} catch {
|
|
33572
33771
|
landed = false;
|
|
33573
33772
|
}
|
|
33574
|
-
const gone = await untilGone(
|
|
33773
|
+
const gone = await untilGone([running.pid]);
|
|
33774
|
+
if (gone) {
|
|
33775
|
+
await this.untilTheExitIsRecorded(running);
|
|
33776
|
+
}
|
|
33777
|
+
if (landed && gone) {
|
|
33778
|
+
running.exitCode = null;
|
|
33779
|
+
running.exitSignal = "SIGTERM";
|
|
33780
|
+
}
|
|
33781
|
+
clearTheRecord();
|
|
33575
33782
|
running.log.record("info", landed ? `gdharness ended pid ${running.pid}, which the operating system described as ${described}.` : `gdharness signalled pid ${running.pid} and it was already gone; the operating system had described it as ${described}.`);
|
|
33576
|
-
return gone;
|
|
33783
|
+
return gone ? "gone" : "lingering";
|
|
33577
33784
|
}
|
|
33578
33785
|
spawnGame(godotPath, cmdArgs, env) {
|
|
33579
|
-
const child =
|
|
33786
|
+
const child = spawn2(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
|
|
33580
33787
|
const cancelled = callSignal();
|
|
33581
33788
|
if (cancelled !== undefined) {
|
|
33582
33789
|
const end = () => {
|
|
@@ -33628,62 +33835,33 @@ class GodotServer {
|
|
|
33628
33835
|
});
|
|
33629
33836
|
return started;
|
|
33630
33837
|
}
|
|
33631
|
-
|
|
33838
|
+
async launchKeptGame(godotPath, cmdArgs, projectPath, env) {
|
|
33632
33839
|
const startedAt = Date.now();
|
|
33633
33840
|
const transcript = openTranscript(startedAt);
|
|
33634
|
-
|
|
33635
|
-
|
|
33636
|
-
|
|
33637
|
-
|
|
33638
|
-
|
|
33639
|
-
|
|
33640
|
-
|
|
33641
|
-
|
|
33642
|
-
|
|
33841
|
+
closeSync2(transcript.fd);
|
|
33842
|
+
const launched = await launchOutsideTheTree({
|
|
33843
|
+
command: godotPath,
|
|
33844
|
+
args: cmdArgs,
|
|
33845
|
+
...env === undefined ? {} : { env },
|
|
33846
|
+
run: { transcript: transcript.path, startedAt, projectPath }
|
|
33847
|
+
});
|
|
33848
|
+
if ("error" in launched) {
|
|
33849
|
+
return launched;
|
|
33643
33850
|
}
|
|
33644
|
-
|
|
33645
|
-
|
|
33646
|
-
|
|
33647
|
-
|
|
33648
|
-
pid: child.pid ?? null,
|
|
33649
|
-
log,
|
|
33851
|
+
sweepTranscripts();
|
|
33852
|
+
return {
|
|
33853
|
+
pid: launched.pid,
|
|
33854
|
+
log: new GameLog,
|
|
33650
33855
|
transcript: transcript.path,
|
|
33651
33856
|
readOffset: 0,
|
|
33652
33857
|
projectPath,
|
|
33653
33858
|
startedAt,
|
|
33859
|
+
command: godotPath,
|
|
33654
33860
|
exitCode: null,
|
|
33655
33861
|
exitSignal: null,
|
|
33656
33862
|
throughEditor: false,
|
|
33657
33863
|
brokeOn: null
|
|
33658
33864
|
};
|
|
33659
|
-
child.on("exit", (code, signal) => {
|
|
33660
|
-
this.logDebug(`Godot process exited with code ${code ?? "none"}, signal ${signal ?? "none"}`);
|
|
33661
|
-
this.drainTranscript(started);
|
|
33662
|
-
log.finish();
|
|
33663
|
-
started.exitCode = code;
|
|
33664
|
-
started.exitSignal = code === null ? signal : null;
|
|
33665
|
-
if (started.pid !== null) {
|
|
33666
|
-
recordRunEnded(started.pid, { exitCode: started.exitCode, exitSignal: started.exitSignal });
|
|
33667
|
-
}
|
|
33668
|
-
});
|
|
33669
|
-
child.on("error", (err) => {
|
|
33670
|
-
console.error("Failed to start Godot process:", err);
|
|
33671
|
-
log.append("stderr", `${err.message}
|
|
33672
|
-
`);
|
|
33673
|
-
started.exitCode = -1;
|
|
33674
|
-
});
|
|
33675
|
-
if (started.pid !== null) {
|
|
33676
|
-
writeRunRecord({
|
|
33677
|
-
pid: started.pid,
|
|
33678
|
-
transcript: transcript.path,
|
|
33679
|
-
startedAt,
|
|
33680
|
-
projectPath,
|
|
33681
|
-
arguments: cmdArgs,
|
|
33682
|
-
command: godotPath
|
|
33683
|
-
});
|
|
33684
|
-
}
|
|
33685
|
-
sweepTranscripts();
|
|
33686
|
-
return started;
|
|
33687
33865
|
}
|
|
33688
33866
|
drainTranscript(game) {
|
|
33689
33867
|
if (game.transcript === null) {
|
|
@@ -33736,10 +33914,8 @@ class GodotServer {
|
|
|
33736
33914
|
this.logDebug(`Picked up an editor-played run without its adapter: ${errorMessage(error)}`);
|
|
33737
33915
|
}
|
|
33738
33916
|
const project = this.godotBridge.getStatus().projectPath ?? null;
|
|
33739
|
-
const
|
|
33740
|
-
const note = left !== null && project !== null && isSameDirectory(left.projectPath, project) ? left : null;
|
|
33917
|
+
const note = project === null ? null : readEditorRunNote(project);
|
|
33741
33918
|
const picked = {
|
|
33742
|
-
process: null,
|
|
33743
33919
|
pid: null,
|
|
33744
33920
|
log: new GameLog,
|
|
33745
33921
|
transcript: note?.transcript ?? null,
|
|
@@ -33871,22 +34047,24 @@ class GodotServer {
|
|
|
33871
34047
|
return endpoint.editorPid === undefined || endpoint.editorPid !== editor;
|
|
33872
34048
|
}
|
|
33873
34049
|
adoptRecordedRun() {
|
|
33874
|
-
const
|
|
34050
|
+
const project = this.ourProject();
|
|
34051
|
+
const record = project === null ? null : readRunRecord(project);
|
|
33875
34052
|
if (record === null || !this.couldBeOurs(record.projectPath)) {
|
|
33876
34053
|
return null;
|
|
33877
34054
|
}
|
|
33878
34055
|
const adopted = {
|
|
33879
|
-
process: null,
|
|
33880
34056
|
pid: record.pid,
|
|
33881
34057
|
log: new GameLog,
|
|
33882
34058
|
transcript: record.transcript,
|
|
33883
34059
|
readOffset: 0,
|
|
33884
34060
|
projectPath: record.projectPath === "" ? null : record.projectPath,
|
|
33885
34061
|
startedAt: record.startedAt,
|
|
34062
|
+
...record.command === undefined ? {} : { command: record.command },
|
|
33886
34063
|
exitCode: null,
|
|
33887
34064
|
exitSignal: null,
|
|
33888
34065
|
throughEditor: false,
|
|
33889
|
-
brokeOn: null
|
|
34066
|
+
brokeOn: null,
|
|
34067
|
+
pickedUp: true
|
|
33890
34068
|
};
|
|
33891
34069
|
this.drainTranscript(adopted);
|
|
33892
34070
|
if (!alive(record.pid) || !couldStillBeTheRecordedRun(record)) {
|
|
@@ -33903,18 +34081,18 @@ class GodotServer {
|
|
|
33903
34081
|
const noEditor = !status.connected ? " No editor is connected, so a game the editor is playing cannot be seen from here at all:" + " editor_status says whether one is on its way, and a run of yours that is still up is" + " reachable again once it is." : "";
|
|
33904
34082
|
const live = this.aRuntimeOfOursIsAnswering();
|
|
33905
34083
|
if (live !== null) {
|
|
33906
|
-
return `A game is running for ${live.project.path} and has announced its runtime on port ${live.port}, pid ${live.pid},` + " but this server did not start it and cannot reach it through the editor, so editor_run and" +
|
|
34084
|
+
return `A game is running for ${live.project.path} and has announced its runtime on port ${live.port}, pid ${live.pid},` + " but this server did not start it and cannot reach it through the editor, so editor_run and" + ` editor_output have nothing to answer about. The runtime_* tools reach it now, and editor_run stop with pid ${live.pid} ends it.` + " A run started here is a separate process rather than a replacement, so starting one leaves" + " this game running as well.";
|
|
33907
34085
|
}
|
|
33908
34086
|
const unreadable = this.aRuntimeOfOursIsTooFarOff();
|
|
33909
34087
|
if (unreadable !== null) {
|
|
33910
34088
|
const behind = whichHalfIsBehind([unreadable]);
|
|
33911
|
-
return `A game is running for ${unreadable.project.path}, pid ${unreadable.pid}, announced in protocol` + ` ${unreadable.protocol}, which this server does not speak: it speaks ${RUNTIME_PROTOCOL}, so ${behind}.` +
|
|
34089
|
+
return `A game is running for ${unreadable.project.path}, pid ${unreadable.pid}, announced in protocol` + ` ${unreadable.protocol}, which this server does not speak: it speaks ${RUNTIME_PROTOCOL}, so ${behind}.` + ` Until then the runtime_* tools cannot reach it, and editor_run stop with pid ${unreadable.pid} ends it.` + " A run started here is a separate process rather than a replacement, so starting one leaves" + " this game running as well.";
|
|
33912
34090
|
}
|
|
33913
|
-
const
|
|
34091
|
+
const mine = this.ourProject();
|
|
34092
|
+
const record = everyRunRecord().find((one) => mine === null || !isSameDirectory(one.projectPath, mine)) ?? null;
|
|
33914
34093
|
if (record === null) {
|
|
33915
34094
|
return `No game is running.${noEditor} Start one with editor_run.`;
|
|
33916
34095
|
}
|
|
33917
|
-
const mine = this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
|
|
33918
34096
|
const whose = record.projectPath === "" ? "a project it does not name" : record.projectPath;
|
|
33919
34097
|
const ours = mine === null ? "this server was started without GDHARNESS_PROJECT and has no editor connected, so it cannot say whose that run is" : `this server serves ${mine}`;
|
|
33920
34098
|
return `No game of this server's is running.${noEditor} A run started from ${whose} is recorded on this machine, and ${ours}, so it is not this server's to answer for or to end. Start one with editor_run.`;
|
|
@@ -33957,13 +34135,16 @@ class GodotServer {
|
|
|
33957
34135
|
];
|
|
33958
34136
|
}
|
|
33959
34137
|
couldBeOurs(project) {
|
|
33960
|
-
const
|
|
33961
|
-
const mine = this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
|
|
34138
|
+
const mine = this.ourProject();
|
|
33962
34139
|
if (mine === null) {
|
|
33963
34140
|
return false;
|
|
33964
34141
|
}
|
|
33965
34142
|
return project !== "" && isSameDirectory(mine, project);
|
|
33966
34143
|
}
|
|
34144
|
+
ourProject() {
|
|
34145
|
+
const status = this.godotBridge.getStatus();
|
|
34146
|
+
return this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
|
|
34147
|
+
}
|
|
33967
34148
|
currentRun() {
|
|
33968
34149
|
return this.activeProcess ?? this.adoptRecordedRun();
|
|
33969
34150
|
}
|
|
@@ -34021,8 +34202,8 @@ class GodotServer {
|
|
|
34021
34202
|
async runStillGoing(run, editorSays) {
|
|
34022
34203
|
if (!run.throughEditor) {
|
|
34023
34204
|
const going = stillRunning(run);
|
|
34024
|
-
if (!going && run.
|
|
34025
|
-
|
|
34205
|
+
if (!going && run.pid !== null && !exited(run) && run.endedUnwatched !== true && typeof run.endedHere !== "string" && !await this.untilTheExitIsRecorded(run)) {
|
|
34206
|
+
run.endedUnwatched = true;
|
|
34026
34207
|
}
|
|
34027
34208
|
return going;
|
|
34028
34209
|
}
|
|
@@ -34039,6 +34220,21 @@ class GodotServer {
|
|
|
34039
34220
|
}
|
|
34040
34221
|
return runIsUp(run, said);
|
|
34041
34222
|
}
|
|
34223
|
+
async untilTheExitIsRecorded(run) {
|
|
34224
|
+
const until = Date.now() + EXIT_REPORTED_WITHIN_MS;
|
|
34225
|
+
for (;; ) {
|
|
34226
|
+
const record = run.projectPath === null ? null : readRunRecord(run.projectPath);
|
|
34227
|
+
if (record !== null && record.pid === run.pid && (record.exitCode !== undefined || record.exitSignal !== undefined)) {
|
|
34228
|
+
run.exitCode = record.exitCode ?? null;
|
|
34229
|
+
run.exitSignal = record.exitSignal ?? null;
|
|
34230
|
+
return true;
|
|
34231
|
+
}
|
|
34232
|
+
if (Date.now() >= until) {
|
|
34233
|
+
return false;
|
|
34234
|
+
}
|
|
34235
|
+
await delay3(50);
|
|
34236
|
+
}
|
|
34237
|
+
}
|
|
34042
34238
|
handleEditorConsole(args) {
|
|
34043
34239
|
const status = this.godotBridge.getStatus();
|
|
34044
34240
|
const projectPath = status.projectPath ?? this.ownProject ?? undefined;
|
|
@@ -34140,7 +34336,8 @@ class GodotServer {
|
|
|
34140
34336
|
throughEditor: run.throughEditor,
|
|
34141
34337
|
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
34142
34338
|
couldAnnounce: run.projectPath !== null && existsSync10(join16(run.projectPath, RUNTIME_AUTOLOAD.path)),
|
|
34143
|
-
endedHere: typeof run.endedHere === "string"
|
|
34339
|
+
endedHere: typeof run.endedHere === "string",
|
|
34340
|
+
pickedUp: run.pickedUp === true
|
|
34144
34341
|
}));
|
|
34145
34342
|
}
|
|
34146
34343
|
if (run.pickedUpPlaying === true) {
|
|
@@ -34166,7 +34363,7 @@ class GodotServer {
|
|
|
34166
34363
|
...hold,
|
|
34167
34364
|
endedUnwatched: run.endedUnwatched === true ? true : undefined,
|
|
34168
34365
|
endedBy: stillRunning(run) ? undefined : run.endedHere ?? null,
|
|
34169
|
-
project:
|
|
34366
|
+
project: run.throughEditor ? undefined : run.projectPath ?? undefined,
|
|
34170
34367
|
transcript: run.transcript ?? undefined,
|
|
34171
34368
|
startedAt: new Date(run.startedAt).toISOString(),
|
|
34172
34369
|
elapsedMs: Date.now() - run.startedAt,
|
|
@@ -34179,6 +34376,10 @@ class GodotServer {
|
|
|
34179
34376
|
async handleStopProject(args) {
|
|
34180
34377
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
34181
34378
|
const stopped = this.currentRun();
|
|
34379
|
+
const named = readNumber(args, "pid");
|
|
34380
|
+
if (named !== undefined && (stopped === null || named !== stopped.pid && named !== this.announcedPidOf(stopped))) {
|
|
34381
|
+
return await this.stopAnnouncedGame(named, readBoolean(args, "andChildren") === true);
|
|
34382
|
+
}
|
|
34182
34383
|
if (!stopped) {
|
|
34183
34384
|
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
34184
34385
|
}
|
|
@@ -34189,35 +34390,87 @@ class GodotServer {
|
|
|
34189
34390
|
const endedPid = stopped.pid ?? announcedPid;
|
|
34190
34391
|
const children = readBoolean(args, "andChildren") === true && wasRunning ? await this.whatTheRunStarted(stopped) : null;
|
|
34191
34392
|
this.logDebug("Stopping the running game");
|
|
34192
|
-
const
|
|
34193
|
-
const
|
|
34194
|
-
|
|
34393
|
+
const ending = await this.endActiveGame("editor_run stop", wasRunning);
|
|
34394
|
+
const refused = ending === "refused";
|
|
34395
|
+
const ended = children === null || refused ? null : endChildrenAmong(children);
|
|
34396
|
+
if (wasRunning && !refused) {
|
|
34195
34397
|
for (const pid of new Set([endedPid, announcedPid])) {
|
|
34196
34398
|
if (pid !== null) {
|
|
34197
34399
|
await announcementEnded(pid);
|
|
34198
34400
|
}
|
|
34199
34401
|
}
|
|
34200
34402
|
}
|
|
34403
|
+
const verdict = stopVerdict({
|
|
34404
|
+
wasRunning,
|
|
34405
|
+
ending,
|
|
34406
|
+
endedPid,
|
|
34407
|
+
throughEditor: stopped.throughEditor,
|
|
34408
|
+
exitSignal: stopped.exitSignal,
|
|
34409
|
+
errors: stopped.log.count("error")
|
|
34410
|
+
});
|
|
34201
34411
|
return this.jsonTextResponse({
|
|
34202
|
-
stopped:
|
|
34412
|
+
stopped: verdict.stopped,
|
|
34203
34413
|
through: stopped.throughEditor ? "editor" : "gdharness",
|
|
34204
|
-
endedPid,
|
|
34205
|
-
|
|
34206
|
-
|
|
34207
|
-
identifiedBy: ended.ended.length === 0 ? undefined : Object.fromEntries(ended.ended.map((one) => [String(one.pid), one.identifiedBy])),
|
|
34208
|
-
childrenLeft: ended.left.length === 0 ? undefined : ended.left,
|
|
34209
|
-
childrenUnknown: ended.unknown === false ? undefined : true
|
|
34210
|
-
},
|
|
34414
|
+
endedPid: verdict.endedPid,
|
|
34415
|
+
notSignalled: verdict.notSignalled,
|
|
34416
|
+
...childrenFields(ended),
|
|
34211
34417
|
exitedBeforeStop: !wasRunning,
|
|
34212
34418
|
exitCode: stopped.exitCode,
|
|
34213
34419
|
exitSignal: stopped.exitSignal ?? undefined,
|
|
34214
34420
|
errors: stopped.log.count("error"),
|
|
34215
34421
|
warnings: stopped.log.count("warning"),
|
|
34216
|
-
clean:
|
|
34217
|
-
note:
|
|
34422
|
+
clean: verdict.clean,
|
|
34423
|
+
note: verdict.withChildren ? `${verdict.note} ${aboutTheChildren(ended, stopped.throughEditor)}` : verdict.note,
|
|
34218
34424
|
entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
34219
34425
|
});
|
|
34220
34426
|
}
|
|
34427
|
+
async stopAnnouncedGame(pid, andChildren) {
|
|
34428
|
+
const announced = runtimesAnnounced();
|
|
34429
|
+
const game = [
|
|
34430
|
+
...this.allAnnouncedForOurProject(announced.running),
|
|
34431
|
+
...this.allAnnouncedForOurProject(announced.unspoken)
|
|
34432
|
+
].find((one) => one.pid === pid);
|
|
34433
|
+
if (game === undefined) {
|
|
34434
|
+
return this.createErrorResponse(`pid ${pid} is not a game announced for this project, so nothing was signalled. editor_status lists this project's games under runtimes. A game that announces no runtime is ended by the server holding its run, or with andChildren by a stop of the run that started it.`);
|
|
34435
|
+
}
|
|
34436
|
+
const read = game.announcedAt === undefined ? null : stillTheAnnouncedGame(pid, game.project.path, game.announcedAt);
|
|
34437
|
+
if (read === null) {
|
|
34438
|
+
return this.createErrorResponse(`pid ${pid} announced itself as a game of this project, but the operating system does not describe it as this project's engine run started before that announcement (the engine with --path on the project and no editor flag), so nothing was signalled. If it is still the game, runtime_invoke can ask it to quit.`);
|
|
34439
|
+
}
|
|
34440
|
+
const run = {
|
|
34441
|
+
pid,
|
|
34442
|
+
log: new GameLog,
|
|
34443
|
+
transcript: null,
|
|
34444
|
+
readOffset: 0,
|
|
34445
|
+
projectPath: game.project.path,
|
|
34446
|
+
startedAt: game.announcedAt ?? Date.now(),
|
|
34447
|
+
exitCode: null,
|
|
34448
|
+
exitSignal: null,
|
|
34449
|
+
throughEditor: false,
|
|
34450
|
+
brokeOn: null,
|
|
34451
|
+
announcedPid: pid
|
|
34452
|
+
};
|
|
34453
|
+
const children = andChildren ? await this.whatTheRunStarted(run) : null;
|
|
34454
|
+
let landed = true;
|
|
34455
|
+
try {
|
|
34456
|
+
process.kill(pid);
|
|
34457
|
+
} catch {
|
|
34458
|
+
landed = false;
|
|
34459
|
+
}
|
|
34460
|
+
const gone = await untilGone([pid]);
|
|
34461
|
+
const ended = children === null ? null : endChildrenAmong(children);
|
|
34462
|
+
if (gone) {
|
|
34463
|
+
await announcementEnded(pid);
|
|
34464
|
+
}
|
|
34465
|
+
return this.jsonTextResponse({
|
|
34466
|
+
stopped: true,
|
|
34467
|
+
through: "gdharness",
|
|
34468
|
+
endedPid: pid,
|
|
34469
|
+
...childrenFields(ended),
|
|
34470
|
+
exitedBeforeStop: !landed,
|
|
34471
|
+
note: `${landed ? `pid ${pid} was ended: a game of this project that no server here was holding,` : `pid ${pid} had already gone when it was signalled: a game of this project that no server here was holding,`} identified by its announcement and by its command line, ${read.executable} run with --path ${read.projectPath ?? ""}. Nothing here read what it printed or holds its exit code, so neither is in this answer.${gone ? "" : ` It had not exited ${STOP_WAIT_MS / 1000} seconds after being told to, so it may still be going: editor_status says whether it is.`} ${aboutTheChildren(ended, false)}`
|
|
34472
|
+
});
|
|
34473
|
+
}
|
|
34221
34474
|
async whatTheRunStarted(run) {
|
|
34222
34475
|
const tree = await processTree();
|
|
34223
34476
|
if (tree === undefined) {
|