gdharness 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js
CHANGED
|
@@ -16155,7 +16155,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16155
16155
|
property: {
|
|
16156
16156
|
type: "string",
|
|
16157
16157
|
ops: ["property", "find"],
|
|
16158
|
-
description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A negative number counts from the end. A step that is not there is named, and says what was there instead.'
|
|
16158
|
+
description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that takes no arguments and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. A step that is not there is named, and says what was there instead.'
|
|
16159
16159
|
},
|
|
16160
16160
|
depth: { type: "number", ops: ["tree"], description: "tree: levels to descend. Default 3." },
|
|
16161
16161
|
includeProperties: {
|
|
@@ -16236,7 +16236,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16236
16236
|
property: {
|
|
16237
16237
|
type: "string",
|
|
16238
16238
|
ops: ["set"],
|
|
16239
|
-
description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end. The answer reads back off the same holder, so a write a typed container refused shows as an unchanged value.'
|
|
16239
|
+
description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end, and a step written as a call, "get_viewport():gui_embed_subwindows", walks through what a method taking no arguments returned. The answer reads back off the same holder, so a write a typed container refused shows as an unchanged value.'
|
|
16240
16240
|
},
|
|
16241
16241
|
value: {
|
|
16242
16242
|
ops: ["set"],
|
|
@@ -16246,7 +16246,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16246
16246
|
method: {
|
|
16247
16247
|
type: "string",
|
|
16248
16248
|
ops: ["call"],
|
|
16249
|
-
description: 'call: which one to call. Colons call through what a node holds, "_game:run:advance", the same way a property is written through them,
|
|
16249
|
+
description: 'call: which one to call. Colons call through what a node holds, "_game:run:advance", the same way a property is written through them, a list or a map is stepped into by index or key, "_game:run:roster:0:retire", and a step written as a call walks through what a method taking no arguments returned, "get_viewport():gui_get_focus_owner".'
|
|
16250
16250
|
},
|
|
16251
16251
|
args: {
|
|
16252
16252
|
type: "array",
|
|
@@ -16306,7 +16306,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16306
16306
|
pressed: {
|
|
16307
16307
|
type: "boolean",
|
|
16308
16308
|
ops: ["action", "key", "mouse_click"],
|
|
16309
|
-
description: "action, key: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. mouse_click is one raw event, so it is down unless you say false."
|
|
16309
|
+
description: "action, key: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. mouse_click is one raw event, so it is down unless you say false; a wheel button down is a whole step, since a wheel is never held."
|
|
16310
16310
|
},
|
|
16311
16311
|
strength: { type: "number", ops: ["action"], description: "action: 0 to 1. Default 1." },
|
|
16312
16312
|
keycode: {
|
|
@@ -16384,7 +16384,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16384
16384
|
requires: ["text"]
|
|
16385
16385
|
},
|
|
16386
16386
|
mouse_click: {
|
|
16387
|
-
summary: "one mouse button event at a position, carrying the buttons held once it has happened. A drag is a press held with pressed true, motions, and a release with pressed false",
|
|
16387
|
+
summary: "one mouse button event at a position, carrying the buttons held once it has happened. A drag is a press held with pressed true, motions, and a release with pressed false. A wheel button is a whole step, press and release together as a mouse sends one, and the answer says released: a lone wheel press holds the viewport on the control that took it, so every click after it lands there",
|
|
16388
16388
|
requires: ["x", "y"]
|
|
16389
16389
|
},
|
|
16390
16390
|
mouse_motion: {
|
|
@@ -16406,7 +16406,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
16406
16406
|
},
|
|
16407
16407
|
nodePath: { type: "string", ops: ["signal", "until"], description: "signal, until: the node." },
|
|
16408
16408
|
signal: { type: "string", ops: ["signal"], description: "signal: the signal name." },
|
|
16409
|
-
property: {
|
|
16409
|
+
property: {
|
|
16410
|
+
type: "string",
|
|
16411
|
+
ops: ["until"],
|
|
16412
|
+
description: 'until: the property name, or a colon path through what the node holds, "_game:run:day", read the way runtime_inspect property reads one, a step written as a call included. Walked again on every frame, so a holder the game replaces while the wait is on is followed.'
|
|
16413
|
+
},
|
|
16410
16414
|
value: { ops: ["until"], description: "until: the value to wait for, fitted to the property's type." },
|
|
16411
16415
|
says: {
|
|
16412
16416
|
type: "string",
|
|
@@ -36462,18 +36466,59 @@ var init_lsp_client = __esm(() => {
|
|
|
36462
36466
|
});
|
|
36463
36467
|
|
|
36464
36468
|
// src/process-children.ts
|
|
36465
|
-
import { execFile as execFile5 } from "node:child_process";
|
|
36469
|
+
import { execFile as execFile5, execFileSync, spawnSync as spawnSync2 } from "node:child_process";
|
|
36466
36470
|
import { basename as basename2 } from "node:path";
|
|
36467
36471
|
import { promisify as promisify5 } from "node:util";
|
|
36472
|
+
function startTimesOf(pids) {
|
|
36473
|
+
const began = new Map;
|
|
36474
|
+
if (pids.length === 0) {
|
|
36475
|
+
return began;
|
|
36476
|
+
}
|
|
36477
|
+
try {
|
|
36478
|
+
if (process.platform === "win32") {
|
|
36479
|
+
const filter = pids.map((pid) => `ProcessId=${pid}`).join(" OR ");
|
|
36480
|
+
const said = execFileSync("powershell", [
|
|
36481
|
+
"-NoProfile",
|
|
36482
|
+
"-NonInteractive",
|
|
36483
|
+
"-Command",
|
|
36484
|
+
`Get-CimInstance Win32_Process -Filter "${filter}" | ForEach-Object { "$($_.ProcessId) $(([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds())" }`
|
|
36485
|
+
], { encoding: "utf8", timeout: ASK_TIMEOUT_MS, windowsHide: true });
|
|
36486
|
+
for (const line of said.split(`
|
|
36487
|
+
`)) {
|
|
36488
|
+
const found = /^\s*(\d+)\s+(\d+)\s*$/.exec(line);
|
|
36489
|
+
if (found?.[1] !== undefined && found[2] !== undefined) {
|
|
36490
|
+
began.set(Number(found[1]), Number(found[2]));
|
|
36491
|
+
}
|
|
36492
|
+
}
|
|
36493
|
+
return began;
|
|
36494
|
+
}
|
|
36495
|
+
const said = spawnSync2("ps", ["-p", pids.join(","), "-o", "pid=,lstart="], {
|
|
36496
|
+
encoding: "utf8",
|
|
36497
|
+
timeout: ASK_TIMEOUT_MS,
|
|
36498
|
+
env: { ...process.env, LC_ALL: "C" }
|
|
36499
|
+
}).stdout;
|
|
36500
|
+
for (const line of said.split(`
|
|
36501
|
+
`)) {
|
|
36502
|
+
const found = /^\s*(\d+)\s+(.+)$/.exec(line);
|
|
36503
|
+
const at = Date.parse(found?.[2]?.trim() ?? "");
|
|
36504
|
+
if (found?.[1] !== undefined && !Number.isNaN(at)) {
|
|
36505
|
+
began.set(Number(found[1]), at);
|
|
36506
|
+
}
|
|
36507
|
+
}
|
|
36508
|
+
} catch {}
|
|
36509
|
+
return began;
|
|
36510
|
+
}
|
|
36468
36511
|
async function processTree() {
|
|
36469
36512
|
try {
|
|
36470
|
-
|
|
36471
|
-
|
|
36513
|
+
if (process.platform === "win32") {
|
|
36514
|
+
return parseProcessTable(await askWindows(), true);
|
|
36515
|
+
}
|
|
36516
|
+
return parseProcessTable(await askPosix());
|
|
36472
36517
|
} catch {
|
|
36473
36518
|
return;
|
|
36474
36519
|
}
|
|
36475
36520
|
}
|
|
36476
|
-
function parseProcessTable(printed) {
|
|
36521
|
+
function parseProcessTable(printed, started = false) {
|
|
36477
36522
|
const tree = new Map;
|
|
36478
36523
|
for (const line of printed.split(/\r?\n/)) {
|
|
36479
36524
|
const fields = /^\s*(\d+)\s+(\d+)(?:\s+(.*))?$/.exec(line);
|
|
@@ -36482,16 +36527,38 @@ function parseProcessTable(printed) {
|
|
|
36482
36527
|
}
|
|
36483
36528
|
const pid = Number(fields[1]);
|
|
36484
36529
|
const parent = Number(fields[2]);
|
|
36485
|
-
|
|
36486
|
-
|
|
36530
|
+
const rest = (fields[3] ?? "").trim();
|
|
36531
|
+
if (pid <= 0) {
|
|
36532
|
+
continue;
|
|
36533
|
+
}
|
|
36534
|
+
if (!started) {
|
|
36535
|
+
tree.set(pid, { parent, command: rest });
|
|
36536
|
+
continue;
|
|
36537
|
+
}
|
|
36538
|
+
const timed = /^(\d+)(?:\s+(.*))?$/.exec(rest);
|
|
36539
|
+
if (timed === null) {
|
|
36540
|
+
continue;
|
|
36487
36541
|
}
|
|
36542
|
+
const began = Number(timed[1]);
|
|
36543
|
+
tree.set(pid, { parent, command: (timed[2] ?? "").trim(), ...began > 0 ? { startedAt: began } : {} });
|
|
36488
36544
|
}
|
|
36489
36545
|
return tree;
|
|
36490
36546
|
}
|
|
36547
|
+
function linked(tree, child, parent) {
|
|
36548
|
+
const from = tree.get(child);
|
|
36549
|
+
const to = tree.get(parent);
|
|
36550
|
+
if (from === undefined || from.parent !== parent) {
|
|
36551
|
+
return false;
|
|
36552
|
+
}
|
|
36553
|
+
if (from.startedAt === undefined || to?.startedAt === undefined) {
|
|
36554
|
+
return true;
|
|
36555
|
+
}
|
|
36556
|
+
return from.startedAt >= to.startedAt;
|
|
36557
|
+
}
|
|
36491
36558
|
function childrenIn(tree, pid) {
|
|
36492
36559
|
const children = [];
|
|
36493
|
-
for (const
|
|
36494
|
-
if (
|
|
36560
|
+
for (const child of tree.keys()) {
|
|
36561
|
+
if (linked(tree, child, pid)) {
|
|
36495
36562
|
children.push(child);
|
|
36496
36563
|
}
|
|
36497
36564
|
}
|
|
@@ -36519,13 +36586,15 @@ function descendantsIn(tree, pid) {
|
|
|
36519
36586
|
function ancestorsIn(tree, pid, root) {
|
|
36520
36587
|
const chain = [];
|
|
36521
36588
|
const seen = new Set([pid]);
|
|
36589
|
+
let child = pid;
|
|
36522
36590
|
let parent = tree.get(pid)?.parent;
|
|
36523
|
-
while (parent !== undefined && !seen.has(parent)) {
|
|
36591
|
+
while (parent !== undefined && !seen.has(parent) && linked(tree, child, parent)) {
|
|
36524
36592
|
chain.push(parent);
|
|
36525
36593
|
if (parent === root) {
|
|
36526
36594
|
break;
|
|
36527
36595
|
}
|
|
36528
36596
|
seen.add(parent);
|
|
36597
|
+
child = parent;
|
|
36529
36598
|
parent = tree.get(parent)?.parent;
|
|
36530
36599
|
}
|
|
36531
36600
|
return chain;
|
|
@@ -36581,7 +36650,7 @@ async function askWindows() {
|
|
|
36581
36650
|
"-NoProfile",
|
|
36582
36651
|
"-NonInteractive",
|
|
36583
36652
|
"-Command",
|
|
36584
|
-
'Get-CimInstance Win32_Process | ForEach-Object { "$($_.ProcessId) $($_.ParentProcessId) $($_.CommandLine)" }'
|
|
36653
|
+
'Get-CimInstance Win32_Process | ForEach-Object { $began = 0; if ($_.CreationDate) { $began = ([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds() }; "$($_.ProcessId) $($_.ParentProcessId) $began $($_.CommandLine)" }'
|
|
36585
36654
|
], { timeout: ASK_TIMEOUT_MS, windowsHide: true, maxBuffer: 16 * 1024 * 1024 });
|
|
36586
36655
|
return stdout;
|
|
36587
36656
|
}
|
|
@@ -36813,7 +36882,7 @@ var init_restart_note = __esm(() => {
|
|
|
36813
36882
|
import { existsSync as existsSync13, readdirSync as readdirSync5, readFileSync as readFileSync15, statSync as statSync3, unlinkSync } from "node:fs";
|
|
36814
36883
|
import { createConnection as createConnection3 } from "node:net";
|
|
36815
36884
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
36816
|
-
import { join as join17, resolve as resolve4 } from "node:path";
|
|
36885
|
+
import { dirname as dirname11, join as join17, resolve as resolve4 } from "node:path";
|
|
36817
36886
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
36818
36887
|
function runtimeDirectory(variables = process.env) {
|
|
36819
36888
|
const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
|
|
@@ -36922,11 +36991,31 @@ function announcedAndGone(projectPath) {
|
|
|
36922
36991
|
const wanted = resolve4(projectPath);
|
|
36923
36992
|
return gone.filter((one) => one.noticedAt >= since && resolve4(one.project) === wanted);
|
|
36924
36993
|
}
|
|
36994
|
+
function strangersAmong(candidates, now, startTimes = startTimesOf) {
|
|
36995
|
+
const toJudge = candidates.filter((one) => {
|
|
36996
|
+
if (now - one.writtenAt < JUDGED_AFTER_MS) {
|
|
36997
|
+
return false;
|
|
36998
|
+
}
|
|
36999
|
+
const known = verdicts.get(one.file);
|
|
37000
|
+
return known === undefined || known.writtenAt !== one.writtenAt || !known.stranger && now - known.judgedAt > CONFIRMED_FOR_MS;
|
|
37001
|
+
});
|
|
37002
|
+
if (toJudge.length > 0) {
|
|
37003
|
+
const began = startTimes(toJudge.map((one) => one.pid));
|
|
37004
|
+
for (const one of toJudge) {
|
|
37005
|
+
const at = began.get(one.pid);
|
|
37006
|
+
const stranger = at !== undefined && at - one.writtenAt > STARTED_AFTER_ANNOUNCING_MS;
|
|
37007
|
+
verdicts.set(one.file, { writtenAt: one.writtenAt, judgedAt: now, stranger });
|
|
37008
|
+
}
|
|
37009
|
+
}
|
|
37010
|
+
return new Set(candidates.filter((one) => verdicts.get(one.file)?.stranger === true).map((one) => one.file));
|
|
37011
|
+
}
|
|
36925
37012
|
function announcedIn(directory) {
|
|
36926
37013
|
if (!existsSync13(directory)) {
|
|
36927
37014
|
return [];
|
|
36928
37015
|
}
|
|
36929
37016
|
const found = [];
|
|
37017
|
+
const candidates = [];
|
|
37018
|
+
const entries = [];
|
|
36930
37019
|
for (const entry of readdirSync5(directory)) {
|
|
36931
37020
|
const report = ERROR_REPORT_PATTERN.exec(entry);
|
|
36932
37021
|
if (report) {
|
|
@@ -36940,20 +37029,59 @@ function announcedIn(directory) {
|
|
|
36940
37029
|
const file = join17(directory, entry);
|
|
36941
37030
|
const pid = Number.parseInt(match[1] ?? "", 10);
|
|
36942
37031
|
const alive = processAlive(pid);
|
|
36943
|
-
|
|
37032
|
+
entries.push({ file, pid, alive });
|
|
37033
|
+
if (alive) {
|
|
37034
|
+
try {
|
|
37035
|
+
candidates.push({ file, pid, writtenAt: statSync3(file).mtimeMs });
|
|
37036
|
+
} catch {}
|
|
37037
|
+
}
|
|
37038
|
+
}
|
|
37039
|
+
const strangers = strangersAmong(candidates, Date.now());
|
|
37040
|
+
for (const { file, pid, alive } of entries) {
|
|
37041
|
+
const announced = alive && !strangers.has(file) ? parseAnnouncement(file, pid) : { kind: "rubbish" };
|
|
36944
37042
|
if (announced.kind === "rubbish") {
|
|
36945
|
-
if (!alive) {
|
|
37043
|
+
if (!alive || strangers.has(file)) {
|
|
36946
37044
|
wentAway(file, pid);
|
|
36947
37045
|
}
|
|
36948
37046
|
try {
|
|
36949
37047
|
unlinkSync(file);
|
|
36950
37048
|
} catch {}
|
|
37049
|
+
verdicts.delete(file);
|
|
36951
37050
|
continue;
|
|
36952
37051
|
}
|
|
36953
37052
|
found.push(announced);
|
|
36954
37053
|
}
|
|
37054
|
+
const held = new Set(entries.map((one) => one.file));
|
|
37055
|
+
const here = join17(directory, ".");
|
|
37056
|
+
for (const file of verdicts.keys()) {
|
|
37057
|
+
if (dirname11(file) === here && !held.has(file)) {
|
|
37058
|
+
verdicts.delete(file);
|
|
37059
|
+
}
|
|
37060
|
+
}
|
|
36955
37061
|
return found;
|
|
36956
37062
|
}
|
|
37063
|
+
async function announcementEnded(pid, directories = runtimeDirectories()) {
|
|
37064
|
+
const files = directories.map((directory) => join17(directory, `runtime-${pid}.json`)).filter((file) => existsSync13(file));
|
|
37065
|
+
const first = files[0];
|
|
37066
|
+
if (first === undefined) {
|
|
37067
|
+
return;
|
|
37068
|
+
}
|
|
37069
|
+
const until = Date.now() + ENDED_WITHIN_MS;
|
|
37070
|
+
while (processAlive(pid)) {
|
|
37071
|
+
const left = until - Date.now();
|
|
37072
|
+
if (left <= 0) {
|
|
37073
|
+
return;
|
|
37074
|
+
}
|
|
37075
|
+
await delay2(Math.min(LOOK_EVERY_MS, left));
|
|
37076
|
+
}
|
|
37077
|
+
wentAway(first, pid);
|
|
37078
|
+
for (const file of files) {
|
|
37079
|
+
try {
|
|
37080
|
+
unlinkSync(file);
|
|
37081
|
+
} catch {}
|
|
37082
|
+
verdicts.delete(file);
|
|
37083
|
+
}
|
|
37084
|
+
}
|
|
36957
37085
|
function sweepErrorReport(path, pid, now = Date.now()) {
|
|
36958
37086
|
if (processAlive(pid)) {
|
|
36959
37087
|
return;
|
|
@@ -37134,16 +37262,18 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
|
|
|
37134
37262
|
});
|
|
37135
37263
|
});
|
|
37136
37264
|
}
|
|
37137
|
-
var RUNTIME_PROTOCOL = 2, ANNOUNCEMENT_PATTERN, ERROR_REPORT_PATTERN, ERROR_REPORT_KEEP_MS, GONE_WINDOW_MS = 120000, GONE_KEPT = 32, gone, LOOK_EVERY_MS = 50, ANNOUNCE_BUDGET_MS = 5000, nextRequestId = 1;
|
|
37265
|
+
var RUNTIME_PROTOCOL = 2, ANNOUNCEMENT_PATTERN, ERROR_REPORT_PATTERN, ERROR_REPORT_KEEP_MS, GONE_WINDOW_MS = 120000, GONE_KEPT = 32, gone, STARTED_AFTER_ANNOUNCING_MS = 2000, CONFIRMED_FOR_MS = 60000, JUDGED_AFTER_MS = 60000, verdicts, ENDED_WITHIN_MS = 3000, LOOK_EVERY_MS = 50, ANNOUNCE_BUDGET_MS = 5000, nextRequestId = 1;
|
|
37138
37266
|
var init_runtime_client = __esm(() => {
|
|
37267
|
+
init_process_children();
|
|
37139
37268
|
ANNOUNCEMENT_PATTERN = /^runtime-(\d+)\.json$/;
|
|
37140
37269
|
ERROR_REPORT_PATTERN = /^runtime-(\d+)\.log$/;
|
|
37141
37270
|
ERROR_REPORT_KEEP_MS = 60 * 60 * 1000;
|
|
37142
37271
|
gone = [];
|
|
37272
|
+
verdicts = new Map;
|
|
37143
37273
|
});
|
|
37144
37274
|
|
|
37145
37275
|
// src/run-record.ts
|
|
37146
|
-
import { execFileSync } from "node:child_process";
|
|
37276
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
37147
37277
|
import { mkdirSync as mkdirSync9, openSync, readdirSync as readdirSync6, readFileSync as readFileSync16, rmSync as rmSync9, statSync as statSync4, writeFileSync as writeFileSync11 } from "node:fs";
|
|
37148
37278
|
import { basename as basename3, join as join18 } from "node:path";
|
|
37149
37279
|
function runsDirectory() {
|
|
@@ -37269,7 +37399,7 @@ function runningAs(pid) {
|
|
|
37269
37399
|
const raw = readFileSync16(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
37270
37400
|
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
37271
37401
|
}
|
|
37272
|
-
const args =
|
|
37402
|
+
const args = execFileSync2("ps", ["-p", String(pid), "-o", "args="], {
|
|
37273
37403
|
encoding: "utf8",
|
|
37274
37404
|
timeout: 15000
|
|
37275
37405
|
}).trim();
|
|
@@ -37281,20 +37411,14 @@ function runningAs(pid) {
|
|
|
37281
37411
|
function listeningPid(port) {
|
|
37282
37412
|
try {
|
|
37283
37413
|
if (process.platform === "win32") {
|
|
37284
|
-
|
|
37285
|
-
"-NoProfile",
|
|
37286
|
-
"-NonInteractive",
|
|
37287
|
-
"-Command",
|
|
37288
|
-
`(Get-NetTCPConnection -State Listen -LocalPort ${port} -ErrorAction SilentlyContinue | Select-Object -First 1).OwningProcess`
|
|
37289
|
-
], { encoding: "utf8", timeout: 15000 }).trim();
|
|
37290
|
-
return /^\d+$/.test(said) ? Number(said) : null;
|
|
37414
|
+
return listeningPidInNetstat(execFileSync2("netstat", ["-ano"], { encoding: "utf8", timeout: 15000 }), port);
|
|
37291
37415
|
}
|
|
37292
37416
|
for (const [command, args, pattern] of [
|
|
37293
37417
|
["lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], /^(\d+)/],
|
|
37294
37418
|
["ss", ["-ltnpH", `sport = :${port}`], /pid=(\d+)/]
|
|
37295
37419
|
]) {
|
|
37296
37420
|
try {
|
|
37297
|
-
const said =
|
|
37421
|
+
const said = execFileSync2(command, [...args], { encoding: "utf8", timeout: 15000 }).trim();
|
|
37298
37422
|
const found = pattern.exec(said);
|
|
37299
37423
|
if (found?.[1] !== undefined) {
|
|
37300
37424
|
return Number(found[1]);
|
|
@@ -37306,9 +37430,22 @@ function listeningPid(port) {
|
|
|
37306
37430
|
return null;
|
|
37307
37431
|
}
|
|
37308
37432
|
}
|
|
37433
|
+
function listeningPidInNetstat(printed, port) {
|
|
37434
|
+
for (const line of printed.split(/\r?\n/)) {
|
|
37435
|
+
const fields = /^\s*TCP\s+(\S+)\s+\S+\s+LISTENING\s+(\d+)\s*$/i.exec(line);
|
|
37436
|
+
if (fields === null) {
|
|
37437
|
+
continue;
|
|
37438
|
+
}
|
|
37439
|
+
const local = fields[1] ?? "";
|
|
37440
|
+
if (Number(local.slice(local.lastIndexOf(":") + 1)) === port) {
|
|
37441
|
+
return Number(fields[2]);
|
|
37442
|
+
}
|
|
37443
|
+
}
|
|
37444
|
+
return null;
|
|
37445
|
+
}
|
|
37309
37446
|
function windowsCommandLine(pid) {
|
|
37310
37447
|
try {
|
|
37311
|
-
const answer =
|
|
37448
|
+
const answer = execFileSync2("powershell", [
|
|
37312
37449
|
"-NoProfile",
|
|
37313
37450
|
"-NonInteractive",
|
|
37314
37451
|
"-Command",
|
|
@@ -37326,7 +37463,7 @@ function startedAt(pid, windowsAnswer) {
|
|
|
37326
37463
|
return Number.isNaN(when) ? null : when;
|
|
37327
37464
|
}
|
|
37328
37465
|
try {
|
|
37329
|
-
const said =
|
|
37466
|
+
const said = execFileSync2("ps", ["-p", String(pid), "-o", "lstart="], {
|
|
37330
37467
|
encoding: "utf8",
|
|
37331
37468
|
timeout: 15000
|
|
37332
37469
|
}).trim();
|
|
@@ -37338,7 +37475,7 @@ function startedAt(pid, windowsAnswer) {
|
|
|
37338
37475
|
}
|
|
37339
37476
|
function windowsImage(pid) {
|
|
37340
37477
|
try {
|
|
37341
|
-
const csv =
|
|
37478
|
+
const csv = execFileSync2("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
|
|
37342
37479
|
encoding: "utf8",
|
|
37343
37480
|
timeout: 15000,
|
|
37344
37481
|
windowsHide: true
|
|
@@ -37441,7 +37578,7 @@ import {
|
|
|
37441
37578
|
readSync
|
|
37442
37579
|
} from "node:fs";
|
|
37443
37580
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
37444
|
-
import { basename as basename4, dirname as
|
|
37581
|
+
import { basename as basename4, dirname as dirname12, join as join19, normalize as normalize3, relative as relative3 } from "node:path";
|
|
37445
37582
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
37446
37583
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
37447
37584
|
import { promisify as promisify7 } from "node:util";
|
|
@@ -38659,11 +38796,11 @@ class GodotServer {
|
|
|
38659
38796
|
if (!engine.ok) {
|
|
38660
38797
|
return engine.response;
|
|
38661
38798
|
}
|
|
38662
|
-
const folder =
|
|
38799
|
+
const folder = dirname12(output.absolutePath);
|
|
38663
38800
|
try {
|
|
38664
38801
|
mkdirSync10(folder, { recursive: true });
|
|
38665
38802
|
} catch (error) {
|
|
38666
|
-
return this.createErrorResponse(`Cannot create ${
|
|
38803
|
+
return this.createErrorResponse(`Cannot create ${dirname12(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
38667
38804
|
}
|
|
38668
38805
|
const debug = readBoolean(args, "debug") ?? false;
|
|
38669
38806
|
const exportLogs = mkdtempSync3(join19(tmpdir5(), "gdharness-export-"));
|
|
@@ -39117,11 +39254,14 @@ class GodotServer {
|
|
|
39117
39254
|
return null;
|
|
39118
39255
|
}
|
|
39119
39256
|
try {
|
|
39120
|
-
const
|
|
39121
|
-
|
|
39122
|
-
|
|
39123
|
-
]);
|
|
39257
|
+
const askedAt = Date.now();
|
|
39258
|
+
const asked = this.godotBridge.invokeTool("playing_status", {});
|
|
39259
|
+
const answer = await Promise.race([asked, delay3(PLAYING_STATUS_MS).then(() => null)]);
|
|
39124
39260
|
if (answer === null) {
|
|
39261
|
+
this.logDebug(`playing_status did not answer within ${PLAYING_STATUS_MS}ms`);
|
|
39262
|
+
asked.then(() => {
|
|
39263
|
+
this.logDebug(`playing_status answered after ${Date.now() - askedAt}ms`);
|
|
39264
|
+
}).catch(() => {});
|
|
39125
39265
|
return null;
|
|
39126
39266
|
}
|
|
39127
39267
|
return {
|
|
@@ -39441,6 +39581,9 @@ class GodotServer {
|
|
|
39441
39581
|
let asking = null;
|
|
39442
39582
|
let editorAskedAt = 0;
|
|
39443
39583
|
let editorSaysGoing = true;
|
|
39584
|
+
let editorAsks = 0;
|
|
39585
|
+
const waitBegan = Date.now();
|
|
39586
|
+
const lastSaid = () => editorSaysGoing ? "playing" : "not playing";
|
|
39444
39587
|
const endpoint = await announcedSince(projectPath, before, {
|
|
39445
39588
|
budgetMs,
|
|
39446
39589
|
accept: (one) => this.isTheGameOf(this.currentRun(), one, projectPath),
|
|
@@ -39454,6 +39597,7 @@ class GodotServer {
|
|
|
39454
39597
|
}
|
|
39455
39598
|
if (asking === null && Date.now() - editorAskedAt >= RUN_POLL_MS) {
|
|
39456
39599
|
editorAskedAt = Date.now();
|
|
39600
|
+
editorAsks += 1;
|
|
39457
39601
|
asking = this.runStillGoing(run).then((going) => {
|
|
39458
39602
|
editorSaysGoing = going;
|
|
39459
39603
|
}).finally(() => {
|
|
@@ -39463,6 +39607,7 @@ class GodotServer {
|
|
|
39463
39607
|
return !editorSaysGoing;
|
|
39464
39608
|
}
|
|
39465
39609
|
});
|
|
39610
|
+
this.logDebug(`announce wait ended after ${Date.now() - waitBegan}ms: ${endpoint === null ? "nothing announced" : `pid ${endpoint.pid} announced`}, editor asked ${editorAsks} times, last said ${lastSaid()}`);
|
|
39466
39611
|
const going = this.currentRun();
|
|
39467
39612
|
if (endpoint !== null && going !== null && going.announcedPid === undefined) {
|
|
39468
39613
|
going.announcedPid = endpoint.pid;
|
|
@@ -40139,11 +40284,19 @@ class GodotServer {
|
|
|
40139
40284
|
this.drainEditorOutput(stopped);
|
|
40140
40285
|
this.drainTranscript(stopped);
|
|
40141
40286
|
const wasRunning = await this.runStillGoing(stopped);
|
|
40142
|
-
const
|
|
40287
|
+
const announcedPid = this.announcedPidOf(stopped) ?? null;
|
|
40288
|
+
const endedPid = stopped.pid ?? announcedPid;
|
|
40143
40289
|
const children = readBoolean(args, "andChildren") === true && wasRunning ? await this.whatTheRunStarted(stopped) : null;
|
|
40144
40290
|
this.logDebug("Stopping the running game");
|
|
40145
40291
|
await this.endActiveGame("editor_run stop");
|
|
40146
40292
|
const ended = children === null ? null : endChildrenAmong(children);
|
|
40293
|
+
if (wasRunning) {
|
|
40294
|
+
for (const pid of new Set([endedPid, announcedPid])) {
|
|
40295
|
+
if (pid !== null) {
|
|
40296
|
+
await announcementEnded(pid);
|
|
40297
|
+
}
|
|
40298
|
+
}
|
|
40299
|
+
}
|
|
40147
40300
|
return this.jsonTextResponse({
|
|
40148
40301
|
stopped: true,
|
|
40149
40302
|
through: stopped.throughEditor ? "editor" : "gdharness",
|
|
@@ -40591,7 +40744,7 @@ var init_server3 = __esm(() => {
|
|
|
40591
40744
|
init_tool_definitions();
|
|
40592
40745
|
init_update_check();
|
|
40593
40746
|
run7 = promisify7(execFile7);
|
|
40594
|
-
__dirname2 =
|
|
40747
|
+
__dirname2 = dirname12(fileURLToPath4(import.meta.url));
|
|
40595
40748
|
CONSOLE_WRAPPER = /_console(\.exe)?$/i;
|
|
40596
40749
|
VALUE_NAMES = {
|
|
40597
40750
|
string: "a string",
|
|
@@ -42142,6 +42295,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
42142
42295
|
| Where a control is, and whether it is visible | \`runtime_inspect\` \`find\`, \`rect\` |
|
|
42143
42296
|
| What a property reads right now, on a node whose path you already hold | \`runtime_inspect\` \`property\`, \`runtime_invoke\` |
|
|
42144
42297
|
| What a property on an object a node holds reads | the same two, with colons: \`_game:clock:speed\` |
|
|
42298
|
+
| Which control holds the focus, or anything only a method answers | the same path with a step written as a call, \`get_viewport():gui_get_focus_owner\`, which calls a method taking no arguments and walks into what it returned |
|
|
42145
42299
|
| Press a button | \`runtime_input click\`, which says what was under the pointer |
|
|
42146
42300
|
| Press the next thing after a click that changed screen | \`runtime_wait until\` with \`says\` and a word the new screen shows, then the click. A click sent straight after a screen change lands on a control the new screen is about to free, answers \`landed: true\`, and does nothing; a frame count is a number nobody can pick correctly across a cold start and a warm one, and waiting for the words answered in about 130ms where twenty frames had not been enough |
|
|
42147
42301
|
| Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\` with \`replace\`; a submitted field has to be clicked again |
|
|
@@ -352,8 +352,15 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
|
|
|
352
352
|
# its drag from the press, which is where a real pointer would be.
|
|
353
353
|
var _moved: Vector2 = _arrive(position)
|
|
354
354
|
Input.parse_input_event(_button(position, button, pressed, double))
|
|
355
|
+
# A wheel step is a press and a release together, as a mouse sends one: there is no holding a
|
|
356
|
+
# wheel. A lone wheel press left the viewport's mouse focus on the control that took it, with
|
|
357
|
+
# the wheel's bit in the focus mask, and every click after it landed on that control rather
|
|
358
|
+
# than under the pointer, landing true and doing nothing, until a release was sent by hand.
|
|
359
|
+
var notch: bool = pressed and _mask_of(button) == 0
|
|
360
|
+
if notch:
|
|
361
|
+
Input.parse_input_event(_button(position, button, false, false))
|
|
355
362
|
|
|
356
|
-
|
|
363
|
+
var answer: Dictionary = {
|
|
357
364
|
"type": "input_injected",
|
|
358
365
|
"input_type": "mouse_click",
|
|
359
366
|
"position": [position.x, position.y],
|
|
@@ -361,6 +368,9 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
|
|
|
361
368
|
"pressed": pressed,
|
|
362
369
|
"double": double
|
|
363
370
|
}
|
|
371
|
+
if notch:
|
|
372
|
+
answer["released"] = true
|
|
373
|
+
return answer
|
|
364
374
|
|
|
365
375
|
|
|
366
376
|
## Moves the pointer to a position, with the movement the event carries taken from where the
|
|
@@ -895,4 +905,7 @@ func _resolve_mouse_button(raw: Variant) -> int:
|
|
|
895
905
|
return MOUSE_BUTTON_WHEEL_DOWN
|
|
896
906
|
_:
|
|
897
907
|
return -1
|
|
898
|
-
|
|
908
|
+
# The engine's buttons run from 1 to 9: 0 is MOUSE_BUTTON_NONE, which no event carries, and a
|
|
909
|
+
# number past the two extra buttons names nothing a mask bit can be made for.
|
|
910
|
+
var number: int = Read.as_int(raw, -1)
|
|
911
|
+
return number if number >= MOUSE_BUTTON_LEFT and number <= MOUSE_BUTTON_XBUTTON2 else -1
|