gdharness 1.0.30 → 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 +321 -85
- package/build/index.js +322 -87
- package/build/keeper.js +74 -24
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -10202,7 +10202,7 @@ 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
10207
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
10208
10208
|
import { promisify as promisify5 } from "node:util";
|
|
@@ -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
|
}
|
|
@@ -28887,10 +28895,27 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
28887
28895
|
// src/outside.ts
|
|
28888
28896
|
import { spawn } from "node:child_process";
|
|
28889
28897
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
28890
|
-
|
|
28891
|
-
|
|
28892
|
-
|
|
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;
|
|
28893
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));
|
|
28894
28919
|
function readLaunched(printed) {
|
|
28895
28920
|
const line = printed.split(`
|
|
28896
28921
|
`).find((one) => one.trim() !== "")?.trim() ?? "";
|
|
@@ -28902,10 +28927,16 @@ function readLaunched(printed) {
|
|
|
28902
28927
|
}
|
|
28903
28928
|
async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = KEEPER_SCRIPT) {
|
|
28904
28929
|
return await new Promise((resolve) => {
|
|
28905
|
-
const launcher = spawn(runtime, [keeper, "launch"
|
|
28906
|
-
stdio: ["
|
|
28930
|
+
const launcher = spawn(runtime, [keeper, "launch"], {
|
|
28931
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
28907
28932
|
windowsHide: true
|
|
28908
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
|
+
});
|
|
28909
28940
|
let printed = "";
|
|
28910
28941
|
let complained = "";
|
|
28911
28942
|
launcher.stdout.on("data", (chunk) => {
|
|
@@ -29371,30 +29402,62 @@ function restartSettled(projectPath) {
|
|
|
29371
29402
|
|
|
29372
29403
|
// src/run-record.ts
|
|
29373
29404
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
29374
|
-
import {
|
|
29375
|
-
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";
|
|
29376
29417
|
function runsDirectory() {
|
|
29377
29418
|
return join14(runtimeDirectory(), "runs");
|
|
29378
29419
|
}
|
|
29379
|
-
function
|
|
29380
|
-
|
|
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);
|
|
29381
29440
|
}
|
|
29441
|
+
function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
29442
|
+
return join14(directory, "runs", `note-${projectKey(projectPath)}.json`);
|
|
29443
|
+
}
|
|
29444
|
+
var SHARED_NOTE = "run.json";
|
|
29382
29445
|
function openTranscript(startedAt) {
|
|
29383
29446
|
mkdirSync8(runsDirectory(), { recursive: true });
|
|
29384
29447
|
const path = join14(runsDirectory(), `run-${startedAt}.log`);
|
|
29385
29448
|
return { path, fd: openSync2(path, "a") };
|
|
29386
29449
|
}
|
|
29387
|
-
function editorNotePath() {
|
|
29388
|
-
return join14(
|
|
29450
|
+
function editorNotePath(projectPath, directory = runtimeDirectory()) {
|
|
29451
|
+
return join14(directory, "runs", `editor-${projectKey(projectPath)}.json`);
|
|
29389
29452
|
}
|
|
29390
29453
|
function writeEditorRunNote(note) {
|
|
29391
29454
|
mkdirSync8(runsDirectory(), { recursive: true });
|
|
29392
|
-
writeFileSync9(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
|
|
29455
|
+
writeFileSync9(editorNotePath(note.projectPath), JSON.stringify(note, null, 2), "utf8");
|
|
29393
29456
|
}
|
|
29394
|
-
function readEditorRunNote() {
|
|
29457
|
+
function readEditorRunNote(projectPath) {
|
|
29395
29458
|
for (const directory of runtimeDirectories()) {
|
|
29396
|
-
const note = editorNoteAt(join14(directory, "runs", "editor-run.json"));
|
|
29397
|
-
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)) {
|
|
29398
29461
|
return note;
|
|
29399
29462
|
}
|
|
29400
29463
|
}
|
|
@@ -29419,15 +29482,39 @@ function editorNoteAt(path) {
|
|
|
29419
29482
|
}
|
|
29420
29483
|
return { projectPath, transcript, startedAt };
|
|
29421
29484
|
}
|
|
29422
|
-
function readRunRecord() {
|
|
29423
|
-
for (const
|
|
29424
|
-
const
|
|
29425
|
-
if (
|
|
29426
|
-
return
|
|
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;
|
|
29427
29494
|
}
|
|
29428
29495
|
}
|
|
29429
29496
|
return null;
|
|
29430
29497
|
}
|
|
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
|
+
}
|
|
29514
|
+
}
|
|
29515
|
+
}
|
|
29516
|
+
return found.sort((one, other) => other.startedAt - one.startedAt);
|
|
29517
|
+
}
|
|
29431
29518
|
function recordAt(path) {
|
|
29432
29519
|
let parsed;
|
|
29433
29520
|
try {
|
|
@@ -29572,7 +29659,6 @@ function stillTheRecordedRun(record) {
|
|
|
29572
29659
|
function couldStillBeTheRecordedRun(record) {
|
|
29573
29660
|
return judgeRun(record, runningAs(record.pid), "possible");
|
|
29574
29661
|
}
|
|
29575
|
-
var AN_EDITOR = /(?:^|\s)(?:-e|--editor)(?:\s|$)/;
|
|
29576
29662
|
var SAME_RUN_WINDOW_MS = 1e4;
|
|
29577
29663
|
function judgeRun(record, running, needed) {
|
|
29578
29664
|
if (running === null) {
|
|
@@ -29587,7 +29673,7 @@ function judgeRun(record, running, needed) {
|
|
|
29587
29673
|
if (wanted !== null && !said.includes(wanted)) {
|
|
29588
29674
|
return false;
|
|
29589
29675
|
}
|
|
29590
|
-
if (running.kind === "commandLine" &&
|
|
29676
|
+
if (running.kind === "commandLine" && readCommandLine(running.text).editor) {
|
|
29591
29677
|
return false;
|
|
29592
29678
|
}
|
|
29593
29679
|
if (record.projectPath === "") {
|
|
@@ -29599,15 +29685,32 @@ function judgeRun(record, running, needed) {
|
|
|
29599
29685
|
const project = process.platform === "win32" ? record.projectPath.toLowerCase() : record.projectPath;
|
|
29600
29686
|
return said.includes(project);
|
|
29601
29687
|
}
|
|
29602
|
-
function
|
|
29603
|
-
|
|
29604
|
-
|
|
29605
|
-
|
|
29606
|
-
|
|
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 {}
|
|
29607
29713
|
}
|
|
29608
|
-
try {
|
|
29609
|
-
rmSync6(path);
|
|
29610
|
-
} catch {}
|
|
29611
29714
|
}
|
|
29612
29715
|
}
|
|
29613
29716
|
function ageMs(path, now) {
|
|
@@ -29619,7 +29722,7 @@ function ageMs(path, now) {
|
|
|
29619
29722
|
}
|
|
29620
29723
|
function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
29621
29724
|
const directory = runsDirectory();
|
|
29622
|
-
const
|
|
29725
|
+
const kept = new Set(everyRunRecord().map((record) => record.transcript));
|
|
29623
29726
|
let entries;
|
|
29624
29727
|
try {
|
|
29625
29728
|
entries = readdirSync5(directory);
|
|
@@ -29631,7 +29734,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
29631
29734
|
continue;
|
|
29632
29735
|
}
|
|
29633
29736
|
const path = join14(directory, entry);
|
|
29634
|
-
if (path
|
|
29737
|
+
if (kept.has(path) || ageMs(path, now) < keepMs) {
|
|
29635
29738
|
continue;
|
|
29636
29739
|
}
|
|
29637
29740
|
try {
|
|
@@ -29641,7 +29744,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
29641
29744
|
}
|
|
29642
29745
|
|
|
29643
29746
|
// src/setup.ts
|
|
29644
|
-
import { createHash as
|
|
29747
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
29645
29748
|
import {
|
|
29646
29749
|
cpSync,
|
|
29647
29750
|
existsSync as existsSync9,
|
|
@@ -29652,7 +29755,7 @@ import {
|
|
|
29652
29755
|
statSync as statSync6,
|
|
29653
29756
|
writeFileSync as writeFileSync10
|
|
29654
29757
|
} from "node:fs";
|
|
29655
|
-
import { dirname as
|
|
29758
|
+
import { dirname as dirname10, join as join15 } from "node:path";
|
|
29656
29759
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
29657
29760
|
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
29658
29761
|
var EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
|
|
@@ -29663,7 +29766,7 @@ var RUNTIME_AUTOLOAD = {
|
|
|
29663
29766
|
var VERSION_MARKER = ".gdharness-version";
|
|
29664
29767
|
var DIGEST_MARKER = ".gdharness-digest";
|
|
29665
29768
|
function editorAddonDigest(from = shippedAddonsDirectory()) {
|
|
29666
|
-
const hash =
|
|
29769
|
+
const hash = createHash4("sha256");
|
|
29667
29770
|
const walk = (directory, relative) => {
|
|
29668
29771
|
for (const name of readdirSync6(directory).sort()) {
|
|
29669
29772
|
if (name === VERSION_MARKER || name === DIGEST_MARKER) {
|
|
@@ -29699,7 +29802,7 @@ function shippedEditorDigest() {
|
|
|
29699
29802
|
return shippedDigest ?? undefined;
|
|
29700
29803
|
}
|
|
29701
29804
|
function shippedAddonsDirectory() {
|
|
29702
|
-
return join15(
|
|
29805
|
+
return join15(dirname10(fileURLToPath3(import.meta.url)), "godot", "addons");
|
|
29703
29806
|
}
|
|
29704
29807
|
function installedAddonVersion(projectPath) {
|
|
29705
29808
|
const marker = join15(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
@@ -30381,6 +30484,12 @@ var TOOL_SPECS = [
|
|
|
30381
30484
|
type: "boolean",
|
|
30382
30485
|
ops: ["stop"],
|
|
30383
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."
|
|
30384
30493
|
}
|
|
30385
30494
|
},
|
|
30386
30495
|
requires: [],
|
|
@@ -30390,7 +30499,7 @@ var TOOL_SPECS = [
|
|
|
30390
30499
|
requires: ["projectPath"]
|
|
30391
30500
|
},
|
|
30392
30501
|
stop: {
|
|
30393
|
-
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",
|
|
30394
30503
|
requires: []
|
|
30395
30504
|
},
|
|
30396
30505
|
check: {
|
|
@@ -30966,7 +31075,7 @@ function buildToolDefinitions() {
|
|
|
30966
31075
|
var UPDATE_NOTICE_EVERY = 500;
|
|
30967
31076
|
var FEEDBACK_NOTICE_EVERY = 250;
|
|
30968
31077
|
var run5 = promisify5(execFile5);
|
|
30969
|
-
var __dirname2 =
|
|
31078
|
+
var __dirname2 = dirname11(fileURLToPath4(import.meta.url));
|
|
30970
31079
|
var EDITOR_RESTART_TIMEOUT_MS = 90000;
|
|
30971
31080
|
var HOLD_PING_MS = 2000;
|
|
30972
31081
|
var SLOWEST_FRAME_RATE = 20;
|
|
@@ -30980,9 +31089,9 @@ function captureDestinationRefusal(outputPath, projects) {
|
|
|
30980
31089
|
if (extname2(outputPath).toLowerCase() !== ".png") {
|
|
30981
31090
|
return `outputPath ${outputPath} does not end in .png, which is what a capture is.`;
|
|
30982
31091
|
}
|
|
30983
|
-
const directory = realPathOr(
|
|
31092
|
+
const directory = realPathOr(dirname11(outputPath));
|
|
30984
31093
|
if (!existsSync10(directory)) {
|
|
30985
|
-
return `outputPath's directory ${
|
|
31094
|
+
return `outputPath's directory ${dirname11(outputPath)} does not exist; a capture creates no directories.`;
|
|
30986
31095
|
}
|
|
30987
31096
|
const where = join16(directory, basename3(outputPath));
|
|
30988
31097
|
const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
@@ -31090,12 +31199,41 @@ function endedWithoutACode(run) {
|
|
|
31090
31199
|
}
|
|
31091
31200
|
return `The editor stopped playing this run. ${kept} What it printed is below.`;
|
|
31092
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
|
+
}
|
|
31093
31228
|
function endedPreviousRun(ended) {
|
|
31094
|
-
if (ended === null) {
|
|
31229
|
+
if (ended === null || ended.ending === "refused") {
|
|
31095
31230
|
return;
|
|
31096
31231
|
}
|
|
31097
31232
|
return ended.pid ?? true;
|
|
31098
31233
|
}
|
|
31234
|
+
function previousRunLeft(ended) {
|
|
31235
|
+
return ended?.ending === "refused" && ended.pid !== null ? ended.pid : undefined;
|
|
31236
|
+
}
|
|
31099
31237
|
var CONSOLE_WRAPPER = /_console(\.exe)?$/i;
|
|
31100
31238
|
function engineNamesOf(executable) {
|
|
31101
31239
|
return CONSOLE_WRAPPER.test(executable) ? [executable, executable.replace(CONSOLE_WRAPPER, "$1")] : [executable];
|
|
@@ -31136,6 +31274,17 @@ function aboutTheChildren(ended, throughEditor) {
|
|
|
31136
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.`;
|
|
31137
31275
|
return `${endedToo}${leftToo}`;
|
|
31138
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
|
+
}
|
|
31139
31288
|
function uidsLeftNote(left) {
|
|
31140
31289
|
if (left === 0) {
|
|
31141
31290
|
return "No scene was written: this reads the project and imports it, and does not resave anything.";
|
|
@@ -31143,12 +31292,24 @@ function uidsLeftNote(left) {
|
|
|
31143
31292
|
const them = left === 1 ? "it" : "them";
|
|
31144
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.`;
|
|
31145
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
|
+
}
|
|
31146
31303
|
function endedToStartThis(ended) {
|
|
31147
31304
|
if (ended === null) {
|
|
31148
31305
|
return "";
|
|
31149
31306
|
}
|
|
31150
31307
|
const which = ended.pid === null ? "The game the editor was playing" : `The run that was going, pid ${ended.pid},`;
|
|
31151
|
-
|
|
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}`;
|
|
31152
31313
|
}
|
|
31153
31314
|
var LONGEST_BOOT_NOTED_MS = 5 * 60000;
|
|
31154
31315
|
var MOST_GROUPS = 20;
|
|
@@ -32500,11 +32661,11 @@ class GodotServer {
|
|
|
32500
32661
|
if (!engine.ok) {
|
|
32501
32662
|
return engine.response;
|
|
32502
32663
|
}
|
|
32503
|
-
const folder =
|
|
32664
|
+
const folder = dirname11(output.absolutePath);
|
|
32504
32665
|
try {
|
|
32505
32666
|
mkdirSync10(folder, { recursive: true });
|
|
32506
32667
|
} catch (error) {
|
|
32507
|
-
return this.createErrorResponse(`Cannot create ${
|
|
32668
|
+
return this.createErrorResponse(`Cannot create ${dirname11(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
32508
32669
|
}
|
|
32509
32670
|
const debug = readBoolean(args, "debug") ?? false;
|
|
32510
32671
|
const exportLogs = mkdtempSync2(join16(tmpdir4(), "gdharness-export-"));
|
|
@@ -32649,7 +32810,7 @@ class GodotServer {
|
|
|
32649
32810
|
} finally {
|
|
32650
32811
|
discard(reportsDir, userData);
|
|
32651
32812
|
try {
|
|
32652
|
-
rmdirSync(
|
|
32813
|
+
rmdirSync(dirname11(reportsDir));
|
|
32653
32814
|
} catch {}
|
|
32654
32815
|
}
|
|
32655
32816
|
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
|
|
@@ -33311,8 +33472,10 @@ class GodotServer {
|
|
|
33311
33472
|
const before = this.currentRun();
|
|
33312
33473
|
if (before !== null && await this.runStillGoing(before)) {
|
|
33313
33474
|
this.logDebug("Ending the running game before starting another");
|
|
33314
|
-
ended = {
|
|
33315
|
-
|
|
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
|
+
};
|
|
33316
33479
|
}
|
|
33317
33480
|
const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
|
|
33318
33481
|
const wereHereFirst = this.everyRuntimeOfOurs();
|
|
@@ -33343,8 +33506,7 @@ class GodotServer {
|
|
|
33343
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." : "";
|
|
33344
33507
|
const endedForThis = endedToStartThis(ended);
|
|
33345
33508
|
const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
|
|
33346
|
-
const
|
|
33347
|
-
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);
|
|
33348
33510
|
return this.jsonTextResponse({
|
|
33349
33511
|
started: true,
|
|
33350
33512
|
through: "gdharness",
|
|
@@ -33356,6 +33518,7 @@ class GodotServer {
|
|
|
33356
33518
|
...scanWaitAnswer(scanned),
|
|
33357
33519
|
savesNote: asked.value.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
|
|
33358
33520
|
endedPreviousRun: endedPreviousRun(ended),
|
|
33521
|
+
previousRunLeft: previousRunLeft(ended),
|
|
33359
33522
|
runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
|
|
33360
33523
|
message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}${alsoRunning}`
|
|
33361
33524
|
});
|
|
@@ -33488,6 +33651,7 @@ class GodotServer {
|
|
|
33488
33651
|
...scanWaitAnswer(scanned),
|
|
33489
33652
|
debugPort: readNumber(playAnswer, "debugPort"),
|
|
33490
33653
|
endedPreviousRun: endedPreviousRun(ended),
|
|
33654
|
+
previousRunLeft: previousRunLeft(ended),
|
|
33491
33655
|
breakpoints: breakpoints.applied.length === 0 ? undefined : this.breakpointsAsProjectSpells(projectPath, breakpoints.applied),
|
|
33492
33656
|
breakpointsRefused: breakpoints.refused.length === 0 ? undefined : breakpoints.refused.map((one) => ({
|
|
33493
33657
|
scriptPath: projectSpelling(projectPath, one.scriptPath),
|
|
@@ -33562,31 +33726,41 @@ class GodotServer {
|
|
|
33562
33726
|
}
|
|
33563
33727
|
async endActiveGame(reason, going) {
|
|
33564
33728
|
const running = this.activeProcess;
|
|
33565
|
-
const
|
|
33729
|
+
const project = running?.projectPath ?? this.ourProject();
|
|
33566
33730
|
const clearTheRecord = () => {
|
|
33567
|
-
|
|
33731
|
+
if (project !== null) {
|
|
33732
|
+
clearRunRecord(project);
|
|
33733
|
+
}
|
|
33568
33734
|
};
|
|
33569
33735
|
const aKeeperHoldsIt = running !== null && going && !running.throughEditor && running.pid !== null;
|
|
33570
33736
|
if (!aKeeperHoldsIt) {
|
|
33571
33737
|
clearTheRecord();
|
|
33572
33738
|
}
|
|
33573
33739
|
if (!running || !going) {
|
|
33574
|
-
return
|
|
33740
|
+
return "gone";
|
|
33575
33741
|
}
|
|
33576
33742
|
running.endedHere = reason;
|
|
33577
33743
|
const announced = this.announcedPidOf(running);
|
|
33578
33744
|
if (running.throughEditor) {
|
|
33579
33745
|
await this.handleViaBridge("stop_playing", {});
|
|
33580
|
-
return await untilGone(announced === undefined ? [] : [announced]);
|
|
33746
|
+
return await untilGone(announced === undefined ? [] : [announced]) ? "gone" : "lingering";
|
|
33581
33747
|
}
|
|
33582
33748
|
if (running.pid === null) {
|
|
33583
|
-
return
|
|
33584
|
-
}
|
|
33585
|
-
|
|
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)) {
|
|
33586
33760
|
clearTheRecord();
|
|
33587
33761
|
running.endedHere = null;
|
|
33588
|
-
running.log.record("warning", `This run was not ended here: pid ${running.pid} no longer answers as the run
|
|
33589
|
-
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";
|
|
33590
33764
|
}
|
|
33591
33765
|
const signalled = runningAs(running.pid);
|
|
33592
33766
|
const described = signalled === null ? "nothing it would name" : `${signalled.kind}: ${signalled.text}`;
|
|
@@ -33606,7 +33780,7 @@ class GodotServer {
|
|
|
33606
33780
|
}
|
|
33607
33781
|
clearTheRecord();
|
|
33608
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}.`);
|
|
33609
|
-
return gone;
|
|
33783
|
+
return gone ? "gone" : "lingering";
|
|
33610
33784
|
}
|
|
33611
33785
|
spawnGame(godotPath, cmdArgs, env) {
|
|
33612
33786
|
const child = spawn2(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
|
|
@@ -33682,6 +33856,7 @@ class GodotServer {
|
|
|
33682
33856
|
readOffset: 0,
|
|
33683
33857
|
projectPath,
|
|
33684
33858
|
startedAt,
|
|
33859
|
+
command: godotPath,
|
|
33685
33860
|
exitCode: null,
|
|
33686
33861
|
exitSignal: null,
|
|
33687
33862
|
throughEditor: false,
|
|
@@ -33739,8 +33914,7 @@ class GodotServer {
|
|
|
33739
33914
|
this.logDebug(`Picked up an editor-played run without its adapter: ${errorMessage(error)}`);
|
|
33740
33915
|
}
|
|
33741
33916
|
const project = this.godotBridge.getStatus().projectPath ?? null;
|
|
33742
|
-
const
|
|
33743
|
-
const note = left !== null && project !== null && isSameDirectory(left.projectPath, project) ? left : null;
|
|
33917
|
+
const note = project === null ? null : readEditorRunNote(project);
|
|
33744
33918
|
const picked = {
|
|
33745
33919
|
pid: null,
|
|
33746
33920
|
log: new GameLog,
|
|
@@ -33873,7 +34047,8 @@ class GodotServer {
|
|
|
33873
34047
|
return endpoint.editorPid === undefined || endpoint.editorPid !== editor;
|
|
33874
34048
|
}
|
|
33875
34049
|
adoptRecordedRun() {
|
|
33876
|
-
const
|
|
34050
|
+
const project = this.ourProject();
|
|
34051
|
+
const record = project === null ? null : readRunRecord(project);
|
|
33877
34052
|
if (record === null || !this.couldBeOurs(record.projectPath)) {
|
|
33878
34053
|
return null;
|
|
33879
34054
|
}
|
|
@@ -33884,6 +34059,7 @@ class GodotServer {
|
|
|
33884
34059
|
readOffset: 0,
|
|
33885
34060
|
projectPath: record.projectPath === "" ? null : record.projectPath,
|
|
33886
34061
|
startedAt: record.startedAt,
|
|
34062
|
+
...record.command === undefined ? {} : { command: record.command },
|
|
33887
34063
|
exitCode: null,
|
|
33888
34064
|
exitSignal: null,
|
|
33889
34065
|
throughEditor: false,
|
|
@@ -33905,18 +34081,18 @@ class GodotServer {
|
|
|
33905
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." : "";
|
|
33906
34082
|
const live = this.aRuntimeOfOursIsAnswering();
|
|
33907
34083
|
if (live !== null) {
|
|
33908
|
-
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.";
|
|
33909
34085
|
}
|
|
33910
34086
|
const unreadable = this.aRuntimeOfOursIsTooFarOff();
|
|
33911
34087
|
if (unreadable !== null) {
|
|
33912
34088
|
const behind = whichHalfIsBehind([unreadable]);
|
|
33913
|
-
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.";
|
|
33914
34090
|
}
|
|
33915
|
-
const
|
|
34091
|
+
const mine = this.ourProject();
|
|
34092
|
+
const record = everyRunRecord().find((one) => mine === null || !isSameDirectory(one.projectPath, mine)) ?? null;
|
|
33916
34093
|
if (record === null) {
|
|
33917
34094
|
return `No game is running.${noEditor} Start one with editor_run.`;
|
|
33918
34095
|
}
|
|
33919
|
-
const mine = this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
|
|
33920
34096
|
const whose = record.projectPath === "" ? "a project it does not name" : record.projectPath;
|
|
33921
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}`;
|
|
33922
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.`;
|
|
@@ -33959,13 +34135,16 @@ class GodotServer {
|
|
|
33959
34135
|
];
|
|
33960
34136
|
}
|
|
33961
34137
|
couldBeOurs(project) {
|
|
33962
|
-
const
|
|
33963
|
-
const mine = this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
|
|
34138
|
+
const mine = this.ourProject();
|
|
33964
34139
|
if (mine === null) {
|
|
33965
34140
|
return false;
|
|
33966
34141
|
}
|
|
33967
34142
|
return project !== "" && isSameDirectory(mine, project);
|
|
33968
34143
|
}
|
|
34144
|
+
ourProject() {
|
|
34145
|
+
const status = this.godotBridge.getStatus();
|
|
34146
|
+
return this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
|
|
34147
|
+
}
|
|
33969
34148
|
currentRun() {
|
|
33970
34149
|
return this.activeProcess ?? this.adoptRecordedRun();
|
|
33971
34150
|
}
|
|
@@ -34044,7 +34223,7 @@ class GodotServer {
|
|
|
34044
34223
|
async untilTheExitIsRecorded(run) {
|
|
34045
34224
|
const until = Date.now() + EXIT_REPORTED_WITHIN_MS;
|
|
34046
34225
|
for (;; ) {
|
|
34047
|
-
const record = readRunRecord();
|
|
34226
|
+
const record = run.projectPath === null ? null : readRunRecord(run.projectPath);
|
|
34048
34227
|
if (record !== null && record.pid === run.pid && (record.exitCode !== undefined || record.exitSignal !== undefined)) {
|
|
34049
34228
|
run.exitCode = record.exitCode ?? null;
|
|
34050
34229
|
run.exitSignal = record.exitSignal ?? null;
|
|
@@ -34197,6 +34376,10 @@ class GodotServer {
|
|
|
34197
34376
|
async handleStopProject(args) {
|
|
34198
34377
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
34199
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
|
+
}
|
|
34200
34383
|
if (!stopped) {
|
|
34201
34384
|
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
34202
34385
|
}
|
|
@@ -34207,35 +34390,87 @@ class GodotServer {
|
|
|
34207
34390
|
const endedPid = stopped.pid ?? announcedPid;
|
|
34208
34391
|
const children = readBoolean(args, "andChildren") === true && wasRunning ? await this.whatTheRunStarted(stopped) : null;
|
|
34209
34392
|
this.logDebug("Stopping the running game");
|
|
34210
|
-
const
|
|
34211
|
-
const
|
|
34212
|
-
|
|
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) {
|
|
34213
34397
|
for (const pid of new Set([endedPid, announcedPid])) {
|
|
34214
34398
|
if (pid !== null) {
|
|
34215
34399
|
await announcementEnded(pid);
|
|
34216
34400
|
}
|
|
34217
34401
|
}
|
|
34218
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
|
+
});
|
|
34219
34411
|
return this.jsonTextResponse({
|
|
34220
|
-
stopped:
|
|
34412
|
+
stopped: verdict.stopped,
|
|
34221
34413
|
through: stopped.throughEditor ? "editor" : "gdharness",
|
|
34222
|
-
endedPid,
|
|
34223
|
-
|
|
34224
|
-
|
|
34225
|
-
identifiedBy: ended.ended.length === 0 ? undefined : Object.fromEntries(ended.ended.map((one) => [String(one.pid), one.identifiedBy])),
|
|
34226
|
-
childrenLeft: ended.left.length === 0 ? undefined : ended.left,
|
|
34227
|
-
childrenUnknown: ended.unknown === false ? undefined : true
|
|
34228
|
-
},
|
|
34414
|
+
endedPid: verdict.endedPid,
|
|
34415
|
+
notSignalled: verdict.notSignalled,
|
|
34416
|
+
...childrenFields(ended),
|
|
34229
34417
|
exitedBeforeStop: !wasRunning,
|
|
34230
34418
|
exitCode: stopped.exitCode,
|
|
34231
34419
|
exitSignal: stopped.exitSignal ?? undefined,
|
|
34232
34420
|
errors: stopped.log.count("error"),
|
|
34233
34421
|
warnings: stopped.log.count("warning"),
|
|
34234
|
-
clean:
|
|
34235
|
-
note:
|
|
34422
|
+
clean: verdict.clean,
|
|
34423
|
+
note: verdict.withChildren ? `${verdict.note} ${aboutTheChildren(ended, stopped.throughEditor)}` : verdict.note,
|
|
34236
34424
|
entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
34237
34425
|
});
|
|
34238
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
|
+
}
|
|
34239
34474
|
async whatTheRunStarted(run) {
|
|
34240
34475
|
const tree = await processTree();
|
|
34241
34476
|
if (tree === undefined) {
|