agent-yes 1.266.0 → 1.267.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/{SUPPORTED_CLIS-AFV5pjSx.js → SUPPORTED_CLIS-BByfWaoz.js} +2 -2
- package/dist/{SUPPORTED_CLIS-DRVuSo48.js → SUPPORTED_CLIS-Bob214Yb.js} +3 -3
- package/dist/{agentShare-qdFSp9Uq.js → agentShare-TGChdMCD.js} +3 -3
- package/dist/{callback-DfNFdZin.js → callback-B2thylEC.js} +3 -3
- package/dist/{callback-Da7MRLh6.js → callback-D93yB1xz.js} +3 -3
- package/dist/cli.js +11 -11
- package/dist/cmdPs-D7NlwNFV.js +577 -0
- package/dist/{cwdConflictWarn-CtQhl-vF.js → cwdConflictWarn-BpweoAiy.js} +1 -1
- package/dist/{cwdPassthroughHint-apaX_O7p.js → cwdPassthroughHint-BlRuOGo_.js} +1 -1
- package/dist/{expose-BXn6My6A.js → expose-DyiZ5AU0.js} +1 -1
- package/dist/{forkNested-CDfWPiHw.js → forkNested-B_4c-TQk.js} +1 -1
- package/dist/{hist-BpocAe1N.js → hist-BoZPJ2ah.js} +1 -1
- package/dist/index.js +2 -2
- package/dist/{initMsg-C-w4A_eF.js → initMsg-BKqWDYFk.js} +1 -1
- package/dist/{initMsg-BjRseolL.js → initMsg-D8cmdY76.js} +1 -1
- package/dist/{nodeRuntime-CRoEMWkp.js → nodeRuntime-QihGFQbI.js} +1 -1
- package/dist/{notifyDaemon-BFzqcdN1.js → notifyDaemon-B-vCwFi3.js} +3 -3
- package/dist/{openBrowser-DhQ9pJRi.js → openBrowser-Cs46NiH6.js} +1 -1
- package/dist/{parentLink-B798ASCW.js → parentLink-CQrn998L.js} +1 -1
- package/dist/{parentLink-C0cIRjfL.js → parentLink-CTgZZCgw.js} +1 -1
- package/dist/{parentPingLoop-BIKxzbPH.js → parentPingLoop-CVD0sREv.js} +4 -4
- package/dist/{remotes-iMhdZytp.js → remotes-DdAfueOu.js} +2 -2
- package/dist/{remotes-Dg_JscKk.js → remotes-M8FbM92D.js} +1 -1
- package/dist/{rustBinary-JGNROWfY.js → rustBinary-Bf9514x8.js} +2 -2
- package/dist/{schedule-DbLpjgl5.js → schedule-fgKVzax1.js} +6 -6
- package/dist/{serve-ucVXOci1.js → serve-DM5-VJPX.js} +26 -26
- package/dist/{setup-De7uYRZb.js → setup-DNuxXwYS.js} +4 -4
- package/dist/{spawnGate-BvaZeHE7.js → spawnGate-CZlMs6uV.js} +1 -1
- package/dist/{spawnGate-CEXVmTtv.js → spawnGate-DN4wNXHq.js} +2 -2
- package/dist/{subcommands-C04exkIM.js → subcommands-3rBreZdT.js} +2 -2
- package/dist/{subcommands-DtF31RoW.js → subcommands-ujgVZRT3.js} +24 -24
- package/dist/{systemPathLink-7TFPwJna.js → systemPathLink-BHv1I6wh.js} +1 -1
- package/dist/{terminal-Dm32SXU-.js → terminal-jJ2YDBDS.js} +2 -2
- package/dist/{todoCli-c_R7D7Y-.js → todoCli-BYGZ7_C5.js} +1 -1
- package/dist/{tray-DZnGjJBK.js → tray-CzGs9-ch.js} +1 -1
- package/dist/trayApp-CPDWlxfV.js +5 -0
- package/dist/{trayApp-CfkPG0wy.js → trayApp-DbV2E8dZ.js} +2 -2
- package/dist/{ts-CGtoPER5.js → ts-CQIqcW7_.js} +5 -5
- package/dist/{versionChecker-CJgW9DHY.js → versionChecker-D49xkrMv.js} +2 -2
- package/dist/{webrtcRemote-BDg0jjTs.js → webrtcRemote-Bl8eowma.js} +1 -1
- package/dist/{widget-DXtaX3QR.js → widget-CRXwzRP7.js} +3 -3
- package/dist/{workspaceConfig-HiWQzq6A.js → workspaceConfig-CBKlgA_q.js} +1 -1
- package/dist/{ws-aKRZpI9b.js → ws-B4ZoCy71.js} +3 -3
- package/dist/{ws-CqlS5JhN.js → ws-DomYQT_I.js} +3 -3
- package/lab/ui/sw.js +10 -1
- package/package.json +1 -1
- package/ts/cmdPs.spec.ts +389 -0
- package/ts/cmdPs.ts +343 -0
- package/ts/procStats.spec.ts +506 -0
- package/ts/procStats.ts +484 -0
- package/ts/subcommands.ts +9 -4
- package/dist/trayApp-CZbwLK9t.js +0 -5
package/ts/cmdPs.ts
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ay ps` — what each agent COSTS, rolled up per process tree.
|
|
3
|
+
*
|
|
4
|
+
* The split from `ay ls`: `ls` answers "what is running and what is it doing"
|
|
5
|
+
* (prompt, git, badges, forest); `ps` answers "what is this box spending on
|
|
6
|
+
* each agent, and is any of it safe to reclaim". Same rows, different question.
|
|
7
|
+
*
|
|
8
|
+
* `ps` used to be a plain alias for `ls`. It is now its own command, because the
|
|
9
|
+
* number you actually want is not in any `ps`/`htop` output: one agent is a
|
|
10
|
+
* whole subtree of wrapper + CLI + pty hosts + subagents, and only ay knows
|
|
11
|
+
* which pids collapse into which session.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately local-only. Resource numbers are per-machine and there is nothing
|
|
14
|
+
* meaningful to sum across remotes; `ay ls` remains the fleet-wide view.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import yargs from "yargs";
|
|
21
|
+
import { buildAgentForest, flattenForest } from "./agentTree.ts";
|
|
22
|
+
import {
|
|
23
|
+
humanBytes,
|
|
24
|
+
sampleTrees,
|
|
25
|
+
snapshotProcs,
|
|
26
|
+
systemStats,
|
|
27
|
+
type SystemStats,
|
|
28
|
+
type TreeStats,
|
|
29
|
+
} from "./procStats.ts";
|
|
30
|
+
import { deriveLiveState, listRecords } from "./subcommands.ts";
|
|
31
|
+
|
|
32
|
+
/** Default CPU sampling window. Long enough to be meaningful, short enough to feel instant. */
|
|
33
|
+
const DEFAULT_WINDOW_MS = 1000;
|
|
34
|
+
|
|
35
|
+
function shortenPath(p: string): string {
|
|
36
|
+
const home = homedir();
|
|
37
|
+
return p.startsWith(home) ? "~" + p.slice(home.length) : p;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Trailing path segment that identifies a worktree — `/code/x/foo/tree/main` → `foo`. */
|
|
41
|
+
export function repoLabel(cwd: string): string {
|
|
42
|
+
// Split on BOTH separators, not `path.sep`. Agent cwds come off the registry,
|
|
43
|
+
// which stores whatever the wrapper recorded — a posix path is perfectly
|
|
44
|
+
// normal on a Windows host (msys/WSL shells, or a record synced from another
|
|
45
|
+
// box). Keying on path.sep left the whole path unsplit there, so the REPO
|
|
46
|
+
// column printed the full path instead of the repo name.
|
|
47
|
+
const parts = cwd.split(/[\\/]+/).filter(Boolean);
|
|
48
|
+
const treeAt = parts.lastIndexOf("tree");
|
|
49
|
+
// The repo's own name is more useful than the branch dir every checkout shares.
|
|
50
|
+
if (treeAt > 0) return parts[treeAt - 1] as string;
|
|
51
|
+
return parts[parts.length - 1] ?? shortenPath(cwd);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** `15.23 12.52 9.86 (8 cpu, 1.9x) mem 13.0/15Gi swap 4.8/4.8Gi FULL zombies 1440` */
|
|
55
|
+
export function formatSystemLine(sys: SystemStats): string {
|
|
56
|
+
const bits: string[] = [];
|
|
57
|
+
if (sys.load) {
|
|
58
|
+
const [one, five, fifteen] = sys.load;
|
|
59
|
+
const ratio = one / Math.max(1, sys.ncpu);
|
|
60
|
+
// Flag oversubscription explicitly — "15.23" means nothing without the core
|
|
61
|
+
// count beside it, and that ratio is the whole point of the number.
|
|
62
|
+
bits.push(
|
|
63
|
+
`load ${one.toFixed(2)} ${five.toFixed(2)} ${fifteen.toFixed(2)} ` +
|
|
64
|
+
`(${sys.ncpu} cpu, ${ratio.toFixed(1)}x)`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (sys.memTotalBytes !== null) {
|
|
68
|
+
const used = sys.memAvailableBytes !== null ? sys.memTotalBytes - sys.memAvailableBytes : null;
|
|
69
|
+
bits.push(`mem ${humanBytes(used)}/${humanBytes(sys.memTotalBytes)}`);
|
|
70
|
+
}
|
|
71
|
+
if (sys.swapTotalBytes !== null && sys.swapTotalBytes > 0) {
|
|
72
|
+
const usedSwap = sys.swapFreeBytes !== null ? sys.swapTotalBytes - sys.swapFreeBytes : null;
|
|
73
|
+
// A full swap is the difference between "loaded" and "one allocation away
|
|
74
|
+
// from the OOM killer", so it gets a word rather than just a ratio.
|
|
75
|
+
const full = sys.swapFreeBytes !== null && sys.swapFreeBytes <= 0 ? " FULL" : "";
|
|
76
|
+
bits.push(`swap ${humanBytes(usedSwap)}/${humanBytes(sys.swapTotalBytes)}${full}`);
|
|
77
|
+
}
|
|
78
|
+
if (sys.zombies > 0) bits.push(`zombies ${sys.zombies}`);
|
|
79
|
+
return bits.join(" ");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface PsRow {
|
|
83
|
+
pid: number;
|
|
84
|
+
cli: string;
|
|
85
|
+
status: string;
|
|
86
|
+
cwd: string;
|
|
87
|
+
repo: string;
|
|
88
|
+
stats: TreeStats;
|
|
89
|
+
self: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function pad(s: string, n: number): string {
|
|
93
|
+
return s.length >= n ? s : s + " ".repeat(n - s.length);
|
|
94
|
+
}
|
|
95
|
+
function padStart(s: string, n: number): string {
|
|
96
|
+
return s.length >= n ? s : " ".repeat(n - s.length) + s;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function renderTable(rows: PsRow[], unattributed: TreeStats | null): string {
|
|
100
|
+
const cells = rows.map((r) => ({
|
|
101
|
+
pid: String(r.pid),
|
|
102
|
+
cli: r.cli,
|
|
103
|
+
status: r.status,
|
|
104
|
+
cpu: r.stats.cpuPercent.toFixed(1),
|
|
105
|
+
rss: humanBytes(r.stats.rss),
|
|
106
|
+
procs: String(r.stats.procs),
|
|
107
|
+
repo: r.repo,
|
|
108
|
+
self: r.self,
|
|
109
|
+
}));
|
|
110
|
+
// The unmanaged row participates in the width pass — "unmanaged" is wider than
|
|
111
|
+
// any real CLI name, so sizing without it knocks the whole table out of line.
|
|
112
|
+
if (unattributed && unattributed.procs > 0) {
|
|
113
|
+
cells.push({
|
|
114
|
+
pid: "-",
|
|
115
|
+
cli: "unmanaged",
|
|
116
|
+
status: "-",
|
|
117
|
+
cpu: unattributed.cpuPercent.toFixed(1),
|
|
118
|
+
rss: humanBytes(unattributed.rss),
|
|
119
|
+
procs: String(unattributed.procs),
|
|
120
|
+
repo: "-",
|
|
121
|
+
self: false,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
const w = {
|
|
125
|
+
pid: Math.max(3, ...cells.map((c) => c.pid.length)),
|
|
126
|
+
cli: Math.max(3, ...cells.map((c) => c.cli.length)),
|
|
127
|
+
status: Math.max(6, ...cells.map((c) => c.status.length)),
|
|
128
|
+
cpu: Math.max(5, ...cells.map((c) => c.cpu.length)),
|
|
129
|
+
rss: Math.max(5, ...cells.map((c) => c.rss.length)),
|
|
130
|
+
procs: Math.max(5, ...cells.map((c) => c.procs.length)),
|
|
131
|
+
repo: Math.max(4, ...cells.map((c) => c.repo.length)),
|
|
132
|
+
};
|
|
133
|
+
const line = (
|
|
134
|
+
pid: string,
|
|
135
|
+
cli: string,
|
|
136
|
+
status: string,
|
|
137
|
+
cpu: string,
|
|
138
|
+
rss: string,
|
|
139
|
+
procs: string,
|
|
140
|
+
repo: string,
|
|
141
|
+
tail = "",
|
|
142
|
+
) =>
|
|
143
|
+
`${pad(pid, w.pid)} ${pad(cli, w.cli)} ${pad(status, w.status)} ` +
|
|
144
|
+
`${padStart(cpu, w.cpu)} ${padStart(rss, w.rss)} ${padStart(procs, w.procs)} ` +
|
|
145
|
+
`${pad(repo, w.repo)}${tail}`;
|
|
146
|
+
|
|
147
|
+
const out: string[] = [line("PID", "CLI", "STATUS", "CPU%", "RSS", "PROCS", "REPO")];
|
|
148
|
+
for (const c of cells) {
|
|
149
|
+
out.push(
|
|
150
|
+
line(c.pid, c.cli, c.status, c.cpu, c.rss, c.procs, c.repo, c.self ? " ← this session" : ""),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return out.join("\n");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function cmdPs(rest: string[]): Promise<number> {
|
|
157
|
+
const y = yargs(rest)
|
|
158
|
+
.usage(
|
|
159
|
+
"Usage: ay ps [keyword] [options]\n\n" +
|
|
160
|
+
"Per-agent resource usage, rolled up across each agent's whole process tree\n" +
|
|
161
|
+
"(wrapper + CLI + pty hosts + subagents). Local-only; use `ay ls` for the\n" +
|
|
162
|
+
"fleet view and per-agent prompts.",
|
|
163
|
+
)
|
|
164
|
+
.option("all", {
|
|
165
|
+
type: "boolean",
|
|
166
|
+
default: false,
|
|
167
|
+
description: "Include exited agents",
|
|
168
|
+
})
|
|
169
|
+
.option("json", { type: "boolean", default: false, description: "Output as JSON" })
|
|
170
|
+
.option("sort", {
|
|
171
|
+
type: "string",
|
|
172
|
+
choices: ["rss", "cpu", "procs", "pid"],
|
|
173
|
+
default: "rss",
|
|
174
|
+
description: "Sort rows by resource usage (default: rss, biggest first)",
|
|
175
|
+
})
|
|
176
|
+
.option("tree", {
|
|
177
|
+
type: "boolean",
|
|
178
|
+
default: false,
|
|
179
|
+
description: "Keep ay ls's parent>child forest order instead of sorting",
|
|
180
|
+
})
|
|
181
|
+
.option("interval", {
|
|
182
|
+
type: "number",
|
|
183
|
+
default: DEFAULT_WINDOW_MS / 1000,
|
|
184
|
+
description: "CPU sampling window in seconds — CPU% is a live delta, not a lifetime average",
|
|
185
|
+
})
|
|
186
|
+
.option("cwd", { type: "string", description: "Restrict to agents whose cwd starts with dir" })
|
|
187
|
+
.option("help", { alias: "h", type: "boolean", default: false, description: "Show this help" })
|
|
188
|
+
.example("ay ps", "biggest agents first, with box vitals")
|
|
189
|
+
.example("ay ps --sort cpu", "who is actually burning CPU right now")
|
|
190
|
+
.example("ay ps --json", "machine-readable rollup")
|
|
191
|
+
.help(false)
|
|
192
|
+
.version(false)
|
|
193
|
+
.exitProcess(false);
|
|
194
|
+
|
|
195
|
+
const argv = await y.parseAsync();
|
|
196
|
+
if (argv.help || argv.h) {
|
|
197
|
+
process.stdout.write((await y.getHelp()) + "\n");
|
|
198
|
+
return 0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const keyword = argv._[0] !== undefined ? String(argv._[0]) : undefined;
|
|
202
|
+
const records = await listRecords(keyword, {
|
|
203
|
+
all: argv.all,
|
|
204
|
+
active: false,
|
|
205
|
+
json: false,
|
|
206
|
+
latest: false,
|
|
207
|
+
cwdScope: typeof argv.cwd === "string" ? path.resolve(argv.cwd) : null,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
if (records.length === 0) {
|
|
211
|
+
process.stderr.write(keyword ? `no agents matched "${keyword}"\n` : "no running agents\n");
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// `flattenForest` yields parents before children; sampleTrees attributes
|
|
216
|
+
// first-come, so sampling in that order would let a parent absorb a nested
|
|
217
|
+
// agent's whole subtree and render the child as a bogus 0-proc row. Reverse to
|
|
218
|
+
// claim deepest-first, then display in forest order.
|
|
219
|
+
const ordered = flattenForest(buildAgentForest(records)).map((r) => r.record);
|
|
220
|
+
const windowMs = Math.max(100, (Number.isFinite(argv.interval) ? argv.interval : 1) * 1000);
|
|
221
|
+
const { trees, unattributed } = await sampleTrees(
|
|
222
|
+
[...ordered].reverse().map((r) => r.pid),
|
|
223
|
+
{ windowMs },
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const states = new Map(
|
|
227
|
+
await Promise.all(ordered.map(async (r) => [r.pid, (await deriveLiveState(r)).state] as const)),
|
|
228
|
+
);
|
|
229
|
+
const selfPid = process.pid;
|
|
230
|
+
const rows: PsRow[] = ordered.map((r) => ({
|
|
231
|
+
pid: r.pid,
|
|
232
|
+
cli: r.cli,
|
|
233
|
+
status: states.get(r.pid) ?? r.status,
|
|
234
|
+
cwd: r.cwd,
|
|
235
|
+
repo: repoLabel(r.cwd),
|
|
236
|
+
stats: trees.get(r.pid) ?? { pid: r.pid, rss: 0, cpuPercent: 0, procs: 0 },
|
|
237
|
+
// Mark the tree we are standing in — anything that later grows a kill
|
|
238
|
+
// affordance must not let you shoot the session issuing the command.
|
|
239
|
+
self: trees.get(r.pid) !== undefined && isSelfTree(r.pid, selfPid),
|
|
240
|
+
}));
|
|
241
|
+
|
|
242
|
+
if (!argv.tree) {
|
|
243
|
+
const key = String(argv.sort);
|
|
244
|
+
rows.sort((a, b) => {
|
|
245
|
+
if (key === "cpu") return b.stats.cpuPercent - a.stats.cpuPercent;
|
|
246
|
+
if (key === "procs") return b.stats.procs - a.stats.procs;
|
|
247
|
+
if (key === "pid") return a.pid - b.pid;
|
|
248
|
+
return b.stats.rss - a.stats.rss;
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const sys = await systemStats(await snapshotProcs());
|
|
253
|
+
|
|
254
|
+
if (argv.json) {
|
|
255
|
+
process.stdout.write(
|
|
256
|
+
JSON.stringify(
|
|
257
|
+
{
|
|
258
|
+
system: sys,
|
|
259
|
+
agents: rows.map((r) => ({
|
|
260
|
+
pid: r.pid,
|
|
261
|
+
cli: r.cli,
|
|
262
|
+
status: r.status,
|
|
263
|
+
cwd: r.cwd,
|
|
264
|
+
rssBytes: r.stats.rss,
|
|
265
|
+
cpuPercent: Number(r.stats.cpuPercent.toFixed(2)),
|
|
266
|
+
procs: r.stats.procs,
|
|
267
|
+
self: r.self,
|
|
268
|
+
})),
|
|
269
|
+
unattributed: {
|
|
270
|
+
rssBytes: unattributed.rss,
|
|
271
|
+
cpuPercent: Number(unattributed.cpuPercent.toFixed(2)),
|
|
272
|
+
procs: unattributed.procs,
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
null,
|
|
276
|
+
2,
|
|
277
|
+
) + "\n",
|
|
278
|
+
);
|
|
279
|
+
return 0;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const header = formatSystemLine(sys);
|
|
283
|
+
if (header) process.stdout.write(` ${header}\n\n`);
|
|
284
|
+
process.stdout.write(renderTable(rows, unattributed) + "\n");
|
|
285
|
+
return 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Is `rootPid` the wrapper of the tree this very process lives in?
|
|
290
|
+
*
|
|
291
|
+
* Walks our own ancestry rather than the snapshot: cheap, and correct even when
|
|
292
|
+
* the snapshot raced. Linux-only detail (reads /proc), returns false elsewhere —
|
|
293
|
+
* mislabelling the self row is cosmetic.
|
|
294
|
+
*/
|
|
295
|
+
export function isSelfTree(
|
|
296
|
+
rootPid: number,
|
|
297
|
+
selfPid: number,
|
|
298
|
+
// Injectable so the ancestry walk itself is testable off Linux — the real
|
|
299
|
+
// reader is /proc-only, but the hop/cycle logic is platform-independent and
|
|
300
|
+
// is the part that can actually be wrong.
|
|
301
|
+
readPpid: (pid: number) => number | null = readPpidSync,
|
|
302
|
+
platform: string = process.platform,
|
|
303
|
+
): boolean {
|
|
304
|
+
if (platform !== "linux") return false;
|
|
305
|
+
let pid = selfPid;
|
|
306
|
+
// Bounded: a /proc that reports a cycle (or a pid1 that is its own parent)
|
|
307
|
+
// must not spin here.
|
|
308
|
+
for (let hops = 0; hops < 64 && pid > 1; hops++) {
|
|
309
|
+
if (pid === rootPid) return true;
|
|
310
|
+
const ppid = readPpid(pid);
|
|
311
|
+
if (ppid === null) return false;
|
|
312
|
+
pid = ppid;
|
|
313
|
+
}
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* ppid out of one `/proc/<pid>/stat` line.
|
|
319
|
+
*
|
|
320
|
+
* Split from the read so the parsing — the part that can actually be wrong — is
|
|
321
|
+
* testable on hosts without /proc. Field 2 is `comm`, wrapped in parens and NOT
|
|
322
|
+
* escaped: a process named `foo) bar (baz` is legal, so the fields after it can
|
|
323
|
+
* only be found from the LAST `)`, never by splitting on whitespace.
|
|
324
|
+
*/
|
|
325
|
+
export function parsePpidFromStat(stat: string): number | null {
|
|
326
|
+
const close = stat.lastIndexOf(")");
|
|
327
|
+
if (close < 0) return null;
|
|
328
|
+
const ppid = Number(
|
|
329
|
+
stat
|
|
330
|
+
.slice(close + 1)
|
|
331
|
+
.trim()
|
|
332
|
+
.split(/\s+/)[1],
|
|
333
|
+
);
|
|
334
|
+
return Number.isFinite(ppid) ? ppid : null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function readPpidSync(pid: number): number | null {
|
|
338
|
+
try {
|
|
339
|
+
return parsePpidFromStat(readFileSync(`/proc/${pid}/stat`, "utf8"));
|
|
340
|
+
} catch {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
}
|