tickmarkr 2.6.9 → 2.7.0
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/dist/run/daemon.d.ts +2 -2
- package/dist/run/daemon.js +8 -37
- package/dist/run/suite-census.d.ts +20 -0
- package/dist/run/suite-census.js +100 -0
- package/package.json +1 -1
package/dist/run/daemon.d.ts
CHANGED
|
@@ -253,8 +253,8 @@ export declare function verifyIntegrationTipCached(intWt: string, commands: Reco
|
|
|
253
253
|
type SuitePidProbe = (pid: number) => number | undefined;
|
|
254
254
|
/** Count full-suite roots in one process-table snapshot. The probes are arguments so the ownership
|
|
255
255
|
* rules remain testable on hosts that forbid process inspection; production supplies cwd and the
|
|
256
|
-
* inherited TICKMARKR_SUITE_PARENT marker from
|
|
257
|
-
export declare function countLiveSuites(snapshot: string, repoRoot: string, daemonPid
|
|
256
|
+
* inherited TICKMARKR_SUITE_PARENT marker from one batched async probe (suite-census.ts, queue row 103). */
|
|
257
|
+
export declare function countLiveSuites(snapshot: string, repoRoot: string, daemonPid: number, cwdForPid: (pid: number) => string | undefined, suiteParentForPid: SuitePidProbe): number;
|
|
258
258
|
export declare const setLiveSuiteCountForTests: (probe: (repoRoot: string) => Promise<number>) => void;
|
|
259
259
|
export declare const resetLiveSuiteCountForTests: () => void;
|
|
260
260
|
/** Live full-suite roots attributable to this repository or this daemon. Ancestors are excluded so
|
package/dist/run/daemon.js
CHANGED
|
@@ -9,6 +9,7 @@ import { createRequire } from "node:module";
|
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
10
|
import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { batchedProcessProbes, runnerPids } from "./suite-census.js";
|
|
12
13
|
import { stringify } from "yaml";
|
|
13
14
|
import { BOOTSTRAP_FAILURE_RE, classifyDeadChannel, classifyTransientCapacity, trailerPattern, writePrompt } from "../adapters/prompt.js";
|
|
14
15
|
import { allAdapters, getAdapter, probeAll, readDoctor, rolePools } from "../adapters/registry.js";
|
|
@@ -1243,48 +1244,14 @@ async function observeWorkerProcessTree(marker, cwd) {
|
|
|
1243
1244
|
}
|
|
1244
1245
|
return tree.size === 0 ? "empty" : "running";
|
|
1245
1246
|
}
|
|
1246
|
-
function processCwd(pid) {
|
|
1247
|
-
try {
|
|
1248
|
-
return realpathSync(readlinkSync(`/proc/${pid}/cwd`));
|
|
1249
|
-
}
|
|
1250
|
-
catch { /* Darwin has no /proc */ }
|
|
1251
|
-
try {
|
|
1252
|
-
const out = execFileSync("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], {
|
|
1253
|
-
encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000,
|
|
1254
|
-
});
|
|
1255
|
-
const path = out.split("\n").find((line) => line.startsWith("n"))?.slice(1);
|
|
1256
|
-
return path ? realpathSync(path) : undefined;
|
|
1257
|
-
}
|
|
1258
|
-
catch {
|
|
1259
|
-
return undefined;
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
function processSuiteParent(pid) {
|
|
1263
|
-
try {
|
|
1264
|
-
const env = readFileSync(`/proc/${pid}/environ`, "utf8").split("\0");
|
|
1265
|
-
const value = env.find((entry) => entry.startsWith(`${SUITE_PARENT_ENV}=`))?.slice(SUITE_PARENT_ENV.length + 1);
|
|
1266
|
-
return value && /^\d+$/.test(value) ? Number(value) : undefined;
|
|
1267
|
-
}
|
|
1268
|
-
catch { /* Darwin has no /proc process environments */ }
|
|
1269
|
-
try {
|
|
1270
|
-
const out = execFileSync("ps", ["eww", "-p", String(pid), "-o", "command="], {
|
|
1271
|
-
encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000,
|
|
1272
|
-
});
|
|
1273
|
-
const value = new RegExp(`(?:^|\\s)${SUITE_PARENT_ENV}=(\\d+)(?:\\s|$)`).exec(out)?.[1];
|
|
1274
|
-
return value ? Number(value) : undefined;
|
|
1275
|
-
}
|
|
1276
|
-
catch {
|
|
1277
|
-
return undefined;
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
1247
|
const pathAtOrBelow = (root, candidate) => {
|
|
1281
1248
|
const rel = relative(root, candidate);
|
|
1282
1249
|
return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
|
|
1283
1250
|
};
|
|
1284
1251
|
/** Count full-suite roots in one process-table snapshot. The probes are arguments so the ownership
|
|
1285
1252
|
* rules remain testable on hosts that forbid process inspection; production supplies cwd and the
|
|
1286
|
-
* inherited TICKMARKR_SUITE_PARENT marker from
|
|
1287
|
-
export function countLiveSuites(snapshot, repoRoot, daemonPid
|
|
1253
|
+
* inherited TICKMARKR_SUITE_PARENT marker from one batched async probe (suite-census.ts, queue row 103). */
|
|
1254
|
+
export function countLiveSuites(snapshot, repoRoot, daemonPid, cwdForPid, suiteParentForPid) {
|
|
1288
1255
|
const rows = [];
|
|
1289
1256
|
for (const line of snapshot.split("\n")) {
|
|
1290
1257
|
const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/.exec(line);
|
|
@@ -1344,7 +1311,11 @@ export async function liveSuiteCount(repoRoot) {
|
|
|
1344
1311
|
if (liveSuiteCountForTests)
|
|
1345
1312
|
return liveSuiteCountForTests(repoRoot);
|
|
1346
1313
|
const snapshot = await shGit("ps -Aww -o pid=,ppid=,state=,command=", repoRoot, 15_000);
|
|
1347
|
-
|
|
1314
|
+
if (snapshot.code !== 0)
|
|
1315
|
+
return 0;
|
|
1316
|
+
// row 103: one batched async probe per snapshot — a synchronous lsof/ps per pid held this loop 44–46 s
|
|
1317
|
+
const probes = await batchedProcessProbes(runnerPids(snapshot.stdout));
|
|
1318
|
+
return countLiveSuites(snapshot.stdout, repoRoot, process.pid, probes.cwd, probes.suiteParent);
|
|
1348
1319
|
}
|
|
1349
1320
|
const OBSERVE_CHUNK_BYTES = 64 * 1024;
|
|
1350
1321
|
const OBSERVE_BUDGET_BYTES = 256 * 1024 * 1024;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue row 103 (D-1553): the live-suite census asks each runner-looking process for its cwd and its inherited
|
|
3
|
+
* TICKMARKR_SUITE_PARENT marker. It used to ask one pid at a time with a synchronous `lsof` and `ps`, which held the
|
|
4
|
+
* daemon's event loop 44–46 s under a full suite (the stall traces recorded for queue row 8). Here every pid is answered by /proc where it
|
|
5
|
+
* exists (Linux, child-free), else by ONE async `lsof` and ONE async `ps eww`, plus one more `lsof` for suite parents
|
|
6
|
+
* not already probed — the census asks for a parent's cwd too. The answers feed countLiveSuites' existing lookups.
|
|
7
|
+
*/
|
|
8
|
+
export type ProbeExec = (file: string, args: readonly string[]) => Promise<string>;
|
|
9
|
+
/** The pids of every live (non-zombie) row of a `ps -o pid=,ppid=,state=,command=` snapshot whose command is a runner. */
|
|
10
|
+
export declare function runnerPids(snapshot: string): number[];
|
|
11
|
+
/** The /proc reads, injectable so the Linux branch is testable anywhere; each throws where /proc has no entry. */
|
|
12
|
+
export interface ProcReader {
|
|
13
|
+
cwd(pid: number): string;
|
|
14
|
+
environ(pid: number): string;
|
|
15
|
+
}
|
|
16
|
+
export interface CensusProbes {
|
|
17
|
+
cwd: (pid: number) => string | undefined;
|
|
18
|
+
suiteParent: (pid: number) => number | undefined;
|
|
19
|
+
}
|
|
20
|
+
export declare function batchedProcessProbes(pids: readonly number[], exec?: ProbeExec, proc?: ProcReader): Promise<CensusProbes>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { readFileSync, readlinkSync, realpathSync } from "node:fs";
|
|
3
|
+
import { isRunnerCommand } from "./lease.js";
|
|
4
|
+
import { SUITE_PARENT_ENV } from "./git.js";
|
|
5
|
+
// A pid that exits between the snapshot and the probe makes lsof/ps exit non-zero while still printing every other
|
|
6
|
+
// row, so an ordinary non-zero exit (a numeric code, no signal) keeps whatever was printed. D-1564: a timeout, a kill, a
|
|
7
|
+
// spawn failure or an output-cap overflow is no answer — the base's execFileSync threw there and discarded any partial
|
|
8
|
+
// output, so these answer nothing even when they buffered rows first.
|
|
9
|
+
// D-1561: execFile can also throw SYNCHRONOUSLY (EPERM, EAGAIN under fork pressure); the base's per-pid try/catch answered
|
|
10
|
+
// that as "unknown", so this probe never throws or rejects either — it answers nothing.
|
|
11
|
+
const probeExec = (file, args) => new Promise((resolve) => {
|
|
12
|
+
try {
|
|
13
|
+
execFile(file, [...args], { encoding: "utf8", timeout: 15_000, maxBuffer: 16 * 1024 * 1024 }, (error, stdout) => resolve(error && (error.killed || error.signal || typeof error.code !== "number") ? "" : String(stdout ?? "")));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
resolve("");
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
/** The pids of every live (non-zombie) row of a `ps -o pid=,ppid=,state=,command=` snapshot whose command is a runner. */
|
|
20
|
+
export function runnerPids(snapshot) {
|
|
21
|
+
const pids = [];
|
|
22
|
+
for (const line of snapshot.split("\n")) {
|
|
23
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/.exec(line);
|
|
24
|
+
if (match && !match[3].startsWith("Z") && isRunnerCommand(match[4]))
|
|
25
|
+
pids.push(Number(match[1]));
|
|
26
|
+
}
|
|
27
|
+
return pids;
|
|
28
|
+
}
|
|
29
|
+
const procReader = {
|
|
30
|
+
cwd: (pid) => realpathSync(readlinkSync(`/proc/${pid}/cwd`)),
|
|
31
|
+
environ: (pid) => readFileSync(`/proc/${pid}/environ`, "utf8"),
|
|
32
|
+
};
|
|
33
|
+
export async function batchedProcessProbes(pids, exec = probeExec, proc = procReader) {
|
|
34
|
+
// the census never rejects, whatever exec does (queue row 103's closed case table, D-1561)
|
|
35
|
+
const ask = async (file, args) => { try {
|
|
36
|
+
return await exec(file, args);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return "";
|
|
40
|
+
} };
|
|
41
|
+
const cwds = new Map();
|
|
42
|
+
const parents = new Map();
|
|
43
|
+
const parentIn = (text) => new RegExp(`(?:^|\\s)${SUITE_PARENT_ENV}=(\\d+)(?:\\s|$)`).exec(text)?.[1];
|
|
44
|
+
// D-1567 batch poisoning: lsof and ps fail the WHOLE command for one -p member they cannot take (a marker of 22 digits
|
|
45
|
+
// prints as 1e+21, 310 nines as Infinity; ps refuses one above int32), erasing every other pid's answer. The base probed
|
|
46
|
+
// per pid, so such a pid lost only its own answer: it stays unanswered here and never joins a batch.
|
|
47
|
+
const probeable = (pid) => Number.isSafeInteger(pid) && pid > 0 && pid <= 2_147_483_647;
|
|
48
|
+
const probeCwds = async (wanted) => {
|
|
49
|
+
const rest = [];
|
|
50
|
+
for (const pid of wanted.filter(probeable)) {
|
|
51
|
+
try {
|
|
52
|
+
cwds.set(pid, proc.cwd(pid));
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
rest.push(pid);
|
|
56
|
+
} // Darwin has no /proc
|
|
57
|
+
}
|
|
58
|
+
if (rest.length === 0)
|
|
59
|
+
return;
|
|
60
|
+
let pid;
|
|
61
|
+
for (const line of (await ask("lsof", ["-a", "-p", rest.join(","), "-d", "cwd", "-Fn"])).split("\n")) {
|
|
62
|
+
if (line.startsWith("p"))
|
|
63
|
+
pid = Number(line.slice(1));
|
|
64
|
+
else if (line.startsWith("n") && pid !== undefined) {
|
|
65
|
+
try {
|
|
66
|
+
cwds.set(pid, realpathSync(line.slice(1)));
|
|
67
|
+
}
|
|
68
|
+
catch { /* the directory is gone */ }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const probeParents = async (wanted) => {
|
|
73
|
+
const rest = [];
|
|
74
|
+
for (const pid of wanted.filter(probeable)) {
|
|
75
|
+
try {
|
|
76
|
+
// the base's exact rule (7c8009772 daemon.ts:1418-1423, D-1558): ONE NUL-delimited entry, its WHOLE value digits —
|
|
77
|
+
// a joined string would let another variable's text, or a value with trailing junk, pass for the marker
|
|
78
|
+
const value = proc.environ(pid).split("\0").find((entry) => entry.startsWith(`${SUITE_PARENT_ENV}=`))?.slice(SUITE_PARENT_ENV.length + 1);
|
|
79
|
+
if (value && /^\d+$/.test(value))
|
|
80
|
+
parents.set(pid, Number(value));
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
rest.push(pid);
|
|
84
|
+
} // Darwin has no /proc process environments
|
|
85
|
+
}
|
|
86
|
+
if (rest.length === 0)
|
|
87
|
+
return;
|
|
88
|
+
for (const line of (await ask("ps", ["eww", "-p", rest.join(","), "-o", "pid=,command="])).split("\n")) {
|
|
89
|
+
const row = /^\s*(\d+)\s+(.*)$/.exec(line);
|
|
90
|
+
const value = row ? parentIn(row[2]) : undefined;
|
|
91
|
+
if (row && value)
|
|
92
|
+
parents.set(Number(row[1]), Number(value));
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const unique = [...new Set(pids)];
|
|
96
|
+
await probeCwds(unique);
|
|
97
|
+
await probeParents(unique);
|
|
98
|
+
await probeCwds([...new Set(parents.values())].filter((pid) => !cwds.has(pid)));
|
|
99
|
+
return { cwd: (pid) => cwds.get(pid), suiteParent: (pid) => parents.get(pid) };
|
|
100
|
+
}
|