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/cli.js
CHANGED
|
@@ -204,7 +204,9 @@ function ancestorsIn(tree, pid, root) {
|
|
|
204
204
|
return chain;
|
|
205
205
|
}
|
|
206
206
|
function readCommandLine(command) {
|
|
207
|
-
const
|
|
207
|
+
const all = commandWords(command);
|
|
208
|
+
const separator = all.findIndex((word) => word === "--" || word === "++");
|
|
209
|
+
const words = separator === -1 ? all : all.slice(0, separator);
|
|
208
210
|
const executable = words[0] === undefined ? "" : basename(words[0].replaceAll("\\", "/"));
|
|
209
211
|
const at = words.indexOf("--path");
|
|
210
212
|
let projectPath = null;
|
|
@@ -216,7 +218,7 @@ function readCommandLine(command) {
|
|
|
216
218
|
}
|
|
217
219
|
taken.push(word);
|
|
218
220
|
}
|
|
219
|
-
projectPath = taken.join(" ");
|
|
221
|
+
projectPath = taken.join(" ").replaceAll("%20", " ");
|
|
220
222
|
}
|
|
221
223
|
return {
|
|
222
224
|
executable,
|
|
@@ -16356,6 +16358,12 @@ var init_tool_definitions = __esm(() => {
|
|
|
16356
16358
|
type: "boolean",
|
|
16357
16359
|
ops: ["stop"],
|
|
16358
16360
|
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."
|
|
16361
|
+
},
|
|
16362
|
+
pid: {
|
|
16363
|
+
type: "integer",
|
|
16364
|
+
minimum: 1,
|
|
16365
|
+
ops: ["stop"],
|
|
16366
|
+
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."
|
|
16359
16367
|
}
|
|
16360
16368
|
},
|
|
16361
16369
|
requires: [],
|
|
@@ -16365,7 +16373,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16365
16373
|
requires: ["projectPath"]
|
|
16366
16374
|
},
|
|
16367
16375
|
stop: {
|
|
16368
|
-
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",
|
|
16376
|
+
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",
|
|
16369
16377
|
requires: []
|
|
16370
16378
|
},
|
|
16371
16379
|
check: {
|
|
@@ -31703,8 +31711,13 @@ function parseAnnouncement(file, pid) {
|
|
|
31703
31711
|
name: readString(project, "name") ?? "",
|
|
31704
31712
|
path: readString(project, "path") ?? ""
|
|
31705
31713
|
};
|
|
31714
|
+
let announcedAt;
|
|
31715
|
+
try {
|
|
31716
|
+
announcedAt = statSync5(file).mtimeMs;
|
|
31717
|
+
} catch {}
|
|
31718
|
+
const when = announcedAt === undefined ? {} : { announcedAt };
|
|
31706
31719
|
if (protocol !== RUNTIME_PROTOCOL) {
|
|
31707
|
-
return { kind: "unspoken", unspoken: { pid, protocol: protocol ?? 0, project: named } };
|
|
31720
|
+
return { kind: "unspoken", unspoken: { pid, protocol: protocol ?? 0, project: named, ...when } };
|
|
31708
31721
|
}
|
|
31709
31722
|
const editorPid = readNumber(fields, "editor_pid");
|
|
31710
31723
|
return {
|
|
@@ -31715,7 +31728,8 @@ function parseAnnouncement(file, pid) {
|
|
|
31715
31728
|
address,
|
|
31716
31729
|
project: named,
|
|
31717
31730
|
file,
|
|
31718
|
-
...editorPid !== undefined && Number.isInteger(editorPid) && editorPid > 0 ? { editorPid } : {}
|
|
31731
|
+
...editorPid !== undefined && Number.isInteger(editorPid) && editorPid > 0 ? { editorPid } : {},
|
|
31732
|
+
...when
|
|
31719
31733
|
}
|
|
31720
31734
|
};
|
|
31721
31735
|
}
|
|
@@ -37730,6 +37744,72 @@ var init_lsp_client = __esm(() => {
|
|
|
37730
37744
|
init_ports();
|
|
37731
37745
|
});
|
|
37732
37746
|
|
|
37747
|
+
// src/outside.ts
|
|
37748
|
+
import { spawn } from "node:child_process";
|
|
37749
|
+
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
37750
|
+
function changesFrom(env, base) {
|
|
37751
|
+
const changes = {};
|
|
37752
|
+
if (env === undefined) {
|
|
37753
|
+
return changes;
|
|
37754
|
+
}
|
|
37755
|
+
for (const [name, value] of Object.entries(env)) {
|
|
37756
|
+
if (value !== undefined && base[name] !== value) {
|
|
37757
|
+
changes[name] = value;
|
|
37758
|
+
}
|
|
37759
|
+
}
|
|
37760
|
+
for (const name of Object.keys(base)) {
|
|
37761
|
+
if (env[name] === undefined) {
|
|
37762
|
+
changes[name] = null;
|
|
37763
|
+
}
|
|
37764
|
+
}
|
|
37765
|
+
return changes;
|
|
37766
|
+
}
|
|
37767
|
+
function sendSpec(child, spec) {
|
|
37768
|
+
child.stdin?.end(JSON.stringify(spec), "utf8");
|
|
37769
|
+
}
|
|
37770
|
+
function readLaunched(printed) {
|
|
37771
|
+
const line = printed.split(`
|
|
37772
|
+
`).find((one) => one.trim() !== "")?.trim() ?? "";
|
|
37773
|
+
const pid = /^pid (\d+)$/.exec(line);
|
|
37774
|
+
if (pid?.[1] !== undefined) {
|
|
37775
|
+
return { pid: Number(pid[1]) };
|
|
37776
|
+
}
|
|
37777
|
+
return { error: line.replace(/^error /, "") || "the launcher said nothing" };
|
|
37778
|
+
}
|
|
37779
|
+
async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = KEEPER_SCRIPT) {
|
|
37780
|
+
return await new Promise((resolve) => {
|
|
37781
|
+
const launcher = spawn(runtime, [keeper, "launch"], {
|
|
37782
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
37783
|
+
windowsHide: true
|
|
37784
|
+
});
|
|
37785
|
+
sendSpec(launcher, {
|
|
37786
|
+
command: spec.command,
|
|
37787
|
+
args: spec.args,
|
|
37788
|
+
envChanges: changesFrom(spec.env, process.env),
|
|
37789
|
+
...spec.run === undefined ? {} : { run: spec.run }
|
|
37790
|
+
});
|
|
37791
|
+
let printed = "";
|
|
37792
|
+
let complained = "";
|
|
37793
|
+
launcher.stdout.on("data", (chunk) => {
|
|
37794
|
+
printed += chunk.toString();
|
|
37795
|
+
});
|
|
37796
|
+
launcher.stderr.on("data", (chunk) => {
|
|
37797
|
+
complained += chunk.toString();
|
|
37798
|
+
});
|
|
37799
|
+
launcher.once("error", (error) => {
|
|
37800
|
+
resolve({ error: `the launcher did not start: ${error.message}` });
|
|
37801
|
+
});
|
|
37802
|
+
launcher.once("close", () => {
|
|
37803
|
+
const launched = readLaunched(printed);
|
|
37804
|
+
resolve("error" in launched && complained.trim() !== "" ? { error: `${launched.error}: ${complained.trim()}` } : launched);
|
|
37805
|
+
});
|
|
37806
|
+
});
|
|
37807
|
+
}
|
|
37808
|
+
var KEEPER_SCRIPT;
|
|
37809
|
+
var init_outside = __esm(() => {
|
|
37810
|
+
KEEPER_SCRIPT = fileURLToPath5(new URL("./keeper.js", import.meta.url));
|
|
37811
|
+
});
|
|
37812
|
+
|
|
37733
37813
|
// src/process-time.ts
|
|
37734
37814
|
import { execFile as execFile6 } from "node:child_process";
|
|
37735
37815
|
import { promisify as promisify6 } from "node:util";
|
|
@@ -37944,37 +38024,61 @@ var init_restart_note = __esm(() => {
|
|
|
37944
38024
|
|
|
37945
38025
|
// src/run-record.ts
|
|
37946
38026
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
37947
|
-
import {
|
|
37948
|
-
import {
|
|
38027
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
38028
|
+
import {
|
|
38029
|
+
mkdirSync as mkdirSync13,
|
|
38030
|
+
openSync as openSync2,
|
|
38031
|
+
readdirSync as readdirSync8,
|
|
38032
|
+
readFileSync as readFileSync20,
|
|
38033
|
+
realpathSync as realpathSync3,
|
|
38034
|
+
rmSync as rmSync11,
|
|
38035
|
+
statSync as statSync7,
|
|
38036
|
+
writeFileSync as writeFileSync15
|
|
38037
|
+
} from "node:fs";
|
|
38038
|
+
import { basename as basename3, dirname as dirname16, join as join22, resolve as resolve5 } from "node:path";
|
|
37949
38039
|
function runsDirectory() {
|
|
37950
38040
|
return join22(runtimeDirectory(), "runs");
|
|
37951
38041
|
}
|
|
37952
|
-
function
|
|
37953
|
-
|
|
38042
|
+
function realAsFarAsItExists(path) {
|
|
38043
|
+
const missing = [];
|
|
38044
|
+
let at = path;
|
|
38045
|
+
for (;; ) {
|
|
38046
|
+
try {
|
|
38047
|
+
return join22(realpathSync3.native(at), ...missing);
|
|
38048
|
+
} catch {
|
|
38049
|
+
const parent = dirname16(at);
|
|
38050
|
+
if (parent === at) {
|
|
38051
|
+
return path;
|
|
38052
|
+
}
|
|
38053
|
+
missing.unshift(basename3(at));
|
|
38054
|
+
at = parent;
|
|
38055
|
+
}
|
|
38056
|
+
}
|
|
38057
|
+
}
|
|
38058
|
+
function projectKey(projectPath) {
|
|
38059
|
+
const real = realAsFarAsItExists(resolve5(projectPath)).replace(/[\\/]+$/, "");
|
|
38060
|
+
const folded = process.platform === "win32" || process.platform === "darwin" ? real.toLowerCase() : real;
|
|
38061
|
+
return createHash5("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
|
|
37954
38062
|
}
|
|
37955
|
-
function
|
|
37956
|
-
return
|
|
38063
|
+
function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
38064
|
+
return join22(directory, "runs", `note-${projectKey(projectPath)}.json`);
|
|
37957
38065
|
}
|
|
37958
38066
|
function openTranscript(startedAt) {
|
|
37959
38067
|
mkdirSync13(runsDirectory(), { recursive: true });
|
|
37960
38068
|
const path = join22(runsDirectory(), `run-${startedAt}.log`);
|
|
37961
38069
|
return { path, fd: openSync2(path, "a") };
|
|
37962
38070
|
}
|
|
37963
|
-
function
|
|
37964
|
-
|
|
37965
|
-
writeFileSync15(recordPath(), JSON.stringify(record, null, 2), "utf8");
|
|
37966
|
-
}
|
|
37967
|
-
function editorNotePath() {
|
|
37968
|
-
return join22(runsDirectory(), "editor-run.json");
|
|
38071
|
+
function editorNotePath(projectPath, directory = runtimeDirectory()) {
|
|
38072
|
+
return join22(directory, "runs", `editor-${projectKey(projectPath)}.json`);
|
|
37969
38073
|
}
|
|
37970
38074
|
function writeEditorRunNote(note) {
|
|
37971
38075
|
mkdirSync13(runsDirectory(), { recursive: true });
|
|
37972
|
-
writeFileSync15(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
|
|
38076
|
+
writeFileSync15(editorNotePath(note.projectPath), JSON.stringify(note, null, 2), "utf8");
|
|
37973
38077
|
}
|
|
37974
|
-
function readEditorRunNote() {
|
|
38078
|
+
function readEditorRunNote(projectPath) {
|
|
37975
38079
|
for (const directory of runtimeDirectories()) {
|
|
37976
|
-
const note = editorNoteAt(join22(directory, "runs", "editor-run.json"));
|
|
37977
|
-
if (note !== null) {
|
|
38080
|
+
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join22(directory, "runs", "editor-run.json"));
|
|
38081
|
+
if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
|
|
37978
38082
|
return note;
|
|
37979
38083
|
}
|
|
37980
38084
|
}
|
|
@@ -37999,29 +38103,38 @@ function editorNoteAt(path) {
|
|
|
37999
38103
|
}
|
|
38000
38104
|
return { projectPath, transcript, startedAt };
|
|
38001
38105
|
}
|
|
38002
|
-
function
|
|
38003
|
-
const
|
|
38004
|
-
|
|
38005
|
-
|
|
38006
|
-
|
|
38106
|
+
function readRunRecord(projectPath) {
|
|
38107
|
+
for (const directory of runtimeDirectories()) {
|
|
38108
|
+
const own = recordAt(runRecordPath(projectPath, directory));
|
|
38109
|
+
if (own !== null) {
|
|
38110
|
+
return own;
|
|
38111
|
+
}
|
|
38112
|
+
const shared = recordAt(join22(directory, "runs", SHARED_NOTE));
|
|
38113
|
+
if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
|
|
38114
|
+
return shared;
|
|
38115
|
+
}
|
|
38007
38116
|
}
|
|
38008
|
-
|
|
38009
|
-
...record,
|
|
38010
|
-
...ending.exitCode === null ? {} : { exitCode: ending.exitCode },
|
|
38011
|
-
...ending.exitSignal === null ? {} : { exitSignal: ending.exitSignal }
|
|
38012
|
-
};
|
|
38013
|
-
try {
|
|
38014
|
-
writeFileSync15(path, JSON.stringify(written, null, 2), "utf8");
|
|
38015
|
-
} catch {}
|
|
38117
|
+
return null;
|
|
38016
38118
|
}
|
|
38017
|
-
function
|
|
38018
|
-
|
|
38019
|
-
|
|
38020
|
-
|
|
38021
|
-
|
|
38119
|
+
function everyRunRecord() {
|
|
38120
|
+
const found = [];
|
|
38121
|
+
for (const directory of runtimeDirectories()) {
|
|
38122
|
+
let entries;
|
|
38123
|
+
try {
|
|
38124
|
+
entries = readdirSync8(join22(directory, "runs"));
|
|
38125
|
+
} catch {
|
|
38126
|
+
continue;
|
|
38127
|
+
}
|
|
38128
|
+
for (const entry of entries) {
|
|
38129
|
+
if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
|
|
38130
|
+
const record = recordAt(join22(directory, "runs", entry));
|
|
38131
|
+
if (record !== null) {
|
|
38132
|
+
found.push(record);
|
|
38133
|
+
}
|
|
38134
|
+
}
|
|
38022
38135
|
}
|
|
38023
38136
|
}
|
|
38024
|
-
return
|
|
38137
|
+
return found.sort((one, other) => other.startedAt - one.startedAt);
|
|
38025
38138
|
}
|
|
38026
38139
|
function recordAt(path) {
|
|
38027
38140
|
let parsed;
|
|
@@ -38180,7 +38293,7 @@ function judgeRun(record, running, needed) {
|
|
|
38180
38293
|
if (wanted !== null && !said.includes(wanted)) {
|
|
38181
38294
|
return false;
|
|
38182
38295
|
}
|
|
38183
|
-
if (running.kind === "commandLine" &&
|
|
38296
|
+
if (running.kind === "commandLine" && readCommandLine(running.text).editor) {
|
|
38184
38297
|
return false;
|
|
38185
38298
|
}
|
|
38186
38299
|
if (record.projectPath === "") {
|
|
@@ -38192,15 +38305,32 @@ function judgeRun(record, running, needed) {
|
|
|
38192
38305
|
const project = process.platform === "win32" ? record.projectPath.toLowerCase() : record.projectPath;
|
|
38193
38306
|
return said.includes(project);
|
|
38194
38307
|
}
|
|
38195
|
-
function
|
|
38196
|
-
|
|
38197
|
-
|
|
38198
|
-
|
|
38199
|
-
|
|
38308
|
+
function stillTheAnnouncedGame(pid, projectPath, announcedAt) {
|
|
38309
|
+
return judgeAnnouncedGame(runningAs(pid), projectPath, announcedAt);
|
|
38310
|
+
}
|
|
38311
|
+
function judgeAnnouncedGame(running, projectPath, announcedAt) {
|
|
38312
|
+
if (running?.kind !== "commandLine" || running.startedAt === undefined) {
|
|
38313
|
+
return null;
|
|
38314
|
+
}
|
|
38315
|
+
if (running.startedAt - announcedAt > STARTED_AFTER_ANNOUNCING_MS) {
|
|
38316
|
+
return null;
|
|
38317
|
+
}
|
|
38318
|
+
const read = readCommandLine(running.text);
|
|
38319
|
+
if (read.editor || read.projectPath === null || !isSameDirectory(read.projectPath, projectPath)) {
|
|
38320
|
+
return null;
|
|
38321
|
+
}
|
|
38322
|
+
return read;
|
|
38323
|
+
}
|
|
38324
|
+
function clearRunRecord(projectPath) {
|
|
38325
|
+
for (const directory of runtimeDirectories()) {
|
|
38326
|
+
const shared = join22(directory, "runs", SHARED_NOTE);
|
|
38327
|
+
const sharedRecord = recordAt(shared);
|
|
38328
|
+
const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
|
|
38329
|
+
for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
|
|
38330
|
+
try {
|
|
38331
|
+
rmSync11(path, { force: true });
|
|
38332
|
+
} catch {}
|
|
38200
38333
|
}
|
|
38201
|
-
try {
|
|
38202
|
-
rmSync11(path);
|
|
38203
|
-
} catch {}
|
|
38204
38334
|
}
|
|
38205
38335
|
}
|
|
38206
38336
|
function ageMs(path, now) {
|
|
@@ -38212,7 +38342,7 @@ function ageMs(path, now) {
|
|
|
38212
38342
|
}
|
|
38213
38343
|
function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
38214
38344
|
const directory = runsDirectory();
|
|
38215
|
-
const
|
|
38345
|
+
const kept = new Set(everyRunRecord().map((record) => record.transcript));
|
|
38216
38346
|
let entries;
|
|
38217
38347
|
try {
|
|
38218
38348
|
entries = readdirSync8(directory);
|
|
@@ -38224,7 +38354,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
38224
38354
|
continue;
|
|
38225
38355
|
}
|
|
38226
38356
|
const path = join22(directory, entry);
|
|
38227
|
-
if (path
|
|
38357
|
+
if (kept.has(path) || ageMs(path, now) < keepMs) {
|
|
38228
38358
|
continue;
|
|
38229
38359
|
}
|
|
38230
38360
|
try {
|
|
@@ -38232,15 +38362,15 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
38232
38362
|
} catch {}
|
|
38233
38363
|
}
|
|
38234
38364
|
}
|
|
38235
|
-
var
|
|
38365
|
+
var SHARED_NOTE = "run.json", SAME_RUN_WINDOW_MS = 1e4;
|
|
38236
38366
|
var init_run_record = __esm(() => {
|
|
38367
|
+
init_paths();
|
|
38237
38368
|
init_process_children();
|
|
38238
38369
|
init_runtime_client();
|
|
38239
|
-
AN_EDITOR = /(?:^|\s)(?:-e|--editor)(?:\s|$)/;
|
|
38240
38370
|
});
|
|
38241
38371
|
|
|
38242
38372
|
// src/server.ts
|
|
38243
|
-
import { execFile as execFile7, spawn } from "node:child_process";
|
|
38373
|
+
import { execFile as execFile7, spawn as spawn2 } from "node:child_process";
|
|
38244
38374
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
38245
38375
|
import {
|
|
38246
38376
|
appendFileSync,
|
|
@@ -38259,9 +38389,9 @@ import {
|
|
|
38259
38389
|
statSync as statSync8
|
|
38260
38390
|
} from "node:fs";
|
|
38261
38391
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
38262
|
-
import { basename as basename4, dirname as
|
|
38392
|
+
import { basename as basename4, dirname as dirname17, extname as extname2, isAbsolute as isAbsolute3, join as join23, normalize as normalize3, relative as relative3 } from "node:path";
|
|
38263
38393
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
38264
|
-
import { fileURLToPath as
|
|
38394
|
+
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
38265
38395
|
import { promisify as promisify7 } from "node:util";
|
|
38266
38396
|
function captureDestinationRefusal(outputPath, projects) {
|
|
38267
38397
|
if (/^(res|user):\/\//.test(outputPath)) {
|
|
@@ -38273,9 +38403,9 @@ function captureDestinationRefusal(outputPath, projects) {
|
|
|
38273
38403
|
if (extname2(outputPath).toLowerCase() !== ".png") {
|
|
38274
38404
|
return `outputPath ${outputPath} does not end in .png, which is what a capture is.`;
|
|
38275
38405
|
}
|
|
38276
|
-
const directory = realPathOr(
|
|
38406
|
+
const directory = realPathOr(dirname17(outputPath));
|
|
38277
38407
|
if (!existsSync15(directory)) {
|
|
38278
|
-
return `outputPath's directory ${
|
|
38408
|
+
return `outputPath's directory ${dirname17(outputPath)} does not exist; a capture creates no directories.`;
|
|
38279
38409
|
}
|
|
38280
38410
|
const where = join23(directory, basename4(outputPath));
|
|
38281
38411
|
const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
@@ -38330,7 +38460,7 @@ function stillRunning(run) {
|
|
|
38330
38460
|
if (typeof run.endedHere === "string") {
|
|
38331
38461
|
return false;
|
|
38332
38462
|
}
|
|
38333
|
-
if (!run.throughEditor && run.
|
|
38463
|
+
if (!run.throughEditor && run.pid !== null) {
|
|
38334
38464
|
return alive(run.pid);
|
|
38335
38465
|
}
|
|
38336
38466
|
if (run.announcedPid !== undefined && !alive(run.announcedPid)) {
|
|
@@ -38354,10 +38484,10 @@ function runIsUp(run, editorSays, now = Date.now()) {
|
|
|
38354
38484
|
return editorSays ?? stillRunning(run);
|
|
38355
38485
|
}
|
|
38356
38486
|
function noCodeWillCome(run) {
|
|
38357
|
-
return !exited(run)
|
|
38487
|
+
return !exited(run);
|
|
38358
38488
|
}
|
|
38359
|
-
async function untilGone(
|
|
38360
|
-
const gone = () =>
|
|
38489
|
+
async function untilGone(pids, withinMs = STOP_WAIT_MS) {
|
|
38490
|
+
const gone = () => pids.every((pid) => !alive(pid));
|
|
38361
38491
|
const deadline = Date.now() + withinMs;
|
|
38362
38492
|
while (!gone() && Date.now() < deadline) {
|
|
38363
38493
|
await delay3(50);
|
|
@@ -38365,6 +38495,9 @@ async function untilGone(handle, pids, withinMs = STOP_WAIT_MS) {
|
|
|
38365
38495
|
return gone();
|
|
38366
38496
|
}
|
|
38367
38497
|
function endedWithoutACode(run) {
|
|
38498
|
+
if (!run.throughEditor && !run.pickedUp) {
|
|
38499
|
+
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.";
|
|
38500
|
+
}
|
|
38368
38501
|
if (!run.throughEditor) {
|
|
38369
38502
|
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.";
|
|
38370
38503
|
}
|
|
@@ -38377,12 +38510,41 @@ function endedWithoutACode(run) {
|
|
|
38377
38510
|
}
|
|
38378
38511
|
return `The editor stopped playing this run. ${kept} What it printed is below.`;
|
|
38379
38512
|
}
|
|
38513
|
+
function stopVerdict(stop) {
|
|
38514
|
+
if (!stop.wasRunning) {
|
|
38515
|
+
return {
|
|
38516
|
+
stopped: true,
|
|
38517
|
+
endedPid: stop.endedPid,
|
|
38518
|
+
clean: stop.errors === 0,
|
|
38519
|
+
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.",
|
|
38520
|
+
withChildren: false
|
|
38521
|
+
};
|
|
38522
|
+
}
|
|
38523
|
+
if (stop.ending === "refused") {
|
|
38524
|
+
return {
|
|
38525
|
+
stopped: false,
|
|
38526
|
+
notSignalled: stop.endedPid,
|
|
38527
|
+
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.`,
|
|
38528
|
+
withChildren: false
|
|
38529
|
+
};
|
|
38530
|
+
}
|
|
38531
|
+
return {
|
|
38532
|
+
stopped: true,
|
|
38533
|
+
endedPid: stop.endedPid,
|
|
38534
|
+
clean: stop.errors === 0,
|
|
38535
|
+
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.`}`,
|
|
38536
|
+
withChildren: true
|
|
38537
|
+
};
|
|
38538
|
+
}
|
|
38380
38539
|
function endedPreviousRun(ended) {
|
|
38381
|
-
if (ended === null) {
|
|
38540
|
+
if (ended === null || ended.ending === "refused") {
|
|
38382
38541
|
return;
|
|
38383
38542
|
}
|
|
38384
38543
|
return ended.pid ?? true;
|
|
38385
38544
|
}
|
|
38545
|
+
function previousRunLeft(ended) {
|
|
38546
|
+
return ended?.ending === "refused" && ended.pid !== null ? ended.pid : undefined;
|
|
38547
|
+
}
|
|
38386
38548
|
function engineNamesOf(executable) {
|
|
38387
38549
|
return CONSOLE_WRAPPER.test(executable) ? [executable, executable.replace(CONSOLE_WRAPPER, "$1")] : [executable];
|
|
38388
38550
|
}
|
|
@@ -38422,6 +38584,17 @@ function aboutTheChildren(ended, throughEditor) {
|
|
|
38422
38584
|
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.`;
|
|
38423
38585
|
return `${endedToo}${leftToo}`;
|
|
38424
38586
|
}
|
|
38587
|
+
function childrenFields(ended) {
|
|
38588
|
+
if (ended === null) {
|
|
38589
|
+
return {};
|
|
38590
|
+
}
|
|
38591
|
+
return {
|
|
38592
|
+
endedChildren: ended.ended.map((one) => one.pid),
|
|
38593
|
+
identifiedBy: ended.ended.length === 0 ? undefined : Object.fromEntries(ended.ended.map((one) => [String(one.pid), one.identifiedBy])),
|
|
38594
|
+
childrenLeft: ended.left.length === 0 ? undefined : ended.left,
|
|
38595
|
+
childrenUnknown: ended.unknown === false ? undefined : true
|
|
38596
|
+
};
|
|
38597
|
+
}
|
|
38425
38598
|
function uidsLeftNote(left) {
|
|
38426
38599
|
if (left === 0) {
|
|
38427
38600
|
return "No scene was written: this reads the project and imports it, and does not resave anything.";
|
|
@@ -38429,12 +38602,24 @@ function uidsLeftNote(left) {
|
|
|
38429
38602
|
const them = left === 1 ? "it" : "them";
|
|
38430
38603
|
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.`;
|
|
38431
38604
|
}
|
|
38605
|
+
function leftRunningNote(stranded) {
|
|
38606
|
+
if (stranded.length === 0) {
|
|
38607
|
+
return "";
|
|
38608
|
+
}
|
|
38609
|
+
const named = stranded.map((one) => one.protocol === null ? `pid ${one.pid}` : `pid ${one.pid} on protocol ${one.protocol}`).join(", ");
|
|
38610
|
+
const [only] = stranded;
|
|
38611
|
+
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.`;
|
|
38612
|
+
}
|
|
38432
38613
|
function endedToStartThis(ended) {
|
|
38433
38614
|
if (ended === null) {
|
|
38434
38615
|
return "";
|
|
38435
38616
|
}
|
|
38436
38617
|
const which = ended.pid === null ? "The game the editor was playing" : `The run that was going, pid ${ended.pid},`;
|
|
38437
|
-
|
|
38618
|
+
if (ended.ending === "refused") {
|
|
38619
|
+
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.`;
|
|
38620
|
+
}
|
|
38621
|
+
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.` : "";
|
|
38622
|
+
return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.${after}`;
|
|
38438
38623
|
}
|
|
38439
38624
|
function runtimeVerdict(endpoint, after) {
|
|
38440
38625
|
if (endpoint !== null) {
|
|
@@ -39739,11 +39924,11 @@ class GodotServer {
|
|
|
39739
39924
|
if (!engine.ok) {
|
|
39740
39925
|
return engine.response;
|
|
39741
39926
|
}
|
|
39742
|
-
const folder =
|
|
39927
|
+
const folder = dirname17(output.absolutePath);
|
|
39743
39928
|
try {
|
|
39744
39929
|
mkdirSync14(folder, { recursive: true });
|
|
39745
39930
|
} catch (error) {
|
|
39746
|
-
return this.createErrorResponse(`Cannot create ${
|
|
39931
|
+
return this.createErrorResponse(`Cannot create ${dirname17(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
39747
39932
|
}
|
|
39748
39933
|
const debug = readBoolean(args, "debug") ?? false;
|
|
39749
39934
|
const exportLogs = mkdtempSync3(join23(tmpdir5(), "gdharness-export-"));
|
|
@@ -39888,7 +40073,7 @@ class GodotServer {
|
|
|
39888
40073
|
} finally {
|
|
39889
40074
|
discard(reportsDir, userData);
|
|
39890
40075
|
try {
|
|
39891
|
-
rmdirSync(
|
|
40076
|
+
rmdirSync(dirname17(reportsDir));
|
|
39892
40077
|
} catch {}
|
|
39893
40078
|
}
|
|
39894
40079
|
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
|
|
@@ -40116,7 +40301,12 @@ class GodotServer {
|
|
|
40116
40301
|
return anEditorIsStillComing(listeningSince, this.launchedEditorIsUp());
|
|
40117
40302
|
}
|
|
40118
40303
|
launchedEditorIsUp() {
|
|
40119
|
-
|
|
40304
|
+
const launched = this.launchedEditor;
|
|
40305
|
+
if (launched === null || !alive(launched.pid)) {
|
|
40306
|
+
return false;
|
|
40307
|
+
}
|
|
40308
|
+
const now = startTimesOf([launched.pid]).get(launched.pid);
|
|
40309
|
+
return launched.startedAt === null || now === undefined || now === launched.startedAt;
|
|
40120
40310
|
}
|
|
40121
40311
|
dap() {
|
|
40122
40312
|
const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
|
|
@@ -40477,9 +40667,9 @@ class GodotServer {
|
|
|
40477
40667
|
}
|
|
40478
40668
|
async openAnEditor(engine, projectPath, ports) {
|
|
40479
40669
|
clearEditorLog(projectPath);
|
|
40480
|
-
const
|
|
40481
|
-
|
|
40482
|
-
|
|
40670
|
+
const launched = await launchOutsideTheTree({
|
|
40671
|
+
command: engine,
|
|
40672
|
+
args: editorArguments(projectPath, ports, editorLogPath(projectPath)),
|
|
40483
40673
|
env: {
|
|
40484
40674
|
...process.env,
|
|
40485
40675
|
GDHARNESS_RUNTIME_DIR: runtimeDirectory(),
|
|
@@ -40488,23 +40678,15 @@ class GodotServer {
|
|
|
40488
40678
|
[OPENED_BY_A_SERVER]: "1"
|
|
40489
40679
|
}
|
|
40490
40680
|
});
|
|
40491
|
-
if (
|
|
40492
|
-
|
|
40493
|
-
}
|
|
40494
|
-
const started = await new Promise((resolve) => {
|
|
40495
|
-
editor.once("spawn", () => {
|
|
40496
|
-
resolve(null);
|
|
40497
|
-
});
|
|
40498
|
-
editor.once("error", (error) => {
|
|
40499
|
-
resolve(error.message);
|
|
40500
|
-
});
|
|
40501
|
-
});
|
|
40502
|
-
if (started !== null) {
|
|
40503
|
-
return { pid: null, error: started };
|
|
40681
|
+
if ("error" in launched) {
|
|
40682
|
+
return { pid: null, error: launched.error };
|
|
40504
40683
|
}
|
|
40505
|
-
|
|
40506
|
-
this.launchedEditor =
|
|
40507
|
-
|
|
40684
|
+
writeEditorLogNote(projectPath, launched.pid);
|
|
40685
|
+
this.launchedEditor = {
|
|
40686
|
+
pid: launched.pid,
|
|
40687
|
+
startedAt: startTimesOf([launched.pid]).get(launched.pid) ?? null
|
|
40688
|
+
};
|
|
40689
|
+
return { pid: launched.pid, error: null };
|
|
40508
40690
|
}
|
|
40509
40691
|
async portsForAnEditor() {
|
|
40510
40692
|
const lsp = portFromEnvOrNull("GDHARNESS_LSP_PORT") ?? await freePort(DEFAULT_LSP_PORT);
|
|
@@ -40553,8 +40735,10 @@ class GodotServer {
|
|
|
40553
40735
|
const before = this.currentRun();
|
|
40554
40736
|
if (before !== null && await this.runStillGoing(before)) {
|
|
40555
40737
|
this.logDebug("Ending the running game before starting another");
|
|
40556
|
-
ended = {
|
|
40557
|
-
|
|
40738
|
+
ended = {
|
|
40739
|
+
pid: before.pid ?? this.announcedPidOf(before) ?? null,
|
|
40740
|
+
ending: await this.endActiveGame("editor_run start, which ends the run that was going before it starts another", true)
|
|
40741
|
+
};
|
|
40558
40742
|
}
|
|
40559
40743
|
const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
|
|
40560
40744
|
const wereHereFirst = this.everyRuntimeOfOurs();
|
|
@@ -40574,25 +40758,30 @@ class GodotServer {
|
|
|
40574
40758
|
userArgs: given.value
|
|
40575
40759
|
});
|
|
40576
40760
|
this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
40577
|
-
const started = this.
|
|
40761
|
+
const started = await this.launchKeptGame(engine.value, cmdArgs, project.value.path, needsItsOwnProcess(asked.value) ? environmentFor({ ...asked.value, runtimeDirectory: runtimeDirectory() }) : undefined);
|
|
40762
|
+
if ("error" in started) {
|
|
40763
|
+
return this.createErrorResponse(`The game could not be started: ${started.error}`, [
|
|
40764
|
+
"editor_status says which Godot this server runs and whether it answers"
|
|
40765
|
+
]);
|
|
40766
|
+
}
|
|
40578
40767
|
started.announcedBefore = alreadyPlaying;
|
|
40579
40768
|
this.activeProcess = started;
|
|
40580
40769
|
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." : "";
|
|
40581
40770
|
const endedForThis = endedToStartThis(ended);
|
|
40582
40771
|
const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
|
|
40583
|
-
const
|
|
40584
|
-
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.`;
|
|
40772
|
+
const alsoRunning = leftRunningNote(stranded);
|
|
40585
40773
|
return this.jsonTextResponse({
|
|
40586
40774
|
started: true,
|
|
40587
40775
|
through: "gdharness",
|
|
40588
40776
|
alsoRunning: stranded.length === 0 ? undefined : stranded.map((one) => one.pid),
|
|
40589
|
-
pid: started.
|
|
40777
|
+
pid: started.pid,
|
|
40590
40778
|
arguments: cmdArgs,
|
|
40591
40779
|
transcript: started.transcript,
|
|
40592
40780
|
refreshedClasses: refreshed.value,
|
|
40593
40781
|
...scanWaitAnswer(scanned),
|
|
40594
40782
|
savesNote: asked.value.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
|
|
40595
40783
|
endedPreviousRun: endedPreviousRun(ended),
|
|
40784
|
+
previousRunLeft: previousRunLeft(ended),
|
|
40596
40785
|
runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
|
|
40597
40786
|
message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}${alsoRunning}`
|
|
40598
40787
|
});
|
|
@@ -40701,7 +40890,6 @@ class GodotServer {
|
|
|
40701
40890
|
closeSync2(transcript.fd);
|
|
40702
40891
|
const playAnswer = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
|
|
40703
40892
|
const played = {
|
|
40704
|
-
process: null,
|
|
40705
40893
|
pid: null,
|
|
40706
40894
|
log,
|
|
40707
40895
|
transcript: transcript.path,
|
|
@@ -40726,6 +40914,7 @@ class GodotServer {
|
|
|
40726
40914
|
...scanWaitAnswer(scanned),
|
|
40727
40915
|
debugPort: readNumber(playAnswer, "debugPort"),
|
|
40728
40916
|
endedPreviousRun: endedPreviousRun(ended),
|
|
40917
|
+
previousRunLeft: previousRunLeft(ended),
|
|
40729
40918
|
breakpoints: breakpoints.applied.length === 0 ? undefined : this.breakpointsAsProjectSpells(projectPath, breakpoints.applied),
|
|
40730
40919
|
breakpointsRefused: breakpoints.refused.length === 0 ? undefined : breakpoints.refused.map((one) => ({
|
|
40731
40920
|
scriptPath: projectSpelling(projectPath, one.scriptPath),
|
|
@@ -40800,28 +40989,41 @@ class GodotServer {
|
|
|
40800
40989
|
}
|
|
40801
40990
|
async endActiveGame(reason, going) {
|
|
40802
40991
|
const running = this.activeProcess;
|
|
40803
|
-
const
|
|
40804
|
-
|
|
40992
|
+
const project = running?.projectPath ?? this.ourProject();
|
|
40993
|
+
const clearTheRecord = () => {
|
|
40994
|
+
if (project !== null) {
|
|
40995
|
+
clearRunRecord(project);
|
|
40996
|
+
}
|
|
40997
|
+
};
|
|
40998
|
+
const aKeeperHoldsIt = running !== null && going && !running.throughEditor && running.pid !== null;
|
|
40999
|
+
if (!aKeeperHoldsIt) {
|
|
41000
|
+
clearTheRecord();
|
|
41001
|
+
}
|
|
40805
41002
|
if (!running || !going) {
|
|
40806
|
-
return
|
|
41003
|
+
return "gone";
|
|
40807
41004
|
}
|
|
40808
41005
|
running.endedHere = reason;
|
|
40809
41006
|
const announced = this.announcedPidOf(running);
|
|
40810
41007
|
if (running.throughEditor) {
|
|
40811
41008
|
await this.handleViaBridge("stop_playing", {});
|
|
40812
|
-
return await untilGone(
|
|
40813
|
-
}
|
|
40814
|
-
if (running.process !== null) {
|
|
40815
|
-
running.process.kill();
|
|
40816
|
-
return await untilGone(running.process, announced === undefined ? [] : [announced]);
|
|
41009
|
+
return await untilGone(announced === undefined ? [] : [announced]) ? "gone" : "lingering";
|
|
40817
41010
|
}
|
|
40818
41011
|
if (running.pid === null) {
|
|
40819
|
-
return
|
|
40820
|
-
}
|
|
40821
|
-
|
|
41012
|
+
return "gone";
|
|
41013
|
+
}
|
|
41014
|
+
const startedAs = {
|
|
41015
|
+
pid: running.pid,
|
|
41016
|
+
transcript: running.transcript ?? "",
|
|
41017
|
+
startedAt: running.startedAt,
|
|
41018
|
+
projectPath: running.projectPath ?? "",
|
|
41019
|
+
arguments: [],
|
|
41020
|
+
...running.command === undefined ? {} : { command: running.command }
|
|
41021
|
+
};
|
|
41022
|
+
if (!stillTheRecordedRun(startedAs)) {
|
|
41023
|
+
clearTheRecord();
|
|
40822
41024
|
running.endedHere = null;
|
|
40823
|
-
running.log.record("warning", `This run was not ended here: pid ${running.pid} no longer answers as the run
|
|
40824
|
-
return
|
|
41025
|
+
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.`);
|
|
41026
|
+
return "refused";
|
|
40825
41027
|
}
|
|
40826
41028
|
const signalled = runningAs(running.pid);
|
|
40827
41029
|
const described = signalled === null ? "nothing it would name" : `${signalled.kind}: ${signalled.text}`;
|
|
@@ -40831,12 +41033,20 @@ class GodotServer {
|
|
|
40831
41033
|
} catch {
|
|
40832
41034
|
landed = false;
|
|
40833
41035
|
}
|
|
40834
|
-
const gone = await untilGone(
|
|
41036
|
+
const gone = await untilGone([running.pid]);
|
|
41037
|
+
if (gone) {
|
|
41038
|
+
await this.untilTheExitIsRecorded(running);
|
|
41039
|
+
}
|
|
41040
|
+
if (landed && gone) {
|
|
41041
|
+
running.exitCode = null;
|
|
41042
|
+
running.exitSignal = "SIGTERM";
|
|
41043
|
+
}
|
|
41044
|
+
clearTheRecord();
|
|
40835
41045
|
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}.`);
|
|
40836
|
-
return gone;
|
|
41046
|
+
return gone ? "gone" : "lingering";
|
|
40837
41047
|
}
|
|
40838
41048
|
spawnGame(godotPath, cmdArgs, env) {
|
|
40839
|
-
const child =
|
|
41049
|
+
const child = spawn2(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
|
|
40840
41050
|
const cancelled = callSignal();
|
|
40841
41051
|
if (cancelled !== undefined) {
|
|
40842
41052
|
const end = () => {
|
|
@@ -40888,62 +41098,33 @@ class GodotServer {
|
|
|
40888
41098
|
});
|
|
40889
41099
|
return started;
|
|
40890
41100
|
}
|
|
40891
|
-
|
|
41101
|
+
async launchKeptGame(godotPath, cmdArgs, projectPath, env) {
|
|
40892
41102
|
const startedAt = Date.now();
|
|
40893
41103
|
const transcript = openTranscript(startedAt);
|
|
40894
|
-
|
|
40895
|
-
|
|
40896
|
-
|
|
40897
|
-
|
|
40898
|
-
|
|
40899
|
-
|
|
40900
|
-
|
|
40901
|
-
|
|
40902
|
-
|
|
41104
|
+
closeSync2(transcript.fd);
|
|
41105
|
+
const launched = await launchOutsideTheTree({
|
|
41106
|
+
command: godotPath,
|
|
41107
|
+
args: cmdArgs,
|
|
41108
|
+
...env === undefined ? {} : { env },
|
|
41109
|
+
run: { transcript: transcript.path, startedAt, projectPath }
|
|
41110
|
+
});
|
|
41111
|
+
if ("error" in launched) {
|
|
41112
|
+
return launched;
|
|
40903
41113
|
}
|
|
40904
|
-
|
|
40905
|
-
|
|
40906
|
-
|
|
40907
|
-
|
|
40908
|
-
pid: child.pid ?? null,
|
|
40909
|
-
log,
|
|
41114
|
+
sweepTranscripts();
|
|
41115
|
+
return {
|
|
41116
|
+
pid: launched.pid,
|
|
41117
|
+
log: new GameLog,
|
|
40910
41118
|
transcript: transcript.path,
|
|
40911
41119
|
readOffset: 0,
|
|
40912
41120
|
projectPath,
|
|
40913
41121
|
startedAt,
|
|
41122
|
+
command: godotPath,
|
|
40914
41123
|
exitCode: null,
|
|
40915
41124
|
exitSignal: null,
|
|
40916
41125
|
throughEditor: false,
|
|
40917
41126
|
brokeOn: null
|
|
40918
41127
|
};
|
|
40919
|
-
child.on("exit", (code, signal) => {
|
|
40920
|
-
this.logDebug(`Godot process exited with code ${code ?? "none"}, signal ${signal ?? "none"}`);
|
|
40921
|
-
this.drainTranscript(started);
|
|
40922
|
-
log.finish();
|
|
40923
|
-
started.exitCode = code;
|
|
40924
|
-
started.exitSignal = code === null ? signal : null;
|
|
40925
|
-
if (started.pid !== null) {
|
|
40926
|
-
recordRunEnded(started.pid, { exitCode: started.exitCode, exitSignal: started.exitSignal });
|
|
40927
|
-
}
|
|
40928
|
-
});
|
|
40929
|
-
child.on("error", (err) => {
|
|
40930
|
-
console.error("Failed to start Godot process:", err);
|
|
40931
|
-
log.append("stderr", `${err.message}
|
|
40932
|
-
`);
|
|
40933
|
-
started.exitCode = -1;
|
|
40934
|
-
});
|
|
40935
|
-
if (started.pid !== null) {
|
|
40936
|
-
writeRunRecord({
|
|
40937
|
-
pid: started.pid,
|
|
40938
|
-
transcript: transcript.path,
|
|
40939
|
-
startedAt,
|
|
40940
|
-
projectPath,
|
|
40941
|
-
arguments: cmdArgs,
|
|
40942
|
-
command: godotPath
|
|
40943
|
-
});
|
|
40944
|
-
}
|
|
40945
|
-
sweepTranscripts();
|
|
40946
|
-
return started;
|
|
40947
41128
|
}
|
|
40948
41129
|
drainTranscript(game) {
|
|
40949
41130
|
if (game.transcript === null) {
|
|
@@ -40996,10 +41177,8 @@ class GodotServer {
|
|
|
40996
41177
|
this.logDebug(`Picked up an editor-played run without its adapter: ${errorMessage(error)}`);
|
|
40997
41178
|
}
|
|
40998
41179
|
const project = this.godotBridge.getStatus().projectPath ?? null;
|
|
40999
|
-
const
|
|
41000
|
-
const note = left !== null && project !== null && isSameDirectory(left.projectPath, project) ? left : null;
|
|
41180
|
+
const note = project === null ? null : readEditorRunNote(project);
|
|
41001
41181
|
const picked = {
|
|
41002
|
-
process: null,
|
|
41003
41182
|
pid: null,
|
|
41004
41183
|
log: new GameLog,
|
|
41005
41184
|
transcript: note?.transcript ?? null,
|
|
@@ -41131,22 +41310,24 @@ class GodotServer {
|
|
|
41131
41310
|
return endpoint.editorPid === undefined || endpoint.editorPid !== editor;
|
|
41132
41311
|
}
|
|
41133
41312
|
adoptRecordedRun() {
|
|
41134
|
-
const
|
|
41313
|
+
const project = this.ourProject();
|
|
41314
|
+
const record = project === null ? null : readRunRecord(project);
|
|
41135
41315
|
if (record === null || !this.couldBeOurs(record.projectPath)) {
|
|
41136
41316
|
return null;
|
|
41137
41317
|
}
|
|
41138
41318
|
const adopted = {
|
|
41139
|
-
process: null,
|
|
41140
41319
|
pid: record.pid,
|
|
41141
41320
|
log: new GameLog,
|
|
41142
41321
|
transcript: record.transcript,
|
|
41143
41322
|
readOffset: 0,
|
|
41144
41323
|
projectPath: record.projectPath === "" ? null : record.projectPath,
|
|
41145
41324
|
startedAt: record.startedAt,
|
|
41325
|
+
...record.command === undefined ? {} : { command: record.command },
|
|
41146
41326
|
exitCode: null,
|
|
41147
41327
|
exitSignal: null,
|
|
41148
41328
|
throughEditor: false,
|
|
41149
|
-
brokeOn: null
|
|
41329
|
+
brokeOn: null,
|
|
41330
|
+
pickedUp: true
|
|
41150
41331
|
};
|
|
41151
41332
|
this.drainTranscript(adopted);
|
|
41152
41333
|
if (!alive(record.pid) || !couldStillBeTheRecordedRun(record)) {
|
|
@@ -41163,18 +41344,18 @@ class GodotServer {
|
|
|
41163
41344
|
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." : "";
|
|
41164
41345
|
const live = this.aRuntimeOfOursIsAnswering();
|
|
41165
41346
|
if (live !== null) {
|
|
41166
|
-
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" +
|
|
41347
|
+
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.";
|
|
41167
41348
|
}
|
|
41168
41349
|
const unreadable = this.aRuntimeOfOursIsTooFarOff();
|
|
41169
41350
|
if (unreadable !== null) {
|
|
41170
41351
|
const behind = whichHalfIsBehind([unreadable]);
|
|
41171
|
-
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}.` +
|
|
41352
|
+
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.";
|
|
41172
41353
|
}
|
|
41173
|
-
const
|
|
41354
|
+
const mine = this.ourProject();
|
|
41355
|
+
const record = everyRunRecord().find((one) => mine === null || !isSameDirectory(one.projectPath, mine)) ?? null;
|
|
41174
41356
|
if (record === null) {
|
|
41175
41357
|
return `No game is running.${noEditor} Start one with editor_run.`;
|
|
41176
41358
|
}
|
|
41177
|
-
const mine = this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
|
|
41178
41359
|
const whose = record.projectPath === "" ? "a project it does not name" : record.projectPath;
|
|
41179
41360
|
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}`;
|
|
41180
41361
|
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.`;
|
|
@@ -41217,13 +41398,16 @@ class GodotServer {
|
|
|
41217
41398
|
];
|
|
41218
41399
|
}
|
|
41219
41400
|
couldBeOurs(project) {
|
|
41220
|
-
const
|
|
41221
|
-
const mine = this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
|
|
41401
|
+
const mine = this.ourProject();
|
|
41222
41402
|
if (mine === null) {
|
|
41223
41403
|
return false;
|
|
41224
41404
|
}
|
|
41225
41405
|
return project !== "" && isSameDirectory(mine, project);
|
|
41226
41406
|
}
|
|
41407
|
+
ourProject() {
|
|
41408
|
+
const status = this.godotBridge.getStatus();
|
|
41409
|
+
return this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
|
|
41410
|
+
}
|
|
41227
41411
|
currentRun() {
|
|
41228
41412
|
return this.activeProcess ?? this.adoptRecordedRun();
|
|
41229
41413
|
}
|
|
@@ -41281,8 +41465,8 @@ class GodotServer {
|
|
|
41281
41465
|
async runStillGoing(run, editorSays) {
|
|
41282
41466
|
if (!run.throughEditor) {
|
|
41283
41467
|
const going = stillRunning(run);
|
|
41284
|
-
if (!going && run.
|
|
41285
|
-
|
|
41468
|
+
if (!going && run.pid !== null && !exited(run) && run.endedUnwatched !== true && typeof run.endedHere !== "string" && !await this.untilTheExitIsRecorded(run)) {
|
|
41469
|
+
run.endedUnwatched = true;
|
|
41286
41470
|
}
|
|
41287
41471
|
return going;
|
|
41288
41472
|
}
|
|
@@ -41299,6 +41483,21 @@ class GodotServer {
|
|
|
41299
41483
|
}
|
|
41300
41484
|
return runIsUp(run, said);
|
|
41301
41485
|
}
|
|
41486
|
+
async untilTheExitIsRecorded(run) {
|
|
41487
|
+
const until = Date.now() + EXIT_REPORTED_WITHIN_MS;
|
|
41488
|
+
for (;; ) {
|
|
41489
|
+
const record = run.projectPath === null ? null : readRunRecord(run.projectPath);
|
|
41490
|
+
if (record !== null && record.pid === run.pid && (record.exitCode !== undefined || record.exitSignal !== undefined)) {
|
|
41491
|
+
run.exitCode = record.exitCode ?? null;
|
|
41492
|
+
run.exitSignal = record.exitSignal ?? null;
|
|
41493
|
+
return true;
|
|
41494
|
+
}
|
|
41495
|
+
if (Date.now() >= until) {
|
|
41496
|
+
return false;
|
|
41497
|
+
}
|
|
41498
|
+
await delay3(50);
|
|
41499
|
+
}
|
|
41500
|
+
}
|
|
41302
41501
|
handleEditorConsole(args) {
|
|
41303
41502
|
const status = this.godotBridge.getStatus();
|
|
41304
41503
|
const projectPath = status.projectPath ?? this.ownProject ?? undefined;
|
|
@@ -41400,7 +41599,8 @@ class GodotServer {
|
|
|
41400
41599
|
throughEditor: run.throughEditor,
|
|
41401
41600
|
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
41402
41601
|
couldAnnounce: run.projectPath !== null && existsSync15(join23(run.projectPath, RUNTIME_AUTOLOAD2.path)),
|
|
41403
|
-
endedHere: typeof run.endedHere === "string"
|
|
41602
|
+
endedHere: typeof run.endedHere === "string",
|
|
41603
|
+
pickedUp: run.pickedUp === true
|
|
41404
41604
|
}));
|
|
41405
41605
|
}
|
|
41406
41606
|
if (run.pickedUpPlaying === true) {
|
|
@@ -41426,7 +41626,7 @@ class GodotServer {
|
|
|
41426
41626
|
...hold,
|
|
41427
41627
|
endedUnwatched: run.endedUnwatched === true ? true : undefined,
|
|
41428
41628
|
endedBy: stillRunning(run) ? undefined : run.endedHere ?? null,
|
|
41429
|
-
project:
|
|
41629
|
+
project: run.throughEditor ? undefined : run.projectPath ?? undefined,
|
|
41430
41630
|
transcript: run.transcript ?? undefined,
|
|
41431
41631
|
startedAt: new Date(run.startedAt).toISOString(),
|
|
41432
41632
|
elapsedMs: Date.now() - run.startedAt,
|
|
@@ -41439,6 +41639,10 @@ class GodotServer {
|
|
|
41439
41639
|
async handleStopProject(args) {
|
|
41440
41640
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
41441
41641
|
const stopped = this.currentRun();
|
|
41642
|
+
const named = readNumber(args, "pid");
|
|
41643
|
+
if (named !== undefined && (stopped === null || named !== stopped.pid && named !== this.announcedPidOf(stopped))) {
|
|
41644
|
+
return await this.stopAnnouncedGame(named, readBoolean(args, "andChildren") === true);
|
|
41645
|
+
}
|
|
41442
41646
|
if (!stopped) {
|
|
41443
41647
|
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
41444
41648
|
}
|
|
@@ -41449,35 +41653,87 @@ class GodotServer {
|
|
|
41449
41653
|
const endedPid = stopped.pid ?? announcedPid;
|
|
41450
41654
|
const children = readBoolean(args, "andChildren") === true && wasRunning ? await this.whatTheRunStarted(stopped) : null;
|
|
41451
41655
|
this.logDebug("Stopping the running game");
|
|
41452
|
-
const
|
|
41453
|
-
const
|
|
41454
|
-
|
|
41656
|
+
const ending = await this.endActiveGame("editor_run stop", wasRunning);
|
|
41657
|
+
const refused = ending === "refused";
|
|
41658
|
+
const ended = children === null || refused ? null : endChildrenAmong(children);
|
|
41659
|
+
if (wasRunning && !refused) {
|
|
41455
41660
|
for (const pid of new Set([endedPid, announcedPid])) {
|
|
41456
41661
|
if (pid !== null) {
|
|
41457
41662
|
await announcementEnded(pid);
|
|
41458
41663
|
}
|
|
41459
41664
|
}
|
|
41460
41665
|
}
|
|
41666
|
+
const verdict = stopVerdict({
|
|
41667
|
+
wasRunning,
|
|
41668
|
+
ending,
|
|
41669
|
+
endedPid,
|
|
41670
|
+
throughEditor: stopped.throughEditor,
|
|
41671
|
+
exitSignal: stopped.exitSignal,
|
|
41672
|
+
errors: stopped.log.count("error")
|
|
41673
|
+
});
|
|
41461
41674
|
return this.jsonTextResponse({
|
|
41462
|
-
stopped:
|
|
41675
|
+
stopped: verdict.stopped,
|
|
41463
41676
|
through: stopped.throughEditor ? "editor" : "gdharness",
|
|
41464
|
-
endedPid,
|
|
41465
|
-
|
|
41466
|
-
|
|
41467
|
-
identifiedBy: ended.ended.length === 0 ? undefined : Object.fromEntries(ended.ended.map((one) => [String(one.pid), one.identifiedBy])),
|
|
41468
|
-
childrenLeft: ended.left.length === 0 ? undefined : ended.left,
|
|
41469
|
-
childrenUnknown: ended.unknown === false ? undefined : true
|
|
41470
|
-
},
|
|
41677
|
+
endedPid: verdict.endedPid,
|
|
41678
|
+
notSignalled: verdict.notSignalled,
|
|
41679
|
+
...childrenFields(ended),
|
|
41471
41680
|
exitedBeforeStop: !wasRunning,
|
|
41472
41681
|
exitCode: stopped.exitCode,
|
|
41473
41682
|
exitSignal: stopped.exitSignal ?? undefined,
|
|
41474
41683
|
errors: stopped.log.count("error"),
|
|
41475
41684
|
warnings: stopped.log.count("warning"),
|
|
41476
|
-
clean:
|
|
41477
|
-
note:
|
|
41685
|
+
clean: verdict.clean,
|
|
41686
|
+
note: verdict.withChildren ? `${verdict.note} ${aboutTheChildren(ended, stopped.throughEditor)}` : verdict.note,
|
|
41478
41687
|
entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
41479
41688
|
});
|
|
41480
41689
|
}
|
|
41690
|
+
async stopAnnouncedGame(pid, andChildren) {
|
|
41691
|
+
const announced = runtimesAnnounced();
|
|
41692
|
+
const game = [
|
|
41693
|
+
...this.allAnnouncedForOurProject(announced.running),
|
|
41694
|
+
...this.allAnnouncedForOurProject(announced.unspoken)
|
|
41695
|
+
].find((one) => one.pid === pid);
|
|
41696
|
+
if (game === undefined) {
|
|
41697
|
+
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.`);
|
|
41698
|
+
}
|
|
41699
|
+
const read = game.announcedAt === undefined ? null : stillTheAnnouncedGame(pid, game.project.path, game.announcedAt);
|
|
41700
|
+
if (read === null) {
|
|
41701
|
+
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.`);
|
|
41702
|
+
}
|
|
41703
|
+
const run = {
|
|
41704
|
+
pid,
|
|
41705
|
+
log: new GameLog,
|
|
41706
|
+
transcript: null,
|
|
41707
|
+
readOffset: 0,
|
|
41708
|
+
projectPath: game.project.path,
|
|
41709
|
+
startedAt: game.announcedAt ?? Date.now(),
|
|
41710
|
+
exitCode: null,
|
|
41711
|
+
exitSignal: null,
|
|
41712
|
+
throughEditor: false,
|
|
41713
|
+
brokeOn: null,
|
|
41714
|
+
announcedPid: pid
|
|
41715
|
+
};
|
|
41716
|
+
const children = andChildren ? await this.whatTheRunStarted(run) : null;
|
|
41717
|
+
let landed = true;
|
|
41718
|
+
try {
|
|
41719
|
+
process.kill(pid);
|
|
41720
|
+
} catch {
|
|
41721
|
+
landed = false;
|
|
41722
|
+
}
|
|
41723
|
+
const gone = await untilGone([pid]);
|
|
41724
|
+
const ended = children === null ? null : endChildrenAmong(children);
|
|
41725
|
+
if (gone) {
|
|
41726
|
+
await announcementEnded(pid);
|
|
41727
|
+
}
|
|
41728
|
+
return this.jsonTextResponse({
|
|
41729
|
+
stopped: true,
|
|
41730
|
+
through: "gdharness",
|
|
41731
|
+
endedPid: pid,
|
|
41732
|
+
...childrenFields(ended),
|
|
41733
|
+
exitedBeforeStop: !landed,
|
|
41734
|
+
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)}`
|
|
41735
|
+
});
|
|
41736
|
+
}
|
|
41481
41737
|
async whatTheRunStarted(run) {
|
|
41482
41738
|
const tree = await processTree();
|
|
41483
41739
|
if (tree === undefined) {
|
|
@@ -42056,6 +42312,7 @@ var init_server3 = __esm(() => {
|
|
|
42056
42312
|
init_issues();
|
|
42057
42313
|
init_junit();
|
|
42058
42314
|
init_lsp_client();
|
|
42315
|
+
init_outside();
|
|
42059
42316
|
init_paths();
|
|
42060
42317
|
init_ports();
|
|
42061
42318
|
init_process_children();
|
|
@@ -42071,7 +42328,7 @@ var init_server3 = __esm(() => {
|
|
|
42071
42328
|
init_tool_definitions();
|
|
42072
42329
|
init_update_check();
|
|
42073
42330
|
run7 = promisify7(execFile7);
|
|
42074
|
-
__dirname2 =
|
|
42331
|
+
__dirname2 = dirname17(fileURLToPath6(import.meta.url));
|
|
42075
42332
|
CONSOLE_WRAPPER = /_console(\.exe)?$/i;
|
|
42076
42333
|
LONGEST_BOOT_NOTED_MS = 5 * 60000;
|
|
42077
42334
|
VALUE_NAMES = {
|
|
@@ -42113,7 +42370,7 @@ var init_server3 = __esm(() => {
|
|
|
42113
42370
|
|
|
42114
42371
|
// src/cli.ts
|
|
42115
42372
|
import { existsSync as existsSync16 } from "fs";
|
|
42116
|
-
import { join as join24, resolve as
|
|
42373
|
+
import { join as join24, resolve as resolve6 } from "path";
|
|
42117
42374
|
|
|
42118
42375
|
// src/bridge-announce.ts
|
|
42119
42376
|
init_errors();
|
|
@@ -43742,6 +43999,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
43742
43999
|
| Whether this engine has a method or property, and what it takes | \`editor_classes info\` with \`member\`, which answers that one member and which class declares it rather than the whole class |
|
|
43743
44000
|
| Wait for a run to finish | \`editor_run wait\`, never a sleep or a shell loop on the pid: it answers as \`editor_output\` does, once the run is over |
|
|
43744
44001
|
| End a bench and the workers it opened | \`editor_run stop\` with \`andChildren\`, never a process listing by scene name: it ends every process under the run's, to any depth, that is a game of the project, announced as one or the project's engine run with \`--path\` on it, names them under \`endedChildren\` with what identified each, and leaves and names any other |
|
|
44002
|
+
| End a game of the project that no server is holding | \`editor_run stop\` with its \`pid\` from \`editor_status\` \`runtimes\`, never a process kill by hand: it is ended only when it announced itself for the project and its command line is the project's engine run |
|
|
43745
44003
|
| Play a game without touching the player's saves | \`editor_run start\` with \`savesIn\`, a directory for this run's \`user://\`: the editor plays with its own environment, so a run given one is started by the server, and the answer says under \`savesNote\` when the platform ignores it |
|
|
43746
44004
|
| A picture, for a person who asked to see one | \`runtime_capture\` |
|
|
43747
44005
|
|
|
@@ -43935,7 +44193,7 @@ class UsageError extends Refusal2 {
|
|
|
43935
44193
|
}
|
|
43936
44194
|
function projectArgument(at) {
|
|
43937
44195
|
const named = args.slice(at).find((value) => !value.startsWith("--"));
|
|
43938
|
-
const projectPath =
|
|
44196
|
+
const projectPath = resolve6(named ?? ".");
|
|
43939
44197
|
if (!existsSync16(join24(projectPath, "project.godot"))) {
|
|
43940
44198
|
throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
|
|
43941
44199
|
}
|