gdharness 1.3.56 → 1.3.58
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
|
@@ -1763,6 +1763,9 @@ function nonEmpty(value) {
|
|
|
1763
1763
|
function keptSlotsOf(count) {
|
|
1764
1764
|
return Math.max(2, Math.floor(count / 4));
|
|
1765
1765
|
}
|
|
1766
|
+
function reservedSlotOf(count) {
|
|
1767
|
+
return count - Math.max(1, count - keptSlotsOf(count)) >= 2 ? count - 1 : null;
|
|
1768
|
+
}
|
|
1766
1769
|
function gamesKeptOf(kept) {
|
|
1767
1770
|
return Math.max(0, Math.min(Math.floor(kept / 2), kept - GATE_KEPT - 1));
|
|
1768
1771
|
}
|
|
@@ -1777,19 +1780,24 @@ function slotsDrawnOn(count, taker, queue) {
|
|
|
1777
1780
|
return { numbers: every };
|
|
1778
1781
|
}
|
|
1779
1782
|
if (taker === "tier") {
|
|
1780
|
-
|
|
1783
|
+
const reserve = reservedSlotOf(count);
|
|
1784
|
+
return { numbers: [...kept].reverse().filter((slot) => slot !== reserve), limit: "tier" };
|
|
1781
1785
|
}
|
|
1782
1786
|
return { numbers: kept.slice(0, Math.max(1, kept.length - GATE_KEPT)), limit: "kept" };
|
|
1783
1787
|
}
|
|
1784
1788
|
function inAGate(env = process4.env) {
|
|
1785
1789
|
return nonEmpty(env[SHARE_VARIABLE]) !== undefined && nonEmpty(env[SHARE_COUNT_VARIABLE]) !== undefined;
|
|
1786
1790
|
}
|
|
1787
|
-
function inheritsSlot(tree, pid, holder, executable) {
|
|
1791
|
+
function inheritsSlot(tree, pid, holder, executable, engineStarted) {
|
|
1788
1792
|
const program = (name) => name.toLowerCase().replace(/\.(?:exe|cmd|bat|com)$/, "");
|
|
1789
1793
|
const own = program(executable);
|
|
1790
|
-
for (const above of ancestorsIn(tree, pid, holder)) {
|
|
1794
|
+
for (const above of ancestorsIn(tree, pid, holder.pid)) {
|
|
1791
1795
|
if (program(readCommandLine(tree.get(above)?.command ?? "").executable) !== own) {
|
|
1792
|
-
|
|
1796
|
+
if (above === holder.pid) {
|
|
1797
|
+
return true;
|
|
1798
|
+
}
|
|
1799
|
+
const started = tree.get(above)?.startedAt ?? engineStarted;
|
|
1800
|
+
return above === holder.engine?.pid && started !== undefined && Math.abs(started - holder.engine.started) <= START_TOLERANCE_MS;
|
|
1793
1801
|
}
|
|
1794
1802
|
}
|
|
1795
1803
|
return false;
|
|
@@ -1854,20 +1862,29 @@ function holderOf(content) {
|
|
|
1854
1862
|
if (typeof parsed !== "object" || parsed === null) {
|
|
1855
1863
|
return null;
|
|
1856
1864
|
}
|
|
1857
|
-
const {
|
|
1858
|
-
|
|
1865
|
+
const { by, game, engine } = parsed;
|
|
1866
|
+
const named = namedProcess(parsed);
|
|
1867
|
+
if (named === null) {
|
|
1859
1868
|
return null;
|
|
1860
1869
|
}
|
|
1870
|
+
const runs = namedProcess(engine);
|
|
1861
1871
|
return {
|
|
1862
|
-
|
|
1863
|
-
started,
|
|
1872
|
+
...named,
|
|
1864
1873
|
...typeof by === "string" && by !== "" ? { by } : {},
|
|
1865
|
-
...game === true ? { game } : {}
|
|
1874
|
+
...game === true ? { game } : {},
|
|
1875
|
+
...runs === null ? {} : { engine: runs }
|
|
1866
1876
|
};
|
|
1867
1877
|
} catch {
|
|
1868
1878
|
return null;
|
|
1869
1879
|
}
|
|
1870
1880
|
}
|
|
1881
|
+
function namedProcess(value) {
|
|
1882
|
+
if (typeof value !== "object" || value === null) {
|
|
1883
|
+
return null;
|
|
1884
|
+
}
|
|
1885
|
+
const { pid, started } = value;
|
|
1886
|
+
return typeof pid === "number" && Number.isInteger(pid) && pid > 0 && typeof started === "number" ? { pid, started } : null;
|
|
1887
|
+
}
|
|
1871
1888
|
function lockIsFree(content, writtenAt, now, isAlive, startOf) {
|
|
1872
1889
|
const holder = holderOf(content);
|
|
1873
1890
|
if (holder === null) {
|
|
@@ -1913,7 +1930,7 @@ class EngineSlots {
|
|
|
1913
1930
|
try {
|
|
1914
1931
|
for (;; ) {
|
|
1915
1932
|
signal?.throwIfAborted();
|
|
1916
|
-
const lock = await this.takeFree(game);
|
|
1933
|
+
const lock = await this.takeFree(game, Date.now() - began);
|
|
1917
1934
|
if (lock !== null) {
|
|
1918
1935
|
return lock;
|
|
1919
1936
|
}
|
|
@@ -1945,7 +1962,7 @@ class EngineSlots {
|
|
|
1945
1962
|
}
|
|
1946
1963
|
}
|
|
1947
1964
|
}
|
|
1948
|
-
async takeFree(game = false) {
|
|
1965
|
+
async takeFree(game = false, waitedMs = 0) {
|
|
1949
1966
|
mkdirSync2(this.directory, { recursive: true });
|
|
1950
1967
|
await this.sweep();
|
|
1951
1968
|
const inherited = this.inherited === null ? await this.inheritedLock() : null;
|
|
@@ -1954,11 +1971,14 @@ class EngineSlots {
|
|
|
1954
1971
|
return inherited;
|
|
1955
1972
|
}
|
|
1956
1973
|
const taker = this.taker();
|
|
1957
|
-
|
|
1974
|
+
const aged = taker === "other" && !game && waitedMs >= ONE_OFF_AGED_MS;
|
|
1975
|
+
this.drawn = slotsDrawnOn(this.count, taker, taker === "gate" || taker === "timing" || aged ? "none" : await this.gateQueue());
|
|
1958
1976
|
if (game && this.drawn.limit === "kept") {
|
|
1959
|
-
const
|
|
1960
|
-
|
|
1961
|
-
|
|
1977
|
+
const reserve = reservedSlotOf(this.count);
|
|
1978
|
+
this.drawn = { ...this.drawn, numbers: this.drawn.numbers.filter((slot) => slot !== reserve) };
|
|
1979
|
+
const kept = this.count - Math.max(1, this.count - keptSlotsOf(this.count));
|
|
1980
|
+
const games = await this.heldByGames(Array.from({ length: kept }, (_, at) => this.count - kept + at));
|
|
1981
|
+
if (games.length >= gamesKeptOf(kept)) {
|
|
1962
1982
|
this.drawn = { numbers: games, limit: "games" };
|
|
1963
1983
|
return null;
|
|
1964
1984
|
}
|
|
@@ -1981,7 +2001,9 @@ class EngineSlots {
|
|
|
1981
2001
|
return null;
|
|
1982
2002
|
}
|
|
1983
2003
|
const tree = await processTree();
|
|
1984
|
-
|
|
2004
|
+
const engine = holder.engine?.pid;
|
|
2005
|
+
const engineStarted = engine === undefined || tree?.get(engine)?.startedAt !== undefined ? undefined : (await startTimesOf([engine])).get(engine);
|
|
2006
|
+
return tree !== undefined && inheritsSlot(tree, process4.pid, holder, basename3(process4.execPath), engineStarted) ? lock : null;
|
|
1985
2007
|
}
|
|
1986
2008
|
markWaiting() {
|
|
1987
2009
|
const mark = this.waitMarked();
|
|
@@ -2107,6 +2129,25 @@ class EngineSlots {
|
|
|
2107
2129
|
await this.cleared(join5(this.directory, name), false);
|
|
2108
2130
|
}
|
|
2109
2131
|
}
|
|
2132
|
+
const marks = join5(this.directory, WAITING_FOLDER);
|
|
2133
|
+
let waits;
|
|
2134
|
+
try {
|
|
2135
|
+
waits = readdirSync(marks);
|
|
2136
|
+
} catch {
|
|
2137
|
+
return;
|
|
2138
|
+
}
|
|
2139
|
+
for (const name of waits) {
|
|
2140
|
+
let content;
|
|
2141
|
+
try {
|
|
2142
|
+
content = readWhole(join5(marks, name));
|
|
2143
|
+
} catch {
|
|
2144
|
+
continue;
|
|
2145
|
+
}
|
|
2146
|
+
const pid = content === null ? undefined : holderOf(content)?.pid;
|
|
2147
|
+
if (pid !== undefined && !alive(pid)) {
|
|
2148
|
+
removeWhole(join5(marks, name));
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2110
2151
|
}
|
|
2111
2152
|
takenOver(lock, holder) {
|
|
2112
2153
|
return this.cleared(lock, true, holder);
|
|
@@ -2172,6 +2213,45 @@ function handSlot(lock, from, to) {
|
|
|
2172
2213
|
const holder = content === null ? null : holderOf(content);
|
|
2173
2214
|
return holder?.pid === from && writeWhole(lock, JSON.stringify(holder.game === true ? { ...to, game: true } : to));
|
|
2174
2215
|
}
|
|
2216
|
+
function nameSlotEngine(lock, holder, engine) {
|
|
2217
|
+
let content;
|
|
2218
|
+
try {
|
|
2219
|
+
content = readWhole(lock);
|
|
2220
|
+
} catch {
|
|
2221
|
+
return false;
|
|
2222
|
+
}
|
|
2223
|
+
const named = content === null ? null : holderOf(content);
|
|
2224
|
+
return named?.pid === holder && writeWhole(lock, JSON.stringify({ ...named, engine }));
|
|
2225
|
+
}
|
|
2226
|
+
async function nameSlotEngineOf(lock, holder, pid) {
|
|
2227
|
+
const started = (await startTimesOf([pid])).get(pid);
|
|
2228
|
+
if (started !== undefined) {
|
|
2229
|
+
nameSlotEngine(lock, holder, { pid, started });
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
async function slotEngineNamed(lock, run, pidFile) {
|
|
2233
|
+
if (pidFile === undefined) {
|
|
2234
|
+
if (run.pid !== null) {
|
|
2235
|
+
await nameSlotEngineOf(lock, process4.pid, run.pid);
|
|
2236
|
+
}
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2239
|
+
const state = { settled: false };
|
|
2240
|
+
run.settled.finally(() => {
|
|
2241
|
+
state.settled = true;
|
|
2242
|
+
});
|
|
2243
|
+
while (!state.settled) {
|
|
2244
|
+
let written = Number.NaN;
|
|
2245
|
+
try {
|
|
2246
|
+
written = Number(readFileSync6(pidFile, "utf8").trim());
|
|
2247
|
+
} catch {}
|
|
2248
|
+
if (Number.isInteger(written) && written > 0) {
|
|
2249
|
+
await nameSlotEngineOf(lock, process4.pid, written);
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
await delay2(ENGINE_PID_LOOK_MS);
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2175
2255
|
function giveBackSlot(lock, pid) {
|
|
2176
2256
|
try {
|
|
2177
2257
|
if (holderOf(readFileSync6(lock, "utf8"))?.pid === pid) {
|
|
@@ -2290,7 +2370,7 @@ async function inASlot(body) {
|
|
|
2290
2370
|
held.release();
|
|
2291
2371
|
}
|
|
2292
2372
|
}
|
|
2293
|
-
var HELD_VARIABLE = "GODOT_ENGINE_HELD", SLOTS_VARIABLE = "GODOT_ENGINE_SLOTS", SLOT_COUNT_VARIABLE = "GODOT_ENGINE_SLOT_COUNT", PROCESSORS_A_SLOT = 2, START_TOLERANCE_MS = 2000, UNWRITTEN_MS = 1e4, SLOT_LOOK_MS = 1000, SLOT_SETTLE_MS = 50, WAIT_SAID_MS = 20000, COMMAND_WAIT_SAID_MS = 2000, START_READ_MS = 1e4, WAITING_FOLDER = "waiting", QUEUED_FOLDER = "queued", SHARE_VARIABLE = "GODOT_ENGINE_SHARE", SHARE_COUNT_VARIABLE = "GODOT_ENGINE_SHARE_COUNT", GATE_KEPT = 1, TIMED_VARIABLE = "GODOT_ENGINE_TIMED", TIER_VARIABLE = "GODOT_ENGINE_TIER", SEATED_SUFFIX = ".seated", waitsMarked = 0, ours, saidTo, takenBy;
|
|
2373
|
+
var HELD_VARIABLE = "GODOT_ENGINE_HELD", SLOTS_VARIABLE = "GODOT_ENGINE_SLOTS", SLOT_COUNT_VARIABLE = "GODOT_ENGINE_SLOT_COUNT", PROCESSORS_A_SLOT = 2, START_TOLERANCE_MS = 2000, UNWRITTEN_MS = 1e4, SLOT_LOOK_MS = 1000, SLOT_SETTLE_MS = 50, WAIT_SAID_MS = 20000, COMMAND_WAIT_SAID_MS = 2000, START_READ_MS = 1e4, WAITING_FOLDER = "waiting", QUEUED_FOLDER = "queued", SHARE_VARIABLE = "GODOT_ENGINE_SHARE", SHARE_COUNT_VARIABLE = "GODOT_ENGINE_SHARE_COUNT", GATE_KEPT = 1, ONE_OFF_AGED_MS = 120000, TIMED_VARIABLE = "GODOT_ENGINE_TIMED", TIER_VARIABLE = "GODOT_ENGINE_TIER", SEATED_SUFFIX = ".seated", waitsMarked = 0, ENGINE_PID_LOOK_MS = 100, ours, saidTo, takenBy;
|
|
2294
2374
|
var init_engine_slots = __esm(() => {
|
|
2295
2375
|
init_alive();
|
|
2296
2376
|
init_cache_home();
|
|
@@ -13189,9 +13269,13 @@ async function watchedEngine(launch, lock, args, directory, log, silenceSeconds,
|
|
|
13189
13269
|
return;
|
|
13190
13270
|
}
|
|
13191
13271
|
};
|
|
13272
|
+
const found = (engine) => {
|
|
13273
|
+
watching(engine, true);
|
|
13274
|
+
nameSlotEngineOf(lock, process7.pid, engine);
|
|
13275
|
+
};
|
|
13192
13276
|
let pid = engineAt();
|
|
13193
13277
|
if (pid !== undefined) {
|
|
13194
|
-
|
|
13278
|
+
found(pid);
|
|
13195
13279
|
}
|
|
13196
13280
|
try {
|
|
13197
13281
|
let size = 0;
|
|
@@ -13206,7 +13290,7 @@ async function watchedEngine(launch, lock, args, directory, log, silenceSeconds,
|
|
|
13206
13290
|
if (pid === undefined) {
|
|
13207
13291
|
pid = engineAt();
|
|
13208
13292
|
if (pid !== undefined) {
|
|
13209
|
-
|
|
13293
|
+
found(pid);
|
|
13210
13294
|
}
|
|
13211
13295
|
}
|
|
13212
13296
|
const taken = await sample();
|
|
@@ -14023,6 +14107,9 @@ function runStarted(executable, args, lock, options, signal) {
|
|
|
14023
14107
|
env: onTheSlot(process.env, lock),
|
|
14024
14108
|
...signal === undefined ? {} : { signal }
|
|
14025
14109
|
});
|
|
14110
|
+
if (child.pid !== undefined) {
|
|
14111
|
+
nameSlotEngineOf(lock, process.pid, child.pid);
|
|
14112
|
+
}
|
|
14026
14113
|
child.stdout.on("data", (chunk) => {
|
|
14027
14114
|
log.append("stdout", chunk);
|
|
14028
14115
|
});
|
|
@@ -18097,7 +18184,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18097
18184
|
direction: {
|
|
18098
18185
|
type: "string",
|
|
18099
18186
|
enum: ["forward", "reverse"],
|
|
18100
|
-
description: "forward: what this resource loads or names. Each file named by its path carries kind, as reverse words it: ext_resource, extends, preload, load, include, or path for a path only written in a string, which the file itself may never load. Each reference closing a cycle is marked circular, with the cycle under circular_references. A script's dependencies include every script whose class_name it uses, marked by_class with the name, and the script of every autoload it names, marked by_autoload with the autoload's name. An ext_resource whose uid names another file than its path is followed to that file, as the engine loads it, with the path written under stale_path; one whose uid names no file the project has is followed by its path, as the engine does after warning, with the uid under unknown_uid; and a load or preload of a uid the project does not know is listed under that uid as missing. A shader's #include is followed as the preprocessor reads it, relative to the shader. A file the depth stops at carries beyond_depth, how many files it names that were not walked. A file's dependencies are listed at its first entry, and again further down only where the depth cut that list short and more is left; every other entry for a file naming any carries dependencies_listed_above. summary.total_resources is the file asked about, so 1; total_dependencies counts each file reached, directly or through another, once however many places load it; total_references counts the entries in the tree, and missing names every file referenced that does not exist, with the files referring to it. reverse: every file that refers to it and how, for a file that is gone as well while anything still refers to it, answered with exists: false (a missing file nothing refers to is refused, naming the nearest files), a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name, and for an autoload's script every use of the autoload's name, with autoload naming it. Each reference carries a kind: ext_resource, extends, preload, load, include or path for a reference by path, with stale_path when an ext_resource reaches it by its uid under another path, and unknown_uid when one names it by path beside a uid naming no file; setting, plugin, export_preset and import for one in project.godot, a plugin.cfg, export_presets.cfg or an import sidecar; class_name for a use of the declared name, the declaration itself being the script asked about; autoload for code naming the autoload. Three kinds are not uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs; comment is one in ordinary prose, which it would only make stale; and stale_path is an ext_resource naming it by a path the engine does not load, since its uid leads to the file under loads. summary.in_code is the count with those left out, which is the number to read when the question is whether anything still uses this. Default forward."
|
|
18187
|
+
description: "forward: what this resource loads or names. Each file named by its path carries kind, as reverse words it: ext_resource, extends, preload, load, include, or path for a path only written in a string, which the file itself may never load. Each reference closing a cycle is marked circular, with the cycle under circular_references. A script's dependencies include every script whose class_name it uses, marked by_class with the name, and the script of every autoload it names, marked by_autoload with the autoload's name. An ext_resource whose uid names another file than its path is followed to that file, as the engine loads it, with the path written under stale_path; one whose uid names no file the project has is followed by its path, as the engine does after warning, with the uid under unknown_uid; and a load or preload of a uid the project does not know is listed under that uid as missing. A shader's #include is followed as the preprocessor reads it, relative to the shader. A file the depth stops at carries beyond_depth, how many files it names that were not walked. A file's dependencies are listed at its first entry, and again further down only where the depth cut that list short and more is left; every other entry for a file naming any carries dependencies_listed_above. summary.total_resources is the file asked about, so 1; total_dependencies counts each file reached, directly or through another, once however many places load it; total_references counts the entries in the tree, and missing names every file referenced that does not exist, with the files referring to it. reverse: every file that refers to it and how, for a file that is gone as well while anything still refers to it, answered with exists: false (a missing file nothing refers to is refused, naming the nearest files), a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name, and for an autoload's script every use of the autoload's name, with autoload naming it. Each reference carries a kind: ext_resource, extends, preload, load, include or path for a reference by path, with stale_path when an ext_resource reaches it by its uid under another path, and unknown_uid when one names it by path beside a uid naming no file; setting, plugin, export_preset and import for one in project.godot, a plugin.cfg, export_presets.cfg or an import sidecar; class_name for a use of the declared name, the declaration itself being the script asked about; autoload for code naming the autoload. Three kinds are not uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs; comment is one in ordinary prose, which it would only make stale; and stale_path is an ext_resource naming it by a path the engine does not load, since its uid leads to the file under loads. A file whose header, or an import sidecar, claims the same uid as its own is no use either: it is listed under shares_uid, since the engine maps a uid to one file and loads that one wherever the uid is named. summary.in_code is the count with those left out, which is the number to read when the question is whether anything still uses this. Default forward."
|
|
18101
18188
|
},
|
|
18102
18189
|
depth: {
|
|
18103
18190
|
type: "integer",
|
|
@@ -19119,7 +19206,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
19119
19206
|
},
|
|
19120
19207
|
wait: {
|
|
19121
19208
|
effect: "read",
|
|
19122
|
-
summary: "wait for the run to end, then answer as editor_output does",
|
|
19209
|
+
summary: "wait for the run to end, or to be held by its debugger at a breakpoint, a pause or a step, then answer as editor_output does",
|
|
19123
19210
|
requires: []
|
|
19124
19211
|
},
|
|
19125
19212
|
profile: {
|
|
@@ -56713,6 +56800,7 @@ ${error.text}`;
|
|
|
56713
56800
|
try {
|
|
56714
56801
|
const run = this.spawnHeld(godotPath, cmdArgs, onTheSlot(env ?? process.env, held.lock), onDesktop);
|
|
56715
56802
|
run.settled.finally(held.release);
|
|
56803
|
+
slotEngineNamed(held.lock, run, onDesktop?.files.pid);
|
|
56716
56804
|
return run;
|
|
56717
56805
|
} catch (error) {
|
|
56718
56806
|
held.release();
|
|
@@ -57455,14 +57543,33 @@ ${error.text}`;
|
|
|
57455
57543
|
return elsewhere;
|
|
57456
57544
|
}
|
|
57457
57545
|
const budgetMs = readPositiveNumber(args, "timeoutMs") ?? WAIT_FOR_RUN_MS;
|
|
57458
|
-
const
|
|
57546
|
+
const began = Date.now();
|
|
57547
|
+
const until = began + budgetMs;
|
|
57459
57548
|
const cancelled = callSignal();
|
|
57549
|
+
let held = false;
|
|
57550
|
+
let holdAsked = 0;
|
|
57460
57551
|
while (cancelled?.aborted !== true && await this.runStillGoing(run) && Date.now() < until) {
|
|
57461
57552
|
this.drainEditorOutput(run);
|
|
57462
57553
|
this.drainTranscript(run);
|
|
57554
|
+
if (Date.now() - holdAsked >= HOLD_ASKED_EVERY_MS) {
|
|
57555
|
+
holdAsked = Date.now();
|
|
57556
|
+
if (await this.heldByItsDebugger(run)) {
|
|
57557
|
+
held = true;
|
|
57558
|
+
break;
|
|
57559
|
+
}
|
|
57560
|
+
}
|
|
57463
57561
|
await delay8(RUN_POLL_MS);
|
|
57464
57562
|
}
|
|
57465
|
-
|
|
57563
|
+
if (!await this.runStillGoing(run)) {
|
|
57564
|
+
return await this.handleGetDebugOutput(args);
|
|
57565
|
+
}
|
|
57566
|
+
return await this.handleGetDebugOutput(args, held ? { heldAfterMs: Date.now() - began } : { ranOutMs: budgetMs });
|
|
57567
|
+
}
|
|
57568
|
+
async heldByItsDebugger(run) {
|
|
57569
|
+
if (!run.throughEditor) {
|
|
57570
|
+
return run.keeper !== undefined && await this.keptHold(run.keeper) !== null;
|
|
57571
|
+
}
|
|
57572
|
+
return this.dapClient?.holdIsKnown() === true && this.dapClient.whereItStopped() !== null;
|
|
57466
57573
|
}
|
|
57467
57574
|
async runStillGoing(run, editorSays) {
|
|
57468
57575
|
if (!run.throughEditor) {
|
|
@@ -57611,7 +57718,7 @@ ${error.text}`;
|
|
|
57611
57718
|
"editor_status names every game running on the machine under runtimes"
|
|
57612
57719
|
]);
|
|
57613
57720
|
}
|
|
57614
|
-
async handleGetDebugOutput(args,
|
|
57721
|
+
async handleGetDebugOutput(args, waited) {
|
|
57615
57722
|
if (readString(args, "op") === "editor") {
|
|
57616
57723
|
return this.handleEditorConsole(args);
|
|
57617
57724
|
}
|
|
@@ -57648,8 +57755,11 @@ ${error.text}`;
|
|
|
57648
57755
|
if (wanted && cpuSeconds === undefined && stillRunning(run)) {
|
|
57649
57756
|
notes.push(processToAsk === null ? "cpu was asked for and there is no process to ask: the editor is playing this run and holds it, and the game announced no runtime, so nothing here has its process id. A game with the runtime addon announces one; editor_run start without a connected editor, or with args, starts the game here instead and that run answers cpuSeconds either way." : "cpu was asked for and this platform would not say what the run has used.");
|
|
57650
57757
|
}
|
|
57651
|
-
if (
|
|
57652
|
-
notes.push(`The wait of ${
|
|
57758
|
+
if (waited !== undefined && "ranOutMs" in waited) {
|
|
57759
|
+
notes.push(`The wait of ${waited.ranOutMs}ms ran out and the run is still going, so this is what it had printed by then rather than everything it will print. editor_run wait again to keep waiting.`);
|
|
57760
|
+
}
|
|
57761
|
+
if (waited !== undefined && "heldAfterMs" in waited) {
|
|
57762
|
+
notes.push(`The wait answered after ${waited.heldAfterMs}ms because the game is held by its debugger, where heldAt says, and a held game does not end: debug_control continue lets it go, and editor_run wait then waits on.`);
|
|
57653
57763
|
}
|
|
57654
57764
|
const restarts = run.restartedFrom ?? [];
|
|
57655
57765
|
if (restarts.length > 0) {
|
|
@@ -59507,7 +59617,7 @@ async function runGodotServer() {
|
|
|
59507
59617
|
const server = new GodotServer;
|
|
59508
59618
|
await server.run();
|
|
59509
59619
|
}
|
|
59510
|
-
var TYPE_ARGUMENTS, UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, FAILED_BUDGET = 1e6, ANSWER_BUDGET = 4000000, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, SUCCESSOR_SOUGHT_WITHIN_MS = 20000, CLOSE_ASK_MS = 3000, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_SETTLED_MS = 500, SLOW_ANSWER_MS = 1000, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_TIMEOUT_MS = 600000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", LOOPING_AFTER = 100, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, DESTINATION_ARGUMENTS, OUTPUT_PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, MOST_PRINTED_LINES = 50, MOST_UNDECLARED_ERRORS = 20, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", RAISED_WARNING = " (Warning treated as error.)", PARSE_ERROR2 = "Parse Error: ", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
|
|
59620
|
+
var TYPE_ARGUMENTS, UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, FAILED_BUDGET = 1e6, ANSWER_BUDGET = 4000000, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, SUCCESSOR_SOUGHT_WITHIN_MS = 20000, CLOSE_ASK_MS = 3000, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_SETTLED_MS = 500, SLOW_ANSWER_MS = 1000, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_TIMEOUT_MS = 600000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", LOOPING_AFTER = 100, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, HOLD_ASKED_EVERY_MS = 1000, PATH_SOLUTIONS, DESTINATION_ARGUMENTS, OUTPUT_PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, MOST_PRINTED_LINES = 50, MOST_UNDECLARED_ERRORS = 20, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", RAISED_WARNING = " (Warning treated as error.)", PARSE_ERROR2 = "Parse Error: ", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
|
|
59511
59621
|
var init_server = __esm(() => {
|
|
59512
59622
|
init_dist2();
|
|
59513
59623
|
init_stdio();
|
|
@@ -16,8 +16,9 @@ const SETTLE_MS: int = 8000
|
|
|
16
16
|
## How long a window with nothing in it is walked before the client stops: a game building its tree
|
|
17
17
|
## fills the window within a few frames of being asked, and one building none never does.
|
|
18
18
|
const EMPTY_MS: int = 4000
|
|
19
|
-
## How long starting PowerShell
|
|
20
|
-
## waits on the machine alone: a warm start takes under a second,
|
|
19
|
+
## How long starting PowerShell, loading UI Automation and the client's other cold loads may take
|
|
20
|
+
## before the client is stopped. It waits on the machine alone: a warm start takes under a second,
|
|
21
|
+
## and a cold one on a loaded Windows
|
|
21
22
|
## runner took the whole of a single 23 s budget meant for the walk as well.
|
|
22
23
|
const START_MS: int = 45000
|
|
23
24
|
## How long the walks may take beyond [constant SETTLE_MS] a window once UI Automation is loaded,
|
|
@@ -39,6 +40,7 @@ const NODE_ID: String = "_node_instance"
|
|
|
39
40
|
const WALK: String = r"""
|
|
40
41
|
$ErrorActionPreference = 'Stop'
|
|
41
42
|
function Answer($value) {
|
|
43
|
+
Mark 'answering'
|
|
42
44
|
[System.IO.File]::WriteAllText($Out, (ConvertTo-Json -InputObject $value -Depth 100 -Compress))
|
|
43
45
|
Mark 'answered'
|
|
44
46
|
}
|
|
@@ -48,7 +50,6 @@ function Mark($stage) {
|
|
|
48
50
|
try {
|
|
49
51
|
Mark 'began'
|
|
50
52
|
Add-Type -AssemblyName UIAutomationClient, UIAutomationTypes
|
|
51
|
-
Mark 'loaded'
|
|
52
53
|
$walker = [System.Windows.Automation.TreeWalker]::RawViewWalker
|
|
53
54
|
function Patterned($element, $name) {
|
|
54
55
|
$pattern = ("System.Windows.Automation.$($name)Pattern" -as [type])::Pattern
|
|
@@ -70,6 +71,10 @@ try {
|
|
|
70
71
|
} catch {
|
|
71
72
|
$script:unread = "$_"
|
|
72
73
|
}
|
|
74
|
+
# Serialised once here, since the first ConvertTo-Json loads its serialiser, and on a loaded
|
|
75
|
+
# machine that cold load belongs to the start's budget, not to the walks'.
|
|
76
|
+
$null = ConvertTo-Json -InputObject @{ warm = @(1) } -Depth 100 -Compress
|
|
77
|
+
Mark 'loaded'
|
|
73
78
|
function FullDescription($element) {
|
|
74
79
|
if ($null -ne $script:unread) { return '' }
|
|
75
80
|
try {
|
|
@@ -281,7 +286,9 @@ func get_accessibility(params: Dictionary) -> Dictionary:
|
|
|
281
286
|
var looked_at: int = began_at
|
|
282
287
|
while OS.is_process_running(pid):
|
|
283
288
|
var now: int = Time.get_ticks_msec()
|
|
284
|
-
|
|
289
|
+
# Read again at the deadline too, so an answer marked since the last look is taken rather
|
|
290
|
+
# than stopped.
|
|
291
|
+
if now - looked_at >= LOOK_MS or now > deadline:
|
|
285
292
|
looked_at = now
|
|
286
293
|
var marked: String = FileAccess.get_file_as_string(progress)
|
|
287
294
|
if not walking and marked.contains("loaded "):
|
|
@@ -756,8 +763,8 @@ static func _marks(progress: String) -> Array:
|
|
|
756
763
|
|
|
757
764
|
## How far a client started at [param started_ms] and stopped at [param stopped_ms] got by the marks
|
|
758
765
|
## in [param progress], said for a caller whose read it stopped: the time went to starting
|
|
759
|
-
## PowerShell, loading UI Automation, the walks,
|
|
760
|
-
## machine from a window that never answers.
|
|
766
|
+
## PowerShell, loading UI Automation, the walks, after the last walk, or writing the answer, and
|
|
767
|
+
## which tells a loaded machine from a window that never answers.
|
|
761
768
|
static func _reached(progress: String, started_ms: int, stopped_ms: int) -> String:
|
|
762
769
|
var marks: Array = _marks(progress)
|
|
763
770
|
var at: Dictionary = {}
|
|
@@ -786,7 +793,7 @@ static func _reached(progress: String, started_ms: int, stopped_ms: int) -> Stri
|
|
|
786
793
|
)
|
|
787
794
|
% [began, loaded]
|
|
788
795
|
)
|
|
789
|
-
|
|
796
|
+
var walked: String = (
|
|
790
797
|
(
|
|
791
798
|
"PowerShell began the client after %d ms and loaded UI Automation in %d ms, and the "
|
|
792
799
|
+ "windows were walked %d time%s, the last finding %s element%s and ending %d ms "
|
|
@@ -802,6 +809,10 @@ static func _reached(progress: String, started_ms: int, stopped_ms: int) -> Stri
|
|
|
802
809
|
stopped_ms - last_at,
|
|
803
810
|
]
|
|
804
811
|
)
|
|
812
|
+
if not at.has("answering"):
|
|
813
|
+
return walked
|
|
814
|
+
var answering: int = stopped_ms - at["answering"]
|
|
815
|
+
return walked + ", which had been writing the answer for %d ms" % answering
|
|
805
816
|
|
|
806
817
|
|
|
807
818
|
## Where the time of a client started at [param started_ms] and done at [param done_ms] went, by
|
|
@@ -281,6 +281,8 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
|
|
|
281
281
|
var by_kind: Dictionary = {}
|
|
282
282
|
var total: int = 0
|
|
283
283
|
var in_code: int = 0
|
|
284
|
+
var shares_uid: Array[String] = []
|
|
285
|
+
var target_uid: String = target["uid"]
|
|
284
286
|
|
|
285
287
|
for file_path: String in all_files:
|
|
286
288
|
# Its own sidecar names it as the file it was imported from, which is not a use of it.
|
|
@@ -305,6 +307,11 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
|
|
|
305
307
|
var references: Array[Dictionary] = []
|
|
306
308
|
for i: int in range(lines.size()):
|
|
307
309
|
var line: String = lines[i]
|
|
310
|
+
# A file declaring the target's uid as its own is not using it: a resource copied with
|
|
311
|
+
# its header claims the same uid, and its header line was answered as a use by path.
|
|
312
|
+
if not target_uid.is_empty() and _own_uid(line, i, file_path) == target_uid:
|
|
313
|
+
shares_uid.append(file_path)
|
|
314
|
+
continue
|
|
308
315
|
var by_its_path: bool = _names_target(line, file_path, target)
|
|
309
316
|
var by_its_name: bool = false
|
|
310
317
|
var name_in_comment: bool = false
|
|
@@ -408,9 +415,30 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
|
|
|
408
415
|
}
|
|
409
416
|
if not autoload_named.is_empty():
|
|
410
417
|
answer["autoload"] = autoload_named
|
|
418
|
+
if not shares_uid.is_empty():
|
|
419
|
+
answer["shares_uid"] = shares_uid
|
|
411
420
|
return answer
|
|
412
421
|
|
|
413
422
|
|
|
423
|
+
# The uid [param line], line [param index] of [param file_path], declares as the file's own: a
|
|
424
|
+
# scene's or a resource's header, or an import sidecar's uid of the file it imports; empty for any
|
|
425
|
+
# other line.
|
|
426
|
+
static func _own_uid(line: String, index: int, file_path: String) -> String:
|
|
427
|
+
var trimmed: String = line.strip_edges()
|
|
428
|
+
var declares: bool = (
|
|
429
|
+
(
|
|
430
|
+
index == 0
|
|
431
|
+
and file_path.get_extension() in ["tscn", "tres"]
|
|
432
|
+
and (trimmed.begins_with("[gd_scene") or trimmed.begins_with("[gd_resource"))
|
|
433
|
+
)
|
|
434
|
+
or (file_path.get_extension() == "import" and trimmed.begins_with('uid="'))
|
|
435
|
+
)
|
|
436
|
+
if not declares:
|
|
437
|
+
return ""
|
|
438
|
+
var found: RegExMatch = GdhPatterns.compiled('uid="(uid://[^"]+)"').search(trimmed)
|
|
439
|
+
return "" if found == null else found.get_string(1)
|
|
440
|
+
|
|
441
|
+
|
|
414
442
|
# The class_name a script declares, or empty for a scene, a resource or a script without one.
|
|
415
443
|
func _declared_class_name(path: String) -> String:
|
|
416
444
|
if not path.ends_with(".gd"):
|
package/build/keeper.js
CHANGED
|
@@ -8833,20 +8833,29 @@ function holderOf(content) {
|
|
|
8833
8833
|
if (typeof parsed !== "object" || parsed === null) {
|
|
8834
8834
|
return null;
|
|
8835
8835
|
}
|
|
8836
|
-
const {
|
|
8837
|
-
|
|
8836
|
+
const { by, game, engine } = parsed;
|
|
8837
|
+
const named = namedProcess(parsed);
|
|
8838
|
+
if (named === null) {
|
|
8838
8839
|
return null;
|
|
8839
8840
|
}
|
|
8841
|
+
const runs = namedProcess(engine);
|
|
8840
8842
|
return {
|
|
8841
|
-
|
|
8842
|
-
started,
|
|
8843
|
+
...named,
|
|
8843
8844
|
...typeof by === "string" && by !== "" ? { by } : {},
|
|
8844
|
-
...game === true ? { game } : {}
|
|
8845
|
+
...game === true ? { game } : {},
|
|
8846
|
+
...runs === null ? {} : { engine: runs }
|
|
8845
8847
|
};
|
|
8846
8848
|
} catch {
|
|
8847
8849
|
return null;
|
|
8848
8850
|
}
|
|
8849
8851
|
}
|
|
8852
|
+
function namedProcess(value) {
|
|
8853
|
+
if (typeof value !== "object" || value === null) {
|
|
8854
|
+
return null;
|
|
8855
|
+
}
|
|
8856
|
+
const { pid, started } = value;
|
|
8857
|
+
return typeof pid === "number" && Number.isInteger(pid) && pid > 0 && typeof started === "number" ? { pid, started } : null;
|
|
8858
|
+
}
|
|
8850
8859
|
async function ownStart() {
|
|
8851
8860
|
const said = (await startTimesOf([process4.pid])).get(process4.pid);
|
|
8852
8861
|
return said ?? Math.round(Date.now() - process4.uptime() * 1000);
|
|
@@ -8861,6 +8870,16 @@ function handSlot(lock, from, to) {
|
|
|
8861
8870
|
const holder = content === null ? null : holderOf(content);
|
|
8862
8871
|
return holder?.pid === from && writeWhole(lock, JSON.stringify(holder.game === true ? { ...to, game: true } : to));
|
|
8863
8872
|
}
|
|
8873
|
+
function nameSlotEngine(lock, holder, engine) {
|
|
8874
|
+
let content;
|
|
8875
|
+
try {
|
|
8876
|
+
content = readWhole(lock);
|
|
8877
|
+
} catch {
|
|
8878
|
+
return false;
|
|
8879
|
+
}
|
|
8880
|
+
const named = content === null ? null : holderOf(content);
|
|
8881
|
+
return named?.pid === holder && writeWhole(lock, JSON.stringify({ ...named, engine }));
|
|
8882
|
+
}
|
|
8864
8883
|
function giveBackSlot(lock, pid) {
|
|
8865
8884
|
try {
|
|
8866
8885
|
if (holderOf(readFileSync5(lock, "utf8"))?.pid === pid) {
|
|
@@ -11067,10 +11086,12 @@ async function keep() {
|
|
|
11067
11086
|
});
|
|
11068
11087
|
const slot = run?.slot;
|
|
11069
11088
|
if (slot !== undefined && keeperStarted !== undefined) {
|
|
11089
|
+
const gameStarted = (await startTimesOf([pid])).get(pid);
|
|
11070
11090
|
handSlot(slot.lock, slot.from, {
|
|
11071
11091
|
pid: process.pid,
|
|
11072
11092
|
started: await keeperStarted,
|
|
11073
|
-
by: `${slot.by}, through the keeper process that runs it
|
|
11093
|
+
by: `${slot.by}, through the keeper process that runs it`,
|
|
11094
|
+
...gameStarted === undefined ? {} : { engine: { pid, started: gameStarted } }
|
|
11074
11095
|
});
|
|
11075
11096
|
}
|
|
11076
11097
|
let { code, signal } = await exited;
|
|
@@ -11106,6 +11127,9 @@ async function keep() {
|
|
|
11106
11127
|
restartedFrom: [...note.restartedFrom ?? [], before]
|
|
11107
11128
|
};
|
|
11108
11129
|
writeNoteIfItCan(note);
|
|
11130
|
+
if (slot !== undefined) {
|
|
11131
|
+
nameSlotEngine(slot.lock, process.pid, { pid: next.pid, started: next.startedAt });
|
|
11132
|
+
}
|
|
11109
11133
|
end = () => {
|
|
11110
11134
|
try {
|
|
11111
11135
|
process.kill(next.pid);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.58",
|
|
4
4
|
"mcpName": "io.github.Aureliolo/gdharness",
|
|
5
5
|
"description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
|
|
6
6
|
"type": "module",
|