debuganything 0.0.0-stage → 1.0.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/.claude-plugin/plugin.json +15 -0
- package/DISCLAIMER.md +101 -0
- package/GEMINI.md +97 -0
- package/LICENSE +21 -0
- package/README.md +283 -2
- package/assets/README.md +9 -0
- package/assets/debuganything-logo.png +0 -0
- package/assets/debuganything-mark-512.png +0 -0
- package/dist/chunk-32IDKAZK.js +9 -0
- package/dist/chunk-32IDKAZK.js.map +1 -0
- package/dist/chunk-5BXTDASF.js +135 -0
- package/dist/chunk-5BXTDASF.js.map +1 -0
- package/dist/chunk-5RAKTYW5.js +292 -0
- package/dist/chunk-5RAKTYW5.js.map +1 -0
- package/dist/chunk-7MMXFUUN.js +79 -0
- package/dist/chunk-7MMXFUUN.js.map +1 -0
- package/dist/chunk-D742WJPT.js +436 -0
- package/dist/chunk-D742WJPT.js.map +1 -0
- package/dist/chunk-EMR42ZJQ.js +102 -0
- package/dist/chunk-EMR42ZJQ.js.map +1 -0
- package/dist/chunk-MLDOO673.js +1416 -0
- package/dist/chunk-MLDOO673.js.map +1 -0
- package/dist/chunk-NFL22T3H.js +309 -0
- package/dist/chunk-NFL22T3H.js.map +1 -0
- package/dist/chunk-STFC7C5C.js +245 -0
- package/dist/chunk-STFC7C5C.js.map +1 -0
- package/dist/chunk-VGYXHXTY.js +1015 -0
- package/dist/chunk-VGYXHXTY.js.map +1 -0
- package/dist/chunk-YY6M5RIJ.js +215 -0
- package/dist/chunk-YY6M5RIJ.js.map +1 -0
- package/dist/cli.js +254 -0
- package/dist/cli.js.map +1 -0
- package/dist/docker-ORXYMES4.js +23 -0
- package/dist/docker-ORXYMES4.js.map +1 -0
- package/dist/hook-QYLLBUNK.js +14 -0
- package/dist/hook-QYLLBUNK.js.map +1 -0
- package/dist/import-CT55AEEE.js +268 -0
- package/dist/import-CT55AEEE.js.map +1 -0
- package/dist/investigate-2GFD6EWB.js +102 -0
- package/dist/investigate-2GFD6EWB.js.map +1 -0
- package/dist/lsof-GBLZJBJR.js +22 -0
- package/dist/lsof-GBLZJBJR.js.map +1 -0
- package/dist/psmem-UB3VCMG3.js +20 -0
- package/dist/psmem-UB3VCMG3.js.map +1 -0
- package/dist/report_html-2ENM7HLC.js +99 -0
- package/dist/report_html-2ENM7HLC.js.map +1 -0
- package/dist/server.js +190 -0
- package/dist/server.js.map +1 -0
- package/dist/setup-NTPRUTMT.js +265 -0
- package/dist/setup-NTPRUTMT.js.map +1 -0
- package/dist/target-WJE4WFYA.js +36 -0
- package/dist/target-WJE4WFYA.js.map +1 -0
- package/gemini-extension.json +9 -0
- package/hooks/debuganything-hook.mjs +36 -0
- package/hooks/hooks.json +10 -0
- package/package.json +85 -3
- package/server.json +30 -0
- package/skills/debug-anything/SKILL.md +102 -0
- package/skills/inspect-product/SKILL.md +138 -0
|
@@ -0,0 +1,1416 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
parseLsofFields
|
|
4
|
+
} from "./chunk-NFL22T3H.js";
|
|
5
|
+
import {
|
|
6
|
+
ResultBuilder,
|
|
7
|
+
humanBytes
|
|
8
|
+
} from "./chunk-5BXTDASF.js";
|
|
9
|
+
import {
|
|
10
|
+
targetPids
|
|
11
|
+
} from "./chunk-5RAKTYW5.js";
|
|
12
|
+
import {
|
|
13
|
+
platform
|
|
14
|
+
} from "./chunk-D742WJPT.js";
|
|
15
|
+
import {
|
|
16
|
+
LIMITS,
|
|
17
|
+
assertSafeBpfFragment,
|
|
18
|
+
clampDuration,
|
|
19
|
+
isRoot,
|
|
20
|
+
looksLikePermissionError,
|
|
21
|
+
rootRequiredLimitation,
|
|
22
|
+
runCapped
|
|
23
|
+
} from "./chunk-STFC7C5C.js";
|
|
24
|
+
|
|
25
|
+
// src/adapters/pyspy.ts
|
|
26
|
+
import { readFileSync } from "fs";
|
|
27
|
+
|
|
28
|
+
// src/stacks.ts
|
|
29
|
+
function parseFolded(raw) {
|
|
30
|
+
const out = [];
|
|
31
|
+
for (const line of raw.split("\n")) {
|
|
32
|
+
const t = line.trimEnd();
|
|
33
|
+
if (!t) continue;
|
|
34
|
+
const sp = t.lastIndexOf(" ");
|
|
35
|
+
if (sp < 0) continue;
|
|
36
|
+
const count = Number(t.slice(sp + 1));
|
|
37
|
+
if (!Number.isFinite(count)) continue;
|
|
38
|
+
const frames = t.slice(0, sp).split(";").filter(Boolean);
|
|
39
|
+
if (frames.length) out.push({ frames, count });
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
function aggregateFrames(stacks, limit = 15) {
|
|
44
|
+
const self = /* @__PURE__ */ new Map();
|
|
45
|
+
const total = /* @__PURE__ */ new Map();
|
|
46
|
+
let totalSamples = 0;
|
|
47
|
+
for (const s of stacks) {
|
|
48
|
+
totalSamples += s.count;
|
|
49
|
+
const leaf = s.frames[s.frames.length - 1];
|
|
50
|
+
self.set(leaf, (self.get(leaf) ?? 0) + s.count);
|
|
51
|
+
for (const f of new Set(s.frames)) total.set(f, (total.get(f) ?? 0) + s.count);
|
|
52
|
+
}
|
|
53
|
+
const frames = [...total.keys()].map((frame) => ({
|
|
54
|
+
frame,
|
|
55
|
+
self: self.get(frame) ?? 0,
|
|
56
|
+
total: total.get(frame) ?? 0,
|
|
57
|
+
self_pct: totalSamples ? Math.round(1e3 * (self.get(frame) ?? 0) / totalSamples) / 10 : 0,
|
|
58
|
+
total_pct: totalSamples ? Math.round(1e3 * (total.get(frame) ?? 0) / totalSamples) / 10 : 0
|
|
59
|
+
})).sort((a, b) => b.self - a.self || b.total - a.total).slice(0, limit);
|
|
60
|
+
return { frames, totalSamples };
|
|
61
|
+
}
|
|
62
|
+
function hottestStacks(stacks, limit = 5, maxDepth = 12) {
|
|
63
|
+
const total = stacks.reduce((s, x) => s + x.count, 0);
|
|
64
|
+
return [...stacks].sort((a, b) => b.count - a.count).slice(0, limit).map((s) => ({ count: s.count, pct: total ? Math.round(1e3 * s.count / total) / 10 : 0, stack: s.frames.length > maxDepth ? [...s.frames.slice(0, 3), `\u2026 ${s.frames.length - maxDepth + 3} frames \u2026`, ...s.frames.slice(-(maxDepth - 4))] : s.frames }));
|
|
65
|
+
}
|
|
66
|
+
var IDLE_FRAME = /\b(semaphore_wait_trap|__psynch_mutexwait|__ulock_wait|__workq_kernreturn|__sigsuspend|__wait4|epoll_wait|epoll_pwait|kevent|poll|select|futex|nanosleep|clock_nanosleep|pthread_cond_wait|pthread_cond_timedwait|__psynch_cvwait|mach_msg|mach_msg2_trap|__semwait_signal|__select|__workq_kernreturn|uv__io_poll|uv_run|sleep|time\.sleep|wait|_wait_for_tstate_lock|<idle>|swapper)\b/i;
|
|
67
|
+
function buildTree(stacks) {
|
|
68
|
+
const root = { name: "all", value: 0, children: /* @__PURE__ */ new Map() };
|
|
69
|
+
for (const s of stacks) {
|
|
70
|
+
root.value += s.count;
|
|
71
|
+
let cur = root;
|
|
72
|
+
for (const f of s.frames) {
|
|
73
|
+
let n = cur.children.get(f);
|
|
74
|
+
if (!n) {
|
|
75
|
+
n = { name: f, value: 0, children: /* @__PURE__ */ new Map() };
|
|
76
|
+
cur.children.set(f, n);
|
|
77
|
+
}
|
|
78
|
+
n.value += s.count;
|
|
79
|
+
cur = n;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return root;
|
|
83
|
+
}
|
|
84
|
+
function esc(s) {
|
|
85
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
86
|
+
}
|
|
87
|
+
function colorFor(name) {
|
|
88
|
+
let h = 0;
|
|
89
|
+
for (let i = 0; i < name.length; i++) h = h * 31 + name.charCodeAt(i) >>> 0;
|
|
90
|
+
const r = 205 + h % 50;
|
|
91
|
+
const g = 60 + (h >> 8) % 120;
|
|
92
|
+
const b = (h >> 16) % 60;
|
|
93
|
+
return `rgb(${r},${g},${b})`;
|
|
94
|
+
}
|
|
95
|
+
function renderFlamegraph(stacks, opts = {}) {
|
|
96
|
+
const width = opts.width ?? 1200;
|
|
97
|
+
const rowH = 16;
|
|
98
|
+
const minW = opts.minWidthPx ?? 0.5;
|
|
99
|
+
const root = buildTree(stacks);
|
|
100
|
+
const total = root.value || 1;
|
|
101
|
+
let maxDepth = 0;
|
|
102
|
+
const rects = [];
|
|
103
|
+
const walk = (n, x, depth) => {
|
|
104
|
+
const w = n.value / total * width;
|
|
105
|
+
if (w < minW) return;
|
|
106
|
+
maxDepth = Math.max(maxDepth, depth);
|
|
107
|
+
const label = w > 40 ? n.name.slice(0, Math.floor(w / 7)) : "";
|
|
108
|
+
rects.push(
|
|
109
|
+
`<g class="f"><title>${esc(n.name)} (${n.value} samples, ${(100 * n.value / total).toFixed(2)}%)</title><rect x="${x.toFixed(2)}" y="${depth}" width="${w.toFixed(2)}" height="${rowH - 1}" fill="${colorFor(n.name)}" rx="1"/>${label ? `<text x="${(x + 2).toFixed(2)}" y="${depth + 11.5}">${esc(label)}</text>` : ""}</g>`
|
|
110
|
+
);
|
|
111
|
+
let cx = x;
|
|
112
|
+
const kids = [...n.children.values()].sort((a, b) => b.value - a.value);
|
|
113
|
+
for (const k of kids) {
|
|
114
|
+
walk(k, cx, depth + 1);
|
|
115
|
+
cx += k.value / total * width;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
walk(root, 0, 0);
|
|
119
|
+
const height = (maxDepth + 1) * rowH + 40;
|
|
120
|
+
const body = rects.map((r) => r.replace(/ y="(\d+)"/, (_m, d) => ` y="${height - 24 - (Number(d) + 1) * rowH}"`).replace(/<text x="([\d.]+)" y="(\d+)\.5"/, (_m, x, d) => `<text x="${x}" y="${height - 24 - (Number(d) + 1) * rowH + 11.5}"`)).join("\n");
|
|
121
|
+
return `<?xml version="1.0" standalone="no"?>
|
|
122
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" font-family="Verdana, sans-serif" font-size="11">
|
|
123
|
+
<style>.f rect:hover{stroke:#000;stroke-width:1}.f text{pointer-events:none;fill:#000}</style>
|
|
124
|
+
<rect width="100%" height="100%" fill="#f8f8f8"/>
|
|
125
|
+
<text x="${width / 2}" y="18" text-anchor="middle" font-size="15">${esc(opts.title ?? "Flame Graph")} \u2014 ${total} samples</text>
|
|
126
|
+
${body}
|
|
127
|
+
<text x="8" y="${height - 6}" fill="#666">Generated by DEA (Debug Anything). Hover for counts; width = share of samples.</text>
|
|
128
|
+
</svg>
|
|
129
|
+
`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/adapters/pyspy.ts
|
|
133
|
+
function parsePySpyDump(raw) {
|
|
134
|
+
const out = { threads: [] };
|
|
135
|
+
let cur = null;
|
|
136
|
+
let localsTarget = null;
|
|
137
|
+
for (const line of raw.split("\n")) {
|
|
138
|
+
let m = /^Process (\d+): (.*)$/.exec(line);
|
|
139
|
+
if (m) {
|
|
140
|
+
out.pid = Number(m[1]);
|
|
141
|
+
out.command = m[2].trim();
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
m = /^Python v([\d.]+)/.exec(line);
|
|
145
|
+
if (m) {
|
|
146
|
+
out.python = m[1];
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
m = /^Thread (0x[0-9A-Fa-f]+|\d+) \((\w+)\)(?:: "(.*)")?(?:.*?\(os thread (\S+)\))?/.exec(line);
|
|
150
|
+
if (m) {
|
|
151
|
+
cur = { id: m[1], state: m[2], name: m[3] ?? "", frames: [], os_tid: m[4] };
|
|
152
|
+
out.threads.push(cur);
|
|
153
|
+
localsTarget = null;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (!cur) continue;
|
|
157
|
+
m = /^\s{4}(\S.*?) \((.+?):(\d+)\)\s*$/.exec(line);
|
|
158
|
+
if (m) {
|
|
159
|
+
cur.frames.push({ func: m[1], file: m[2], line: Number(m[3]) });
|
|
160
|
+
localsTarget = null;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (/^\s{8}(Arguments|Locals):/.test(line)) {
|
|
164
|
+
const f = cur.frames[cur.frames.length - 1];
|
|
165
|
+
if (f) {
|
|
166
|
+
f.locals ??= {};
|
|
167
|
+
localsTarget = f.locals;
|
|
168
|
+
}
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
m = /^\s{12}(\w+): (.*)$/.exec(line);
|
|
172
|
+
if (m && localsTarget) localsTarget[m[1]] = m[2].slice(0, 200);
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
var STDLIB_FILE = /(^|\/)(threading|queue|socket|selectors|ssl|asyncio\/[a-z_]+|concurrent\/futures\/[a-z_]+|subprocess|time|multiprocessing\/[a-z_]+)\.py$/;
|
|
177
|
+
function sourceLine(file, line, reader = defaultReader) {
|
|
178
|
+
if (!file || !/\.py$/.test(file) || line < 1) return void 0;
|
|
179
|
+
const text = reader(file);
|
|
180
|
+
if (!text) return void 0;
|
|
181
|
+
return text.split("\n")[line - 1]?.trim().slice(0, 160);
|
|
182
|
+
}
|
|
183
|
+
var srcCache = /* @__PURE__ */ new Map();
|
|
184
|
+
function defaultReader(f) {
|
|
185
|
+
if (srcCache.has(f)) return srcCache.get(f);
|
|
186
|
+
let v;
|
|
187
|
+
try {
|
|
188
|
+
v = readFileSync(f, "utf8");
|
|
189
|
+
} catch {
|
|
190
|
+
v = void 0;
|
|
191
|
+
}
|
|
192
|
+
srcCache.set(f, v);
|
|
193
|
+
return v;
|
|
194
|
+
}
|
|
195
|
+
var LOCK_SRC = /\bwith\b[^:]*(lock|mutex|sem|cond|rlock|lk\b)|\.acquire\(|\.__enter__\(|lock\(|Lock\(\)\.acquire/i;
|
|
196
|
+
var JOIN_SRC = /\.join\(|\.result\(|\.wait\(|\.get\(|wait_for|as_completed|\.gather\(/;
|
|
197
|
+
var SLEEP_SRC = /\bsleep\(/;
|
|
198
|
+
var IO_SRC = /\.(recv|recv_into|recvfrom|read|readline|readinto|accept|connect|send|sendall|write)\(|requests\.|urlopen|httpx\.|\.execute\(|\.fetch(one|all|many)\(|\.commit\(|select\.select|\.poll\(/;
|
|
199
|
+
function findLockWaits(dump, reader, cwd) {
|
|
200
|
+
const out = [];
|
|
201
|
+
const resolve = (f) => cwd && !f.startsWith("/") ? `${cwd.replace(/\/$/, "")}/${f}` : f;
|
|
202
|
+
for (const t of dump.threads) {
|
|
203
|
+
const top = t.frames[0];
|
|
204
|
+
if (!top) continue;
|
|
205
|
+
const user = t.frames.find((f) => !STDLIB_FILE.test(f.file) && !/<frozen|importlib|site-packages\/(werkzeug|flask|gunicorn|uvicorn|django)/.test(f.file)) ?? top;
|
|
206
|
+
const base = {
|
|
207
|
+
thread: t.name || t.id,
|
|
208
|
+
state: t.state,
|
|
209
|
+
waiting_in: `${top.func} (${top.file.split("/").pop()}:${top.line})`,
|
|
210
|
+
at: `${user.func} (${user.file.split("/").pop()}:${user.line})`,
|
|
211
|
+
lock_repr: Object.entries(top.locals ?? {}).find(([k]) => /lock|cond|mutex|sem/i.test(k))?.[1]
|
|
212
|
+
};
|
|
213
|
+
const inStdlib = STDLIB_FILE.test(top.file);
|
|
214
|
+
let kind;
|
|
215
|
+
if (inStdlib) {
|
|
216
|
+
if (/_wait_for_tstate_lock|^join$/.test(top.func)) kind = "join";
|
|
217
|
+
else if (/acquire|__enter__|^wait$|_acquire_restore|^get$|^put$/.test(top.func) && /threading|queue/.test(top.file)) kind = /^(get|put)$/.test(top.func) ? "join" : "lock";
|
|
218
|
+
else if (/sleep/.test(top.func)) kind = "sleep";
|
|
219
|
+
else if (/select|poll|recv|read|accept|connect|send|^wait$/.test(top.func)) kind = "io";
|
|
220
|
+
else if (/asyncio|run_forever|_run_once|serve_forever/.test(top.file + top.func)) kind = "idle_loop";
|
|
221
|
+
}
|
|
222
|
+
if (!kind && t.state !== "active") {
|
|
223
|
+
const src = sourceLine(resolve(top.file), top.line, reader);
|
|
224
|
+
if (src) {
|
|
225
|
+
if (LOCK_SRC.test(src)) kind = "lock";
|
|
226
|
+
else if (SLEEP_SRC.test(src)) kind = "sleep";
|
|
227
|
+
else if (JOIN_SRC.test(src)) kind = "join";
|
|
228
|
+
else if (IO_SRC.test(src)) kind = "io";
|
|
229
|
+
else kind = "native";
|
|
230
|
+
out.push({ ...base, kind, source: src });
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (!inStdlib) kind = "native";
|
|
234
|
+
}
|
|
235
|
+
if (kind) out.push({ ...base, kind });
|
|
236
|
+
}
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
async function targetCwd(pid) {
|
|
240
|
+
if (platform() === "linux") {
|
|
241
|
+
try {
|
|
242
|
+
const { readlink } = await import("fs/promises");
|
|
243
|
+
return await readlink(`/proc/${pid}/cwd`);
|
|
244
|
+
} catch {
|
|
245
|
+
return void 0;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const r = await runCapped("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], { timeoutMs: 5e3 });
|
|
249
|
+
return r.stdout.split("\n").find((l) => l.startsWith("n"))?.slice(1);
|
|
250
|
+
}
|
|
251
|
+
function sameLockGroups(waits) {
|
|
252
|
+
const by = /* @__PURE__ */ new Map();
|
|
253
|
+
for (const w of waits) {
|
|
254
|
+
if (!w.lock_repr || w.kind !== "lock") continue;
|
|
255
|
+
const k = w.lock_repr.replace(/\s+/g, " ");
|
|
256
|
+
by.set(k, [...by.get(k) ?? [], w.thread]);
|
|
257
|
+
}
|
|
258
|
+
return [...by].filter(([, t]) => t.length > 1).map(([lock, threads]) => ({ lock, threads }));
|
|
259
|
+
}
|
|
260
|
+
function needsSudoNote(b, stderr, cmd) {
|
|
261
|
+
if (looksLikePermissionError(stderr) || /Operation not permitted|ptrace|task_for_pid|Permission denied/i.test(stderr)) {
|
|
262
|
+
b.limitation(rootRequiredLimitation("py-spy", cmd));
|
|
263
|
+
if (platform() === "darwin") b.limitation("macOS: py-spy needs sudo (or the target must be a child of this process). SIP blocks task_for_pid otherwise.");
|
|
264
|
+
if (platform() === "linux") b.limitation("Linux: set kernel.yama.ptrace_scope=0 or run with CAP_SYS_PTRACE; inside Docker add --cap-add SYS_PTRACE");
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
async function summarizePySpyDump(b, dump, art, pid) {
|
|
270
|
+
const active = dump.threads.filter((t) => t.state === "active");
|
|
271
|
+
const summaryEv = b.evidenceOf(
|
|
272
|
+
"stack_summary",
|
|
273
|
+
`${dump.threads.length} thread(s): ${active.length} active, ${dump.threads.length - active.length} idle; python ${dump.python ?? "?"}`,
|
|
274
|
+
dump.threads.map((t) => ({ thread: t.name || t.id, state: t.state, top: t.frames[0] ? `${t.frames[0].func} (${t.frames[0].file.split("/").pop()}:${t.frames[0].line})` : "?", depth: t.frames.length })),
|
|
275
|
+
{ artifact: art }
|
|
276
|
+
);
|
|
277
|
+
for (const t of dump.threads.slice(0, 12)) {
|
|
278
|
+
b.evidenceOf(
|
|
279
|
+
"stack",
|
|
280
|
+
`${t.name || t.id} [${t.state}]: ${t.frames.slice(0, 4).map((f) => `${f.func}@${f.file.split("/").pop()}:${f.line}`).join(" \u2190 ")}`,
|
|
281
|
+
{ thread: t.name || t.id, state: t.state, frames: t.frames.slice(0, 15).map((f) => ({ func: f.func, file: f.file, line: f.line, ...f.locals ? { locals: f.locals } : {} })) },
|
|
282
|
+
{ artifact: art }
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
const cwd = pid ? await targetCwd(pid) : void 0;
|
|
286
|
+
const waits = findLockWaits(dump, void 0, cwd);
|
|
287
|
+
const locks = waits.filter((w) => w.kind === "lock");
|
|
288
|
+
const joins = waits.filter((w) => w.kind === "join");
|
|
289
|
+
const io = waits.filter((w) => w.kind === "io");
|
|
290
|
+
const sleeps = waits.filter((w) => w.kind === "sleep");
|
|
291
|
+
const natives = waits.filter((w) => w.kind === "native");
|
|
292
|
+
const loops = waits.filter((w) => w.kind === "idle_loop");
|
|
293
|
+
const fmt = (w) => `${w.thread} at ${w.at}${w.source ? ` [${w.source}]` : ""}`;
|
|
294
|
+
if (waits.length) {
|
|
295
|
+
const waitEv = b.evidenceOf(
|
|
296
|
+
"lock_wait",
|
|
297
|
+
`${waits.length} blocked thread(s): ${locks.length} on locks, ${joins.length} joining/waiting, ${io.length} in I/O, ${sleeps.length} sleeping, ${natives.length} in other native calls, ${loops.length} idle event loops`,
|
|
298
|
+
waits.map((w) => ({ thread: w.thread, kind: w.kind, at: w.at, waiting_in: w.waiting_in, source: w.source, lock_repr: w.lock_repr })),
|
|
299
|
+
{ artifact: art }
|
|
300
|
+
);
|
|
301
|
+
const groups = sameLockGroups(waits);
|
|
302
|
+
if (groups.length) b.finding("high", `${groups[0].threads.length} threads wait on the same lock ${groups[0].lock}: ${groups[0].threads.join(", ")}`, [waitEv], "observed");
|
|
303
|
+
if (locks.length >= 2 && active.length === 0) {
|
|
304
|
+
b.finding(
|
|
305
|
+
"high",
|
|
306
|
+
`likely deadlock: ${locks.length} thread(s) blocked acquiring locks and no thread is running \u2014 ${locks.slice(0, 6).map(fmt).join("; ")}`,
|
|
307
|
+
[waitEv, summaryEv],
|
|
308
|
+
"inferred",
|
|
309
|
+
`${joins.length ? `${joins.length} thread(s) are joined on them (${joins.slice(0, 3).map((w) => w.thread).join(", ")}). ` : ""}Re-run dump_stacks: identical positions confirm a deadlock. Handoff: attach a step debugger (e.g. mcp-debugger) at ${locks[0].at} to inspect lock owners.`
|
|
310
|
+
);
|
|
311
|
+
} else if (locks.length) {
|
|
312
|
+
b.finding("warn", `${locks.length} thread(s) blocked acquiring a lock: ${locks.slice(0, 6).map(fmt).join("; ")}`, [waitEv], "observed");
|
|
313
|
+
}
|
|
314
|
+
if (joins.length >= 3 && !locks.length) b.finding("warn", `${joins.length} thread(s) waiting on other threads/queues: ${joins.slice(0, 4).map(fmt).join("; ")}`, [waitEv], "observed");
|
|
315
|
+
if (io.length) b.finding(io.length >= 3 ? "warn" : "info", `${io.length} thread(s) blocked in I/O: ${io.slice(0, 4).map(fmt).join("; ")}`, [waitEv], "observed", "inspect_sockets shows which remote they talk to.");
|
|
316
|
+
if (sleeps.length) b.finding("info", `${sleeps.length} thread(s) in sleep(): ${sleeps.slice(0, 3).map(fmt).join("; ")}`, [waitEv], "observed");
|
|
317
|
+
if (natives.length) b.finding("info", `${natives.length} thread(s) blocked in native calls from user code: ${natives.slice(0, 3).map(fmt).join("; ")}`, [waitEv], "observed");
|
|
318
|
+
}
|
|
319
|
+
if (b.findingCount === 0 && active.length === 0) b.finding("info", "all threads idle \u2014 the process is not stuck in Python code; if the caller hangs, look at the network path (inspect_sockets)", [summaryEv], "inferred");
|
|
320
|
+
if (active.length) {
|
|
321
|
+
const t = active[0];
|
|
322
|
+
const f = t.frames[0];
|
|
323
|
+
if (f) b.finding("info", `active thread ${t.name || t.id} is executing ${f.func} (${f.file.split("/").pop()}:${f.line})`, [summaryEv]);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
async function dumpStacks(ctx, withLocals, tool = "dump_stacks") {
|
|
327
|
+
const { session, target } = ctx;
|
|
328
|
+
const b = new ResultBuilder(tool, session, target);
|
|
329
|
+
if (!target.pid) return b.fail("no pid").build();
|
|
330
|
+
const args = ["dump", "--pid", String(target.pid), "--nonblocking"];
|
|
331
|
+
if (withLocals) args.push("--locals");
|
|
332
|
+
const cmd = `py-spy ${args.join(" ")}`;
|
|
333
|
+
const r = await runCapped("py-spy", args, { timeoutMs: 3e4 });
|
|
334
|
+
const raw = r.stdout + (r.stderr ? `
|
|
335
|
+
--- stderr ---
|
|
336
|
+
${r.stderr}` : "");
|
|
337
|
+
const art = await b.writeArtifact("pyspy-dump.txt", raw, "raw_text", cmd);
|
|
338
|
+
if (r.exitCode !== 0) {
|
|
339
|
+
if (!needsSudoNote(b, r.stderr, cmd)) b.limitation(`py-spy exited ${r.exitCode}: ${r.stderr.trim().slice(0, 300)}`);
|
|
340
|
+
b.next(`import_artifact (pyspy_dump, after running the sudo command above)`, "inspect_sockets", "inspect_files", "snapshot_memory");
|
|
341
|
+
return b.fail("py-spy dump failed").build();
|
|
342
|
+
}
|
|
343
|
+
await summarizePySpyDump(b, parsePySpyDump(r.stdout), art, target.pid);
|
|
344
|
+
b.limitation("instant snapshot (nonblocking); take two dumps to distinguish a hang from a slow step");
|
|
345
|
+
b.next("inspect_sockets", "trace_syscalls", "profile_cpu");
|
|
346
|
+
return b.build();
|
|
347
|
+
}
|
|
348
|
+
async function profileCpu(ctx, args) {
|
|
349
|
+
const { session, target } = ctx;
|
|
350
|
+
const b = new ResultBuilder("profile_cpu", session, target);
|
|
351
|
+
if (!target.pid) return b.fail("no pid").build();
|
|
352
|
+
const duration = clampDuration(args.duration_s, 15);
|
|
353
|
+
const rawPath = b.artifactPath("pyspy-folded.txt");
|
|
354
|
+
const cmdArgs = ["record", "--pid", String(target.pid), "--duration", String(duration), "--format", "raw", "--output", rawPath, "--nonblocking", "--subprocesses"];
|
|
355
|
+
const r = await runCapped("py-spy", cmdArgs, { timeoutMs: duration * 1e3 + LIMITS.DURATION_SLACK_MS + 5e3 });
|
|
356
|
+
if (r.exitCode !== 0) {
|
|
357
|
+
await b.writeArtifact("pyspy-stderr.txt", r.stderr, "raw_text");
|
|
358
|
+
if (!needsSudoNote(b, r.stderr, `py-spy ${cmdArgs.join(" ")}`)) b.limitation(`py-spy exited ${r.exitCode}: ${r.stderr.trim().slice(0, 300)}`);
|
|
359
|
+
b.next("import_artifact (folded, after running the sudo command above)", "inspect_sockets", "snapshot_memory");
|
|
360
|
+
return b.fail("py-spy record failed").build();
|
|
361
|
+
}
|
|
362
|
+
const { readFile: readFile4 } = await import("fs/promises");
|
|
363
|
+
const folded = await readFile4(rawPath, "utf8").catch(() => "");
|
|
364
|
+
await b.addArtifact(rawPath, "folded_stacks", { description: `py-spy record ${duration}s, folded stacks` });
|
|
365
|
+
return summarizeProfile(b, folded, duration, "py-spy", 100);
|
|
366
|
+
}
|
|
367
|
+
async function summarizeProfile(b, folded, duration, profiler, sampleHz) {
|
|
368
|
+
const stacks = parseFolded(folded);
|
|
369
|
+
if (!stacks.length) {
|
|
370
|
+
b.limitation(`${profiler} produced no samples in ${duration}s (process idle, or sampling blocked)`);
|
|
371
|
+
const ev = b.evidenceOf("metric", "0 samples", { name: "samples", value: 0 });
|
|
372
|
+
b.finding("info", `no CPU samples in ${duration}s \u2014 the process is idle or sleeping, not CPU-bound`, [ev], "inferred");
|
|
373
|
+
b.next("dump_stacks", "trace_syscalls", "inspect_sockets");
|
|
374
|
+
return b.build();
|
|
375
|
+
}
|
|
376
|
+
const { frames, totalSamples } = aggregateFrames(stacks, 15);
|
|
377
|
+
const workStacks = stacks.filter((s) => !IDLE_FRAME.test(s.frames[s.frames.length - 1]));
|
|
378
|
+
const workSamples = workStacks.reduce((n, s) => n + s.count, 0);
|
|
379
|
+
const idlePct = Math.round(100 * (totalSamples - workSamples) / totalSamples);
|
|
380
|
+
const { frames: workFrames } = aggregateFrames(workStacks, 15);
|
|
381
|
+
const hot = hottestStacks(workStacks.length ? workStacks : stacks, 5);
|
|
382
|
+
const svg = renderFlamegraph(stacks, { title: `${profiler} ${duration}s pid ${b.target.pid}` });
|
|
383
|
+
const svgPath = await b.writeArtifact("flamegraph.svg", svg, "flamegraph_svg", "flame graph (all threads, idle included)");
|
|
384
|
+
const busyWall = sampleHz ? Math.min(100, Math.round(100 * workSamples / (sampleHz * duration))) : void 0;
|
|
385
|
+
const framesEv = b.evidenceOf(
|
|
386
|
+
"frame_hot",
|
|
387
|
+
workFrames.length ? `top self among ${workSamples} work samples: ${workFrames.slice(0, 5).map((f) => `${shortFrame(f.frame)} ${f.self_pct}%`).join(", ")}` : `no work frames; all ${totalSamples} samples idle`,
|
|
388
|
+
(workFrames.length ? workFrames : frames).map((f) => ({ frame: f.frame, self: f.self, total: f.total, self_pct: f.self_pct, total_pct: f.total_pct })),
|
|
389
|
+
{ artifact: svgPath }
|
|
390
|
+
);
|
|
391
|
+
const stacksEv = b.evidenceOf("stack", `hottest work stack (${hot[0]?.pct}% of work): ${hot[0]?.stack.slice(-4).map(shortFrame).join(" \u2190 ")}`, hot, { artifact: svgPath });
|
|
392
|
+
const metricEv = b.evidenceOf(
|
|
393
|
+
"metric",
|
|
394
|
+
`${totalSamples} samples over ${duration}s: ${workSamples} work, ${idlePct}% idle/wait${busyWall !== void 0 ? `; \u2248${busyWall}% of wall time CPU-busy (${sampleHz} Hz)` : ""}`,
|
|
395
|
+
{ name: "samples", value: totalSamples, work_samples: workSamples, idle_pct: idlePct, busy_wall_pct: busyWall, duration_s: duration, sample_hz: sampleHz }
|
|
396
|
+
);
|
|
397
|
+
const topWork = workFrames[0];
|
|
398
|
+
if (topWork && topWork.self_pct >= 20) {
|
|
399
|
+
b.finding(busyWall === void 0 || busyWall >= 25 ? "high" : "warn", `${shortFrame(topWork.frame)} accounts for ${topWork.self_pct}% of CPU work (${topWork.total_pct}% inclusive)${busyWall !== void 0 ? `; the process was CPU-busy \u2248${busyWall}% of the time` : ""}`, [framesEv, stacksEv, metricEv], "observed", `Hot path: ${hot[0]?.stack.slice(-6).map(shortFrame).join(" \u2190 ")}. Handoff: attach a step debugger (e.g. mcp-debugger) at ${topWork.frame}.`);
|
|
400
|
+
} else if (topWork && topWork.self_pct >= 8) {
|
|
401
|
+
b.finding("warn", `${shortFrame(topWork.frame)} is the top CPU frame at ${topWork.self_pct}% of work`, [framesEv, stacksEv], "observed");
|
|
402
|
+
}
|
|
403
|
+
if (busyWall !== void 0 && busyWall < 15) {
|
|
404
|
+
b.finding("info", `process CPU-busy only \u2248${busyWall}% of wall time \u2014 mostly waiting, not CPU-bound`, [metricEv], "inferred", "Look at trace_syscalls (what is it waiting on?) and inspect_sockets (whom?).");
|
|
405
|
+
} else if (busyWall === void 0 && idlePct >= 90 && !topWork) {
|
|
406
|
+
b.finding("info", `~${idlePct}% of samples are in idle/wait frames \u2014 the process is mostly waiting, not CPU-bound`, [metricEv], "inferred");
|
|
407
|
+
}
|
|
408
|
+
if (b.findingCount === 0) b.finding("info", `CPU spread thin: top work frame ${shortFrame((workFrames[0] ?? frames[0]).frame)} at ${(workFrames[0] ?? frames[0]).self_pct}%`, [framesEv]);
|
|
409
|
+
b.limitation(`sampled ${duration}s only (${profiler}${sampleHz ? ` @ ${sampleHz} Hz` : ""}); idle frames across all threads are excluded from the hot-frame ranking`);
|
|
410
|
+
b.next("trace_syscalls", "dump_stacks", "inspect_sockets");
|
|
411
|
+
if (topWork && topWork.self_pct >= 20 && (profiler === "perf" || profiler === "sample")) {
|
|
412
|
+
const dso = /\(([^)]+)\)\s*$/.exec(topWork.frame)?.[1];
|
|
413
|
+
b.next(`static analysis: decompile ${shortFrame(topWork.frame).replace(/\s*\(.*$/, "")}${dso ? ` in ${dso}` : ""} to see what the hot code does`);
|
|
414
|
+
}
|
|
415
|
+
return b.build();
|
|
416
|
+
}
|
|
417
|
+
function shortFrame(f) {
|
|
418
|
+
return f.replace(/\(([^)]*\/)?([^/)]+)\)/, "($2)").replace(/\+0x[0-9a-f]+/, "").slice(0, 80);
|
|
419
|
+
}
|
|
420
|
+
var pyspyAdapter = {
|
|
421
|
+
name: "py-spy",
|
|
422
|
+
tools: ["profile_cpu", "dump_stacks", "snapshot_memory"],
|
|
423
|
+
platforms: ["linux", "darwin"],
|
|
424
|
+
requires: ["py-spy"],
|
|
425
|
+
runtimes: ["python"],
|
|
426
|
+
install: { linux: "pip install py-spy (or: cargo install py-spy)", darwin: "pip install py-spy (or: brew install py-spy)" },
|
|
427
|
+
priority: 20,
|
|
428
|
+
check(base, env) {
|
|
429
|
+
if (env.platform === "darwin" && !env.isRoot) base.notes.push("macOS: attaching to processes you did not spawn requires sudo");
|
|
430
|
+
if (env.platform === "linux" && env.ptraceScope !== null && env.ptraceScope >= 1 && !env.isRoot) base.notes.push(`ptrace_scope=${env.ptraceScope}: can only attach to child processes without root`);
|
|
431
|
+
return base;
|
|
432
|
+
},
|
|
433
|
+
async run(tool, args, ctx) {
|
|
434
|
+
if (tool === "dump_stacks") return dumpStacks(ctx, false);
|
|
435
|
+
if (tool === "profile_cpu") return profileCpu(ctx, args);
|
|
436
|
+
if (tool === "snapshot_memory") {
|
|
437
|
+
const { snapshotMemoryGeneric } = await import("./psmem-UB3VCMG3.js");
|
|
438
|
+
const base = await snapshotMemoryGeneric(ctx, args);
|
|
439
|
+
const dump = await dumpStacks(ctx, true, "snapshot_memory");
|
|
440
|
+
return mergeResults(base, dump, ["instant stack+locals via py-spy; Python has no retained-size heap view without tracemalloc \u2014 enable tracemalloc in the app for allocation-site attribution"]);
|
|
441
|
+
}
|
|
442
|
+
throw new Error(`py-spy adapter cannot run ${tool}`);
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
function mergeResults(base, extra, limitations = []) {
|
|
446
|
+
const offsetE = base.evidence.length;
|
|
447
|
+
const offsetF = base.findings.length;
|
|
448
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
449
|
+
const evidence = [...base.evidence];
|
|
450
|
+
for (const e of extra.evidence) {
|
|
451
|
+
const id = `e${evidence.length + 1}`;
|
|
452
|
+
idMap.set(e.id, id);
|
|
453
|
+
evidence.push({ ...e, id });
|
|
454
|
+
}
|
|
455
|
+
const findings = [...base.findings];
|
|
456
|
+
for (const f of extra.findings) {
|
|
457
|
+
findings.push({ ...f, id: `f${findings.length + 1}`, evidence_ids: f.evidence_ids.map((x) => idMap.get(x) ?? x) });
|
|
458
|
+
}
|
|
459
|
+
void offsetE;
|
|
460
|
+
void offsetF;
|
|
461
|
+
return {
|
|
462
|
+
...base,
|
|
463
|
+
ok: base.ok || extra.ok,
|
|
464
|
+
findings,
|
|
465
|
+
evidence,
|
|
466
|
+
limitations: [.../* @__PURE__ */ new Set([...base.limitations, ...extra.limitations, ...limitations])],
|
|
467
|
+
suggested_next: [.../* @__PURE__ */ new Set([...base.suggested_next, ...extra.suggested_next])],
|
|
468
|
+
artifacts: [...base.artifacts, ...extra.artifacts],
|
|
469
|
+
duration_ms: base.duration_ms + extra.duration_ms,
|
|
470
|
+
error: base.ok || extra.ok ? void 0 : base.error ?? extra.error
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/adapters/strace.ts
|
|
475
|
+
import { readFile } from "fs/promises";
|
|
476
|
+
var CATEGORY_SETS = {
|
|
477
|
+
network: "%network",
|
|
478
|
+
file: "%file,%desc",
|
|
479
|
+
process: "%process",
|
|
480
|
+
signal: "%signal",
|
|
481
|
+
memory: "%memory",
|
|
482
|
+
all: "all"
|
|
483
|
+
};
|
|
484
|
+
var LINE_RE = /^(?:\[pid\s+(\d+)\]\s+|(\d+)\s+)?(?:(\d{2}:\d{2}:\d{2}\.\d+)\s+)?(\w+)\((.*?)\)\s+=\s+(.+?)(?:\s+<([\d.]+)>)?$/;
|
|
485
|
+
var UNFINISHED_RE = /^(?:\[pid\s+(\d+)\]\s+|(\d+)\s+)?(?:(\d{2}:\d{2}:\d{2}\.\d+)\s+)?(\w+)\((.*)<unfinished \.\.\.>$/;
|
|
486
|
+
var RESUMED_RE = /^(?:\[pid\s+(\d+)\]\s+|(\d+)\s+)?(?:(\d{2}:\d{2}:\d{2}\.\d+)\s+)?<\.\.\. (\w+) resumed>\s*(.*?)\)?\s+=\s+(.+?)(?:\s+<([\d.]+)>)?$/;
|
|
487
|
+
function parseStrace(raw) {
|
|
488
|
+
const out = [];
|
|
489
|
+
const lines = raw.split("\n");
|
|
490
|
+
for (let i = 0; i < lines.length; i++) {
|
|
491
|
+
const line = lines[i].trimEnd();
|
|
492
|
+
if (!line || line.startsWith("strace:") || /\+\+\+ (exited|killed)|--- SIG/.test(line)) continue;
|
|
493
|
+
let m = LINE_RE.exec(line);
|
|
494
|
+
if (m) {
|
|
495
|
+
const result = m[6];
|
|
496
|
+
const errno = /^-1 ([A-Z]+)/.exec(result)?.[1];
|
|
497
|
+
out.push({ pid: m[1] ? Number(m[1]) : m[2] ? Number(m[2]) : void 0, time: m[3], name: m[4], args: m[5], result, errno, duration_s: m[7] ? Number(m[7]) : void 0, line: i + 1 });
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
m = UNFINISHED_RE.exec(line);
|
|
501
|
+
if (m) {
|
|
502
|
+
out.push({ pid: m[1] ? Number(m[1]) : m[2] ? Number(m[2]) : void 0, time: m[3], name: m[4], args: m[5], result: "?", unfinished: true, line: i + 1 });
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
m = RESUMED_RE.exec(line);
|
|
506
|
+
if (m) {
|
|
507
|
+
const result = m[6];
|
|
508
|
+
const errno = /^-1 ([A-Z]+)/.exec(result)?.[1];
|
|
509
|
+
const pid = m[1] ? Number(m[1]) : m[2] ? Number(m[2]) : void 0;
|
|
510
|
+
const open = [...out].reverse().find((r) => r.unfinished && r.name === m[4] && r.pid === pid && r.result === "?");
|
|
511
|
+
if (open) {
|
|
512
|
+
open.result = result;
|
|
513
|
+
open.errno = errno;
|
|
514
|
+
open.duration_s = m[7] ? Number(m[7]) : void 0;
|
|
515
|
+
open.args += m[5] ?? "";
|
|
516
|
+
open.resumed = true;
|
|
517
|
+
} else out.push({ pid, time: m[3], name: m[4], args: m[5] ?? "", result, errno, duration_s: m[7] ? Number(m[7]) : void 0, resumed: true, line: i + 1 });
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return out;
|
|
521
|
+
}
|
|
522
|
+
var WAIT_SYSCALLS = /* @__PURE__ */ new Set(["futex", "epoll_wait", "epoll_pwait", "epoll_pwait2", "poll", "ppoll", "select", "pselect6", "nanosleep", "clock_nanosleep", "wait4", "waitid", "read", "recvfrom", "recvmsg", "recv", "accept", "accept4", "connect", "flock", "fcntl", "io_uring_enter", "rt_sigtimedwait", "pause", "sched_yield"]);
|
|
523
|
+
function aggregateSyscalls(records) {
|
|
524
|
+
const by = /* @__PURE__ */ new Map();
|
|
525
|
+
const errBy = /* @__PURE__ */ new Map();
|
|
526
|
+
let totalMs = 0;
|
|
527
|
+
let waitMs = 0;
|
|
528
|
+
for (const r of records) {
|
|
529
|
+
const s = by.get(r.name) ?? { syscall: r.name, count: 0, total_ms: 0, avg_ms: 0, max_ms: 0, errors: 0 };
|
|
530
|
+
s.count++;
|
|
531
|
+
const ms = (r.duration_s ?? 0) * 1e3;
|
|
532
|
+
s.total_ms += ms;
|
|
533
|
+
s.max_ms = Math.max(s.max_ms, ms);
|
|
534
|
+
if (r.errno) {
|
|
535
|
+
s.errors++;
|
|
536
|
+
const k = `${r.errno}:${r.name}`;
|
|
537
|
+
const e = errBy.get(k) ?? { errno: r.errno, syscall: r.name, count: 0, example: r.args.slice(0, 120) };
|
|
538
|
+
e.count++;
|
|
539
|
+
errBy.set(k, e);
|
|
540
|
+
}
|
|
541
|
+
by.set(r.name, s);
|
|
542
|
+
totalMs += ms;
|
|
543
|
+
if (WAIT_SYSCALLS.has(r.name)) waitMs += ms;
|
|
544
|
+
}
|
|
545
|
+
const stats = [...by.values()].map((s) => ({ ...s, total_ms: round(s.total_ms), avg_ms: round(s.count ? s.total_ms / s.count : 0), max_ms: round(s.max_ms) })).sort((a, b) => b.total_ms - a.total_ms);
|
|
546
|
+
const errors = [...errBy.values()].sort((a, b) => b.count - a.count);
|
|
547
|
+
const slowest = [...records].filter((r) => r.duration_s !== void 0).sort((a, b) => b.duration_s - a.duration_s).slice(0, 10);
|
|
548
|
+
return { stats, errors, slowest, totalMs: round(totalMs), waitMs: round(waitMs) };
|
|
549
|
+
}
|
|
550
|
+
function round(n) {
|
|
551
|
+
return Math.round(n * 1e3) / 1e3;
|
|
552
|
+
}
|
|
553
|
+
function describeArgs(r) {
|
|
554
|
+
const path = /"([^"]{1,120})"/.exec(r.args)?.[1];
|
|
555
|
+
const addr = /sin_addr=inet_addr\("([^"]+)"\)|sin6_addr=inet_pton\([^,]+,\s*"([^"]+)"|sun_path="([^"]+)"/.exec(r.args);
|
|
556
|
+
const port = /sin6?_port=htons\((\d+)\)/.exec(r.args)?.[1];
|
|
557
|
+
const parts = [];
|
|
558
|
+
if (path && !addr) parts.push(path);
|
|
559
|
+
if (addr) parts.push(`${addr[1] ?? addr[2] ?? addr[3]}${port ? `:${port}` : ""}`);
|
|
560
|
+
if (!parts.length) parts.push(r.args.slice(0, 60));
|
|
561
|
+
return parts.join(" ");
|
|
562
|
+
}
|
|
563
|
+
function summarizeSyscallRecords(b, records, duration, artifactPath) {
|
|
564
|
+
const agg = aggregateSyscalls(records);
|
|
565
|
+
const topEv = b.evidenceOf(
|
|
566
|
+
"syscall_stat",
|
|
567
|
+
`${records.length} syscalls, ${agg.totalMs} ms inside the kernel over ${duration}s; top by time: ${agg.stats.slice(0, 5).map((s) => `${s.syscall} ${s.total_ms}ms\xD7${s.count}`).join(", ")}`,
|
|
568
|
+
agg.stats.slice(0, 20),
|
|
569
|
+
{ artifact: artifactPath }
|
|
570
|
+
);
|
|
571
|
+
const slow = agg.slowest.slice(0, 8).map((rec) => ({ syscall: rec.name, ms: round((rec.duration_s ?? 0) * 1e3), pid: rec.pid, at: rec.time, target: describeArgs(rec), result: rec.result.slice(0, 60), line: rec.line }));
|
|
572
|
+
const slowEv = b.evidenceOf("syscall_slow", slow.length ? `slowest: ${slow.slice(0, 3).map((s) => `${s.syscall}(${s.target}) ${s.ms}ms`).join("; ")}` : "no timed syscalls", slow, { artifact: artifactPath, lines: slow[0] ? [slow[0].line, slow[0].line] : void 0 });
|
|
573
|
+
const errEv = b.evidenceOf("syscall_error", agg.errors.length ? `errors: ${agg.errors.slice(0, 5).map((e) => `${e.syscall}\u2192${e.errno}\xD7${e.count}`).join(", ")}` : "no failing syscalls", agg.errors.slice(0, 15), { artifact: artifactPath });
|
|
574
|
+
const waitPct = agg.totalMs ? Math.round(100 * agg.waitMs / agg.totalMs) : 0;
|
|
575
|
+
const waitEv = b.evidenceOf("metric", `${waitPct}% of kernel time in blocking waits (futex/poll/epoll/read/recv/nanosleep)`, { name: "wait_pct", value: waitPct, wait_ms: agg.waitMs, total_ms: agg.totalMs });
|
|
576
|
+
const top = agg.stats[0];
|
|
577
|
+
if (top && agg.totalMs > 0 && top.total_ms / agg.totalMs > 0.5) {
|
|
578
|
+
const isWait = WAIT_SYSCALLS.has(top.syscall);
|
|
579
|
+
b.finding(isWait ? "info" : "warn", `${top.syscall} dominates: ${top.total_ms} ms over ${top.count} calls (${Math.round(100 * top.total_ms / agg.totalMs)}% of kernel time)${isWait ? " \u2014 blocking wait, the process is waiting on something" : ""}`, [topEv, slowEv], "observed");
|
|
580
|
+
}
|
|
581
|
+
const nanosleep = agg.stats.find((s) => /nanosleep/.test(s.syscall));
|
|
582
|
+
if (nanosleep && nanosleep.total_ms > duration * 100) b.finding("warn", `${nanosleep.total_ms} ms in ${nanosleep.syscall} (${nanosleep.count} calls) \u2014 deliberate sleeps/timers on the request path`, [topEv], "inferred");
|
|
583
|
+
const fileChurn = agg.stats.filter((s) => /^(openat|open|stat|newfstatat|statx|lstat|access|readlink)$/.test(s.syscall)).reduce((n, s) => n + s.count, 0);
|
|
584
|
+
if (fileChurn > duration * 200) b.finding("warn", `${fileChurn} path lookups (open/stat) in ${duration}s \u2014 filesystem churn (missing cache? module resolution per request?)`, [topEv], "inferred");
|
|
585
|
+
const reads = agg.stats.find((s) => s.syscall === "read");
|
|
586
|
+
if (reads && reads.count > duration * 300) b.finding("warn", `${reads.count} read() calls in ${duration}s (avg ${reads.avg_ms} ms) \u2014 many small reads; sync I/O in a loop?`, [topEv], "inferred");
|
|
587
|
+
for (const e of agg.errors.slice(0, 4)) {
|
|
588
|
+
const sev = /ECONNREFUSED|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|EMFILE|ENOSPC|ENOMEM/.test(e.errno) ? "high" : e.count > 50 ? "warn" : "info";
|
|
589
|
+
const hint = e.errno === "EAGAIN" ? " (non-blocking I/O would-block; normal for event loops unless excessive)" : e.errno === "ENOENT" ? ` (missing file; e.g. ${e.example?.split(",")[0]})` : e.errno === "ECONNREFUSED" ? " \u2014 a downstream service is not accepting connections" : "";
|
|
590
|
+
b.finding(sev, `${e.count}\xD7 ${e.syscall} failed with ${e.errno}${hint}`, [errEv], "observed");
|
|
591
|
+
}
|
|
592
|
+
if (slow[0] && slow[0].ms > 500) b.finding("warn", `slowest single call: ${slow[0].syscall}(${slow[0].target}) took ${slow[0].ms} ms`, [slowEv], "observed");
|
|
593
|
+
if (b.findingCount === 0) b.finding("info", `${records.length} syscalls in ${duration}s, nothing dominant; top ${top?.syscall ?? "?"}`, [topEv, waitEv]);
|
|
594
|
+
}
|
|
595
|
+
async function traceSyscalls(ctx, args) {
|
|
596
|
+
const { session, target } = ctx;
|
|
597
|
+
const b = new ResultBuilder("trace_syscalls", session, target);
|
|
598
|
+
if (!target.pid) return b.fail("no pid").build();
|
|
599
|
+
const duration = clampDuration(args.duration_s, 10);
|
|
600
|
+
const cats = args.categories?.length ? args.categories : ["all"];
|
|
601
|
+
const set = cats.includes("all") ? "all" : [...new Set(cats.map((c) => CATEGORY_SETS[c]))].join(",");
|
|
602
|
+
const out = b.artifactPath("strace.log");
|
|
603
|
+
const straceArgs = ["-f", "-T", "-tt", "-qq", "-s", "64", "-e", `trace=${set}`, "-p", String(target.pid), "-o", out];
|
|
604
|
+
const r = await runCapped("strace", straceArgs, { timeoutMs: duration * 1e3, captureInMemory: true });
|
|
605
|
+
const raw = await readFile(out, "utf8").catch(() => "");
|
|
606
|
+
const art = await b.addArtifact(out, "raw_text", { description: `strace ${straceArgs.join(" ")} for ${duration}s`, truncated: r.outputCapped });
|
|
607
|
+
if (!raw.trim()) {
|
|
608
|
+
const cmd = `strace ${straceArgs.join(" ")}`;
|
|
609
|
+
if (looksLikePermissionError(r.stderr)) {
|
|
610
|
+
b.limitation(rootRequiredLimitation("trace_syscalls", cmd));
|
|
611
|
+
b.limitation("Linux: kernel.yama.ptrace_scope \u2265 1 blocks attaching to non-child processes; sudo sysctl kernel.yama.ptrace_scope=0 for this boot, or run Debug Anything as root / with CAP_SYS_PTRACE");
|
|
612
|
+
b.next("import_artifact (strace, after running the sudo command above)", "inspect_sockets", "inspect_files", "snapshot_memory");
|
|
613
|
+
return b.fail("strace could not attach (permission denied)").build();
|
|
614
|
+
}
|
|
615
|
+
if (r.stderr.trim()) b.limitation(`strace: ${r.stderr.trim().slice(0, 300)}`);
|
|
616
|
+
const ev = b.evidenceOf("syscall_stat", `0 syscalls in ${duration}s`, { name: "syscalls", count: 0 });
|
|
617
|
+
b.finding("info", `no syscalls in ${duration}s \u2014 the process made no kernel calls (spinning in user space, or fully idle in a wait that started before the trace)`, [ev], "inferred");
|
|
618
|
+
b.next("dump_stacks", "profile_cpu");
|
|
619
|
+
return b.build();
|
|
620
|
+
}
|
|
621
|
+
summarizeSyscallRecords(b, parseStrace(raw), duration, art.path);
|
|
622
|
+
b.limitation(`traced ${duration}s only (${set}); strace adds overhead to every syscall \u2014 latencies are inflated`);
|
|
623
|
+
if (r.outputCapped) b.limitation(`raw log hit the ${LIMITS.MAX_RAW_BYTES / 1024 / 1024} MB cap and was cut short`);
|
|
624
|
+
b.next("profile_cpu", "inspect_sockets", "inspect_files");
|
|
625
|
+
return b.build();
|
|
626
|
+
}
|
|
627
|
+
var straceAdapter = {
|
|
628
|
+
name: "strace",
|
|
629
|
+
tools: ["trace_syscalls"],
|
|
630
|
+
platforms: ["linux"],
|
|
631
|
+
requires: ["strace"],
|
|
632
|
+
install: { linux: "sudo apt install strace | sudo dnf install strace | apk add strace" },
|
|
633
|
+
priority: 10,
|
|
634
|
+
check(base, env) {
|
|
635
|
+
if (env.ptraceScope !== null && env.ptraceScope >= 1 && !env.isRoot) base.notes.push(`ptrace_scope=${env.ptraceScope}: can only trace child processes without root`);
|
|
636
|
+
return base;
|
|
637
|
+
},
|
|
638
|
+
async run(tool, args, ctx) {
|
|
639
|
+
if (tool !== "trace_syscalls") throw new Error(`strace adapter cannot run ${tool}`);
|
|
640
|
+
return traceSyscalls(ctx, args);
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
// src/adapters/gdb.ts
|
|
645
|
+
function parseGdbBacktrace(raw) {
|
|
646
|
+
const threads = [];
|
|
647
|
+
let cur = null;
|
|
648
|
+
for (const line of raw.split("\n")) {
|
|
649
|
+
let m = /^Thread (\d+) \(Thread 0x[0-9a-f]+ \(LWP (\d+)\)(?: "([^"]*)")?\):?/.exec(line) ?? /^Thread (\d+) \((?:process )?(\d+)(?: "([^"]*)")?\):?/.exec(line);
|
|
650
|
+
if (m) {
|
|
651
|
+
cur = { index: Number(m[1]), tid: Number(m[2]), name: m[3], frames: [] };
|
|
652
|
+
threads.push(cur);
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
m = /^#(\d+)\s+(?:(0x[0-9a-f]+) in )?(\S+?)\s*\((?:[^)]*)\)(?: at (\S+))?(?: from (\S+))?/.exec(line);
|
|
656
|
+
if (m && cur) {
|
|
657
|
+
cur.frames.push({ n: Number(m[1]), func: m[3], loc: m[4] ?? m[5], addr: m[2] });
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return threads;
|
|
662
|
+
}
|
|
663
|
+
var LOCK_FUNCS = /^(__GI_)?(pthread_mutex_lock|__lll_lock_wait|__lll_lock_wait_private|pthread_rwlock_(rd|wr)lock|uv_mutex_lock|__lll_lock|_?_pthread_mutex_firstfit_lock_slow|__psynch_mutexwait|__ulock_wait)$|absl::Mutex::Lock|std::mutex::lock|SpinLock|ObjectMonitor::enter|futex_wait_queue|__futex_abstimed_wait_(common|cancelable)/;
|
|
664
|
+
var IDLE_FUNCS = /pthread_cond_wait|pthread_cond_timedwait|__psynch_cvwait|uv_cond_wait|sem_wait|uv_sem_wait|semaphore_wait_trap|Monitor::wait|epoll_wait|epoll_pwait|^(__GI_)?p?poll$|^select$|^pselect6$|kevent|io_uring_enter|uv__io_poll|^pause$|sigwait|sigtimedwait|^wait4$|^waitid$|mach_msg|__workq_kernreturn|_dispatch_(worker|event_loop)|GCDAsync|^swapper$|__semwait_signal/;
|
|
665
|
+
var IO_FUNCS = /^(__GI_)?(read|recv|recvfrom|recvmsg|accept|accept4|connect|nanosleep|clock_nanosleep|__clock_nanosleep|__select|__recvfrom|__read_nocancel|__connect|__accept)$|__libc_read|__libc_recv|SSL_read|usleep|^sleep$/;
|
|
666
|
+
function classifyNativeThreads(threads) {
|
|
667
|
+
const lock = [];
|
|
668
|
+
const io = [];
|
|
669
|
+
const idle = [];
|
|
670
|
+
const running = [];
|
|
671
|
+
const isSys = (f) => LOCK_FUNCS.test(f) || IDLE_FUNCS.test(f) || IO_FUNCS.test(f) || /^(__|_IO_|std::|uv_|uv__|start_thread|thread_start|_pthread_start|clone|clone3|\?\?|_dispatch|libsystem)/.test(f);
|
|
672
|
+
for (const t of threads) {
|
|
673
|
+
const top = t.frames.slice(0, 4);
|
|
674
|
+
const lockF = top.find((f) => LOCK_FUNCS.test(f.func));
|
|
675
|
+
const idleF = top.find((f) => IDLE_FUNCS.test(f.func));
|
|
676
|
+
const ioF = top.find((f) => IO_FUNCS.test(f.func));
|
|
677
|
+
const user = t.frames.find((f) => !isSys(f.func));
|
|
678
|
+
const at = user ? `${user.func}${user.loc ? ` (${user.loc.split("/").pop()})` : ""}` : t.frames.find((f) => !LOCK_FUNCS.test(f.func) && !IDLE_FUNCS.test(f.func) && !IO_FUNCS.test(f.func))?.func ?? "?";
|
|
679
|
+
const label = `${t.name ? `${t.name} ` : ""}#${t.index}${t.tid ? ` (tid ${t.tid})` : ""}`;
|
|
680
|
+
if (lockF) lock.push({ thread: label, waiting_in: lockF.func, at, kind: "lock" });
|
|
681
|
+
else if (idleF) idle.push({ thread: label, waiting_in: idleF.func, at, kind: "idle" });
|
|
682
|
+
else if (ioF) io.push({ thread: label, waiting_in: ioF.func, at, kind: "io" });
|
|
683
|
+
else running.push(t);
|
|
684
|
+
}
|
|
685
|
+
return { lock, io, idle, running };
|
|
686
|
+
}
|
|
687
|
+
async function summarizeNativeStacks(b, threads, artifact, debugger_) {
|
|
688
|
+
if (!threads.length) {
|
|
689
|
+
b.limitation(`${debugger_} produced no thread backtraces (no symbols? target exited?)`);
|
|
690
|
+
return b.fail(`${debugger_}: no stacks parsed`).build();
|
|
691
|
+
}
|
|
692
|
+
const { lock, io, idle, running } = classifyNativeThreads(threads);
|
|
693
|
+
const summaryEv = b.evidenceOf(
|
|
694
|
+
"stack_summary",
|
|
695
|
+
`${threads.length} thread(s): ${running.length} running/other, ${lock.length} in locks, ${io.length} in blocking I/O, ${idle.length} idle (cond/event waits)`,
|
|
696
|
+
threads.map((t) => ({ thread: `${t.name ?? ""}#${t.index}`, tid: t.tid, top: t.frames[0]?.func ?? "?", depth: t.frames.length })),
|
|
697
|
+
{ artifact }
|
|
698
|
+
);
|
|
699
|
+
for (const t of threads.slice(0, 12)) {
|
|
700
|
+
b.evidenceOf("stack", `${t.name ? `${t.name} ` : ""}#${t.index}: ${t.frames.slice(0, 5).map((f) => f.func).join(" \u2190 ")}`, { thread: `${t.name ?? ""}#${t.index}`, frames: t.frames.slice(0, 20).map((f) => `${f.func}${f.loc ? ` (${f.loc.split("/").pop()})` : ""}`) }, { artifact });
|
|
701
|
+
}
|
|
702
|
+
const noSymbols = threads.flatMap((t) => t.frames).filter((f) => f.func === "??").length;
|
|
703
|
+
if (noSymbols > 0) b.limitation(`${noSymbols} frame(s) have no symbols (stripped binaries); install debug symbols for better names`);
|
|
704
|
+
if (lock.length) {
|
|
705
|
+
const lockEv = b.evidenceOf("lock_wait", `${lock.length} thread(s) blocked on locks: ${lock.map((l) => `${l.thread} in ${l.waiting_in} at ${l.at}`).join("; ")}`, lock, { artifact });
|
|
706
|
+
if (lock.length >= 2 && running.length === 0) b.finding("high", `likely deadlock: ${lock.length} threads blocked in lock acquisition and none running (${lock.map((l) => `${l.thread}@${l.at}`).join(", ")})`, [lockEv, summaryEv], "inferred", "Take a second dump_stacks; identical positions confirm it. Handoff: step debugger at the lock sites.");
|
|
707
|
+
else b.finding("warn", `${lock.length} thread(s) waiting on locks: ${lock.map((l) => `${l.thread} at ${l.at}`).join("; ")}`, [lockEv], "observed");
|
|
708
|
+
}
|
|
709
|
+
if (io.length) {
|
|
710
|
+
const ioEv = b.evidenceOf("lock_wait", `${io.length} thread(s) in blocking I/O/sleep: ${io.slice(0, 5).map((l) => `${l.thread} in ${l.waiting_in} at ${l.at}`).join("; ")}`, io, { artifact });
|
|
711
|
+
const mainBlocked = io.find((l) => /#1\b/.test(l.thread));
|
|
712
|
+
if (mainBlocked) b.finding("warn", `main thread is blocked in ${mainBlocked.waiting_in} at ${mainBlocked.at} \u2014 waiting on a peer, a file or a timer`, [ioEv], "observed", "inspect_sockets shows which remote; trace_syscalls shows how long each wait takes.");
|
|
713
|
+
else b.finding("info", `${io.length} thread(s) blocked in I/O: ${io.slice(0, 3).map((l) => `${l.thread} in ${l.waiting_in}`).join("; ")}`, [ioEv]);
|
|
714
|
+
}
|
|
715
|
+
if (idle.length) {
|
|
716
|
+
const idleEv = b.evidenceOf("stack_summary", `${idle.length} idle thread(s) in ${[...new Set(idle.map((i) => i.waiting_in))].join(", ")}`, idle.slice(0, 20), { artifact });
|
|
717
|
+
if (!lock.length && !io.length && running.length === 0) b.finding("info", `all ${threads.length} threads are idle in event/condition waits (${[...new Set(idle.map((i) => i.waiting_in))].slice(0, 4).join(", ")}) \u2014 not stuck in user code; if callers hang, the work is pending elsewhere (inspect_sockets, trace_syscalls)`, [idleEv, summaryEv], "inferred");
|
|
718
|
+
}
|
|
719
|
+
if (running.length) {
|
|
720
|
+
const t = running[0];
|
|
721
|
+
b.finding("info", `thread ${t.name ?? ""}#${t.index} is executing ${t.frames[0]?.func ?? "?"}${t.frames[1] ? ` \u2190 ${t.frames[1].func}` : ""}`, [summaryEv]);
|
|
722
|
+
}
|
|
723
|
+
b.limitation("instant snapshot; the target was paused for the duration of the dump (typically <1 s)");
|
|
724
|
+
b.next("inspect_sockets", "trace_syscalls", "profile_cpu");
|
|
725
|
+
const interesting = lock[0]?.at ?? io.find((l) => /#1\b/.test(l.thread))?.at;
|
|
726
|
+
if (interesting && interesting !== "?") b.next(`static analysis: decompile ${interesting.replace(/\s*\(.*$/, "")} to see what it locks/reads`);
|
|
727
|
+
return b.build();
|
|
728
|
+
}
|
|
729
|
+
async function dumpStacks2(ctx) {
|
|
730
|
+
const { session, target } = ctx;
|
|
731
|
+
const b = new ResultBuilder("dump_stacks", session, target);
|
|
732
|
+
if (!target.pid) return b.fail("no pid").build();
|
|
733
|
+
const args = ["-p", String(target.pid), "-batch", "-q", "-nx", "-ex", "set pagination off", "-ex", "thread apply all bt 25", "-ex", "detach", "-ex", "quit"];
|
|
734
|
+
const r = await runCapped("gdb", args, { timeoutMs: 45e3 });
|
|
735
|
+
const raw = r.stdout + (r.stderr ? `
|
|
736
|
+
--- stderr ---
|
|
737
|
+
${r.stderr}` : "");
|
|
738
|
+
const art = await b.writeArtifact("gdb-bt.txt", raw, "raw_text", `gdb ${args.join(" ")}`);
|
|
739
|
+
if (looksLikePermissionError(r.stderr) || /ptrace: Operation not permitted/.test(raw)) {
|
|
740
|
+
b.limitation(rootRequiredLimitation("dump_stacks", `gdb ${args.join(" ")}`));
|
|
741
|
+
b.limitation("Linux: kernel.yama.ptrace_scope \u2265 1 blocks attaching to non-child processes");
|
|
742
|
+
b.next("import_artifact (gdb, after running the sudo command above)", "inspect_sockets", "inspect_files", "snapshot_memory");
|
|
743
|
+
return b.fail("gdb could not attach (permission denied)").build();
|
|
744
|
+
}
|
|
745
|
+
const threads = parseGdbBacktrace(r.stdout);
|
|
746
|
+
if (target.runtime === "node") b.limitation("native stacks only; JS frames show as v8 internals. For JS-level stacks use Node's inspector (--inspect) or `node --cpu-prof`.");
|
|
747
|
+
return summarizeNativeStacks(b, threads, art, "gdb");
|
|
748
|
+
}
|
|
749
|
+
var gdbAdapter = {
|
|
750
|
+
name: "gdb",
|
|
751
|
+
tools: ["dump_stacks"],
|
|
752
|
+
platforms: ["linux"],
|
|
753
|
+
requires: ["gdb"],
|
|
754
|
+
runtimes: ["native", "node", "go", "java", "unknown"],
|
|
755
|
+
install: { linux: "sudo apt install gdb | sudo dnf install gdb" },
|
|
756
|
+
priority: 5,
|
|
757
|
+
check(base, env) {
|
|
758
|
+
if (env.ptraceScope !== null && env.ptraceScope >= 1 && !env.isRoot) base.notes.push(`ptrace_scope=${env.ptraceScope}: can only attach to child processes without root`);
|
|
759
|
+
return base;
|
|
760
|
+
},
|
|
761
|
+
async run(tool, _args, ctx) {
|
|
762
|
+
if (tool !== "dump_stacks") throw new Error(`gdb adapter cannot run ${tool}`);
|
|
763
|
+
return dumpStacks2(ctx);
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
// src/adapters/lldb.ts
|
|
768
|
+
function parseLldbBacktrace(raw) {
|
|
769
|
+
const threads = [];
|
|
770
|
+
let cur = null;
|
|
771
|
+
for (const line of raw.split("\n")) {
|
|
772
|
+
let m = /^\*?\s*thread #(\d+)(?:, name = '([^']*)')?(?:, queue = '([^']*)')?/.exec(line);
|
|
773
|
+
if (m) {
|
|
774
|
+
const idx = Number(m[1]);
|
|
775
|
+
const existing = threads.find((t) => t.index === idx);
|
|
776
|
+
if (existing) {
|
|
777
|
+
cur = existing;
|
|
778
|
+
cur.frames = [];
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
cur = { index: idx, name: m[2] ?? m[3], frames: [] };
|
|
782
|
+
threads.push(cur);
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
m = /^\s*(?:\*\s*)?frame #(\d+): (0x[0-9a-f]+) ([^`]+)`(.+?)(?: \+ \d+)?(?: at (\S+))?$/.exec(line);
|
|
786
|
+
if (m && cur) {
|
|
787
|
+
const func = m[4].replace(/\(.*$/, "").trim();
|
|
788
|
+
cur.frames.push({ n: Number(m[1]), func, loc: m[5] ?? m[3], addr: m[2] });
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return threads;
|
|
792
|
+
}
|
|
793
|
+
async function dumpStacks3(ctx) {
|
|
794
|
+
const { session, target } = ctx;
|
|
795
|
+
const b = new ResultBuilder("dump_stacks", session, target);
|
|
796
|
+
if (!target.pid) return b.fail("no pid").build();
|
|
797
|
+
const args = ["-p", String(target.pid), "--batch", "-o", "thread backtrace all", "-o", "detach", "-o", "quit"];
|
|
798
|
+
const r = await runCapped("lldb", args, { timeoutMs: 45e3 });
|
|
799
|
+
const raw = r.stdout + (r.stderr ? `
|
|
800
|
+
--- stderr ---
|
|
801
|
+
${r.stderr}` : "");
|
|
802
|
+
const art = await b.writeArtifact("lldb-bt.txt", raw, "raw_text", `lldb ${args.join(" ")}`);
|
|
803
|
+
if (looksLikePermissionError(raw) || /attach failed|not permitted|Permission denied|does not exist|unable to attach/i.test(raw)) {
|
|
804
|
+
b.limitation(rootRequiredLimitation("dump_stacks", `lldb ${args.join(" ")}`));
|
|
805
|
+
b.limitation("macOS: attaching to a process you did not spawn needs sudo, or the target must opt in (com.apple.security.get-task-allow). Hardened/SIP-protected binaries cannot be attached at all.");
|
|
806
|
+
b.next("import_artifact (lldb, after running the sudo command above)", "inspect_sockets", "snapshot_memory", "profile_cpu (sample, own processes)");
|
|
807
|
+
return b.fail("lldb could not attach (permission denied)").build();
|
|
808
|
+
}
|
|
809
|
+
const threads = parseLldbBacktrace(r.stdout);
|
|
810
|
+
if (target.runtime === "node") b.limitation("native stacks only; JS frames appear as v8 internals. For JS-level stacks use the inspector (--inspect).");
|
|
811
|
+
return summarizeNativeStacks(b, threads, art, "lldb");
|
|
812
|
+
}
|
|
813
|
+
var lldbAdapter = {
|
|
814
|
+
name: "lldb",
|
|
815
|
+
tools: ["dump_stacks"],
|
|
816
|
+
platforms: ["darwin"],
|
|
817
|
+
requires: ["lldb"],
|
|
818
|
+
runtimes: ["native", "node", "go", "java", "unknown"],
|
|
819
|
+
install: { darwin: "xcode-select --install" },
|
|
820
|
+
priority: 5,
|
|
821
|
+
check(base, env) {
|
|
822
|
+
if (!env.isRoot) base.notes.push("macOS: attaching needs sudo unless the target is your own debuggable child");
|
|
823
|
+
return base;
|
|
824
|
+
},
|
|
825
|
+
async run(tool, _args, ctx) {
|
|
826
|
+
if (tool !== "dump_stacks") throw new Error(`lldb adapter cannot run ${tool}`);
|
|
827
|
+
return dumpStacks3(ctx);
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
// src/adapters/perf.ts
|
|
832
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
833
|
+
function perfScriptToFolded(raw, opts = {}) {
|
|
834
|
+
const counts = /* @__PURE__ */ new Map();
|
|
835
|
+
let comm = null;
|
|
836
|
+
let frames = [];
|
|
837
|
+
const flush = () => {
|
|
838
|
+
if (comm !== null && frames.length) {
|
|
839
|
+
const stack = [...opts.includeComm ? [comm] : [], ...frames.reverse()];
|
|
840
|
+
const key = stack.join(";");
|
|
841
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
842
|
+
}
|
|
843
|
+
comm = null;
|
|
844
|
+
frames = [];
|
|
845
|
+
};
|
|
846
|
+
for (const line of raw.split("\n")) {
|
|
847
|
+
if (!line.trim()) {
|
|
848
|
+
flush();
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
if (!/^\s/.test(line)) {
|
|
852
|
+
flush();
|
|
853
|
+
const m2 = /^(\S+)\s+\d+/.exec(line);
|
|
854
|
+
comm = m2 ? m2[1] : "?";
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
const m = /^\s+[0-9a-f]+\s+(.+?)(?:\+0x[0-9a-f]+)?\s+\((.*)\)\s*$/.exec(line);
|
|
858
|
+
if (m && comm !== null) {
|
|
859
|
+
let sym = m[1];
|
|
860
|
+
const dso = m[2];
|
|
861
|
+
if (sym === "[unknown]") sym = `[unknown] (${dso.split("/").pop()})`;
|
|
862
|
+
frames.push(sym.replace(/;/g, ","));
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
flush();
|
|
866
|
+
return [...counts].map(([k, v]) => `${k} ${v}`).join("\n") + (counts.size ? "\n" : "");
|
|
867
|
+
}
|
|
868
|
+
async function profileCpu2(ctx, args) {
|
|
869
|
+
const { session, target } = ctx;
|
|
870
|
+
const b = new ResultBuilder("profile_cpu", session, target);
|
|
871
|
+
if (!target.pid) return b.fail("no pid").build();
|
|
872
|
+
const duration = clampDuration(args.duration_s, 15);
|
|
873
|
+
const dataPath = b.artifactPath("perf.data");
|
|
874
|
+
const recArgs = ["record", "-F", "99", "-g", "--call-graph", "fp", "-p", String(target.pid), "-o", dataPath, "--", "sleep", String(duration)];
|
|
875
|
+
const rec = await runCapped("perf", recArgs, { timeoutMs: duration * 1e3 + LIMITS.DURATION_SLACK_MS + 1e4 });
|
|
876
|
+
if (rec.exitCode !== 0 || looksLikePermissionError(rec.stderr) || /perf_event_open|Permission|not permitted/i.test(rec.stderr)) {
|
|
877
|
+
await b.writeArtifact("perf-stderr.txt", rec.stderr, "raw_text");
|
|
878
|
+
if (looksLikePermissionError(rec.stderr) || /paranoid|perf_event_open/i.test(rec.stderr)) {
|
|
879
|
+
b.limitation(rootRequiredLimitation("profile_cpu", `perf ${recArgs.join(" ")}`));
|
|
880
|
+
b.limitation("Linux: lower kernel.perf_event_paranoid to 1 (sudo sysctl kernel.perf_event_paranoid=1) or grant CAP_PERFMON; inside Docker add --cap-add PERFMON --cap-add SYS_ADMIN");
|
|
881
|
+
b.next("import_artifact (perf_script, after running the sudo command above)", "dump_stacks", "trace_syscalls", "snapshot_memory");
|
|
882
|
+
return b.fail("perf record needs permissions").build();
|
|
883
|
+
}
|
|
884
|
+
b.limitation(`perf record exited ${rec.exitCode}: ${rec.stderr.trim().slice(0, 300)}`);
|
|
885
|
+
return b.fail("perf record failed").build();
|
|
886
|
+
}
|
|
887
|
+
const scriptPath = b.artifactPath("perf-script.txt");
|
|
888
|
+
const scr = await runCapped("perf", ["script", "-i", dataPath, "-F", "comm,pid,time,event,ip,sym,dso"], { timeoutMs: 6e4, stdoutFile: scriptPath });
|
|
889
|
+
const raw = await readFile2(scriptPath, "utf8").catch(() => "");
|
|
890
|
+
await b.addArtifact(dataPath, "raw_text", { description: "perf.data (binary)" });
|
|
891
|
+
await b.addArtifact(scriptPath, "raw_text", { description: "perf script output", truncated: scr.outputCapped });
|
|
892
|
+
const folded = perfScriptToFolded(raw);
|
|
893
|
+
await b.writeArtifact("perf-folded.txt", folded, "folded_stacks", "folded stacks");
|
|
894
|
+
const unknown = (folded.match(/\[unknown\]/g) ?? []).length;
|
|
895
|
+
if (unknown > 0) b.limitation(`${unknown} stack frames have no symbols${target.runtime === "node" ? " \u2014 start Node with --perf-basic-prof (or --perf-prof) so JIT frames get names" : target.runtime === "java" ? " \u2014 use perf-map-agent or async-profiler for JIT frames" : "; install debug symbols or build with frame pointers"}`);
|
|
896
|
+
b.limitation("perf samples at 99 Hz with frame-pointer unwinding; frames may be missing in code built without frame pointers (use --call-graph dwarf manually for those)");
|
|
897
|
+
return summarizeProfile(b, folded, duration, "perf", 99);
|
|
898
|
+
}
|
|
899
|
+
var perfAdapter = {
|
|
900
|
+
name: "perf",
|
|
901
|
+
tools: ["profile_cpu"],
|
|
902
|
+
platforms: ["linux"],
|
|
903
|
+
requires: ["perf"],
|
|
904
|
+
runtimes: ["native", "node", "go", "java", "unknown"],
|
|
905
|
+
install: { linux: "sudo apt install linux-perf (Debian/Ubuntu: linux-tools-$(uname -r)) | sudo dnf install perf" },
|
|
906
|
+
priority: 5,
|
|
907
|
+
check(base, env) {
|
|
908
|
+
if (env.perfParanoid !== null && env.perfParanoid > 1 && !env.isRoot) base.notes.push(`perf_event_paranoid=${env.perfParanoid}: cannot profile other processes without root`);
|
|
909
|
+
return base;
|
|
910
|
+
},
|
|
911
|
+
async run(tool, args, ctx) {
|
|
912
|
+
if (tool !== "profile_cpu") throw new Error(`perf adapter cannot run ${tool}`);
|
|
913
|
+
return profileCpu2(ctx, args);
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
|
|
917
|
+
// src/adapters/tcpdump.ts
|
|
918
|
+
function splitHostPort(s) {
|
|
919
|
+
const m = /^(.*)\.(\d+)$/.exec(s);
|
|
920
|
+
if (!m) return { host: s };
|
|
921
|
+
return { host: m[1], port: Number(m[2]) };
|
|
922
|
+
}
|
|
923
|
+
function parseTcpdump(raw) {
|
|
924
|
+
const out = [];
|
|
925
|
+
for (const line of raw.split("\n")) {
|
|
926
|
+
const m = /^(\d+\.\d+) (?:[\w.-]+\s+(In|Out|B|M|P)\s+)?(IP6?|ARP) (.*)$/.exec(line.trim());
|
|
927
|
+
if (!m) continue;
|
|
928
|
+
const ts = Number(m[1]);
|
|
929
|
+
const dir = m[2];
|
|
930
|
+
const proto = m[3];
|
|
931
|
+
const rest = m[4];
|
|
932
|
+
const flow = /^(\S+) > (\S+): (.*)$/.exec(rest);
|
|
933
|
+
if (!flow) continue;
|
|
934
|
+
const s = splitHostPort(flow[1]);
|
|
935
|
+
const d = splitHostPort(flow[2].replace(/:$/, ""));
|
|
936
|
+
const body = flow[3];
|
|
937
|
+
const pkt = { ts, proto, src: s.host, sport: s.port, dst: d.host, dport: d.port, kind: "other", dir };
|
|
938
|
+
if (/^Flags \[/.test(body)) {
|
|
939
|
+
pkt.kind = "tcp";
|
|
940
|
+
pkt.flags = /^Flags \[([^\]]*)\]/.exec(body)?.[1];
|
|
941
|
+
pkt.seq = /\bseq (\d+(?::\d+)?)/.exec(body)?.[1];
|
|
942
|
+
const len = /length (\d+)/.exec(body);
|
|
943
|
+
if (len) pkt.length = Number(len[1]);
|
|
944
|
+
const sni = /SNI=([A-Za-z0-9.-]+)/.exec(body);
|
|
945
|
+
if (sni) pkt.tlsSni = sni[1];
|
|
946
|
+
} else if (/^\d+\+?\$? [A-Z]+\??/.test(body) || /^\d+ [A-Z]+\? /.test(body) || /\b(A|AAAA|CNAME|MX|TXT|SRV)\?/.test(body) && (d.port === 53 || s.port === 53)) {
|
|
947
|
+
pkt.kind = "dns";
|
|
948
|
+
const q = /\b(?:A|AAAA|CNAME|MX|TXT|SRV|PTR)\? ([^\s,]+)/.exec(body);
|
|
949
|
+
if (q) pkt.dnsQuery = q[1].replace(/\.$/, "");
|
|
950
|
+
} else if (/^UDP, length (\d+)/.test(body)) {
|
|
951
|
+
pkt.kind = "udp";
|
|
952
|
+
pkt.length = Number(/length (\d+)/.exec(body)[1]);
|
|
953
|
+
}
|
|
954
|
+
out.push(pkt);
|
|
955
|
+
}
|
|
956
|
+
return out;
|
|
957
|
+
}
|
|
958
|
+
function summarizeTraffic(pkts, localPorts) {
|
|
959
|
+
const remotes = /* @__PURE__ */ new Map();
|
|
960
|
+
const dns = /* @__PURE__ */ new Map();
|
|
961
|
+
const sni = /* @__PURE__ */ new Map();
|
|
962
|
+
let rst = 0;
|
|
963
|
+
let retransmits = 0;
|
|
964
|
+
let bytes = 0;
|
|
965
|
+
const deduped = [];
|
|
966
|
+
const lastSeen = /* @__PURE__ */ new Map();
|
|
967
|
+
for (const p of pkts) {
|
|
968
|
+
const key = `${p.src}:${p.sport}>${p.dst}:${p.dport}|${p.flags ?? ""}|${p.seq ?? ""}|${p.length ?? ""}|${p.kind}`;
|
|
969
|
+
const prev = lastSeen.get(key);
|
|
970
|
+
lastSeen.set(key, p.ts);
|
|
971
|
+
if (prev !== void 0 && p.ts - prev < 2e-3 && p.dir !== void 0) continue;
|
|
972
|
+
deduped.push(p);
|
|
973
|
+
}
|
|
974
|
+
const seenSeq = /* @__PURE__ */ new Map();
|
|
975
|
+
const syns = /* @__PURE__ */ new Map();
|
|
976
|
+
const synacks = /* @__PURE__ */ new Set();
|
|
977
|
+
for (const p of deduped) {
|
|
978
|
+
const isLocalSrc = p.sport !== void 0 && localPorts.has(p.sport);
|
|
979
|
+
const remoteHost = isLocalSrc ? p.dst : p.src;
|
|
980
|
+
const r = remotes.get(remoteHost) ?? { remote: remoteHost, packets: 0, bytes: 0, rst: 0, syn: 0, fin: 0, retransmits: 0 };
|
|
981
|
+
r.packets++;
|
|
982
|
+
r.bytes += p.length ?? 0;
|
|
983
|
+
bytes += p.length ?? 0;
|
|
984
|
+
if (p.kind === "tcp" && p.flags) {
|
|
985
|
+
if (p.flags.includes("R")) {
|
|
986
|
+
r.rst++;
|
|
987
|
+
rst++;
|
|
988
|
+
}
|
|
989
|
+
if (p.flags === "S") {
|
|
990
|
+
r.syn++;
|
|
991
|
+
syns.set(`${p.src}:${p.sport}>${p.dst}:${p.dport}`, (syns.get(`${p.src}:${p.sport}>${p.dst}:${p.dport}`) ?? 0) + 1);
|
|
992
|
+
}
|
|
993
|
+
if (p.flags === "S.") synacks.add(`${p.dst}:${p.dport}>${p.src}:${p.sport}`);
|
|
994
|
+
if (p.flags.includes("F")) r.fin++;
|
|
995
|
+
if ((p.length ?? 0) > 0 && p.seq) {
|
|
996
|
+
const key = `${p.src}:${p.sport}>${p.dst}:${p.dport}|${p.seq}`;
|
|
997
|
+
const first = seenSeq.get(key);
|
|
998
|
+
if (first !== void 0 && p.ts - first > 2e-3) {
|
|
999
|
+
r.retransmits++;
|
|
1000
|
+
retransmits++;
|
|
1001
|
+
} else if (first === void 0) seenSeq.set(key, p.ts);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
if (p.kind === "dns" && p.dnsQuery) dns.set(p.dnsQuery, (dns.get(p.dnsQuery) ?? 0) + 1);
|
|
1005
|
+
if (p.tlsSni) sni.set(p.tlsSni, (sni.get(p.tlsSni) ?? 0) + 1);
|
|
1006
|
+
remotes.set(remoteHost, r);
|
|
1007
|
+
}
|
|
1008
|
+
let synNoAck = 0;
|
|
1009
|
+
for (const [flow, n] of syns) if (!synacks.has(flow)) synNoAck += n;
|
|
1010
|
+
const ts = deduped.map((p) => p.ts);
|
|
1011
|
+
return {
|
|
1012
|
+
packets: deduped.length,
|
|
1013
|
+
bytes,
|
|
1014
|
+
byRemote: [...remotes.values()].sort((a, b) => b.bytes - a.bytes || b.packets - a.packets),
|
|
1015
|
+
rst,
|
|
1016
|
+
syn_no_synack: synNoAck,
|
|
1017
|
+
retransmits,
|
|
1018
|
+
dns: [...dns].map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count),
|
|
1019
|
+
sni: [...sni].map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count),
|
|
1020
|
+
duration_s: ts.length ? Math.round((Math.max(...ts) - Math.min(...ts)) * 100) / 100 : 0
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
function buildFilter(ports, extra) {
|
|
1024
|
+
const uniq = [...new Set(ports.filter((p) => p > 0 && p < 65536))].slice(0, 40);
|
|
1025
|
+
if (!uniq.length) throw new Error("target has no sockets/ports to scope the capture to; refusing an unbounded capture");
|
|
1026
|
+
const portExpr = uniq.length === 1 ? `port ${uniq[0]}` : `(${uniq.map((p) => `port ${p}`).join(" or ")})`;
|
|
1027
|
+
const base = `(${portExpr} or (udp and port 53))`;
|
|
1028
|
+
return extra ? `${base} and (${assertSafeBpfFragment(extra)})` : base;
|
|
1029
|
+
}
|
|
1030
|
+
function summarizeCaptureEvidence(b, pkts, localPorts, meta) {
|
|
1031
|
+
const { filter, duration } = meta;
|
|
1032
|
+
const local = localPorts;
|
|
1033
|
+
const s = summarizeTraffic(pkts, local);
|
|
1034
|
+
const filterEv = b.evidenceOf("note", `filter: ${filter}`, { filter, ports: [...local].slice(0, 40), duration_s: duration });
|
|
1035
|
+
const totalEv = b.evidenceOf("packet_summary", `${s.packets} packets, ${humanBytes(s.bytes)} payload over ${s.duration_s}s; RST=${s.rst} retransmit\u2248${s.retransmits} unanswered SYN=${s.syn_no_synack}`, { name: "packets", packets: s.packets, bytes: s.bytes, rst: s.rst, retransmits: s.retransmits, syn_no_synack: s.syn_no_synack, duration_s: s.duration_s }, { artifact: meta.pcapPath ?? meta.textPath });
|
|
1036
|
+
const remoteEv = b.evidenceOf("packet_summary", s.byRemote.length ? `per remote: ${s.byRemote.slice(0, 5).map((r2) => `${r2.remote} ${r2.packets}p/${humanBytes(r2.bytes)}${r2.rst ? ` RST\xD7${r2.rst}` : ""}${r2.retransmits ? ` rtx\xD7${r2.retransmits}` : ""}`).join("; ")}` : "no packets matched", s.byRemote.slice(0, 25), { artifact: meta.textPath });
|
|
1037
|
+
if (s.dns.length) b.evidenceOf("dns_query", `DNS: ${s.dns.slice(0, 6).map((d) => `${d.name}\xD7${d.count}`).join(", ")}`, s.dns.slice(0, 30), { artifact: meta.textPath });
|
|
1038
|
+
if (s.sni.length) b.evidenceOf("tls_sni", `TLS SNI: ${s.sni.slice(0, 6).map((d) => `${d.name}\xD7${d.count}`).join(", ")}`, s.sni.slice(0, 30), { artifact: meta.textPath });
|
|
1039
|
+
if (s.packets === 0) b.finding("info", `no packets on the target's ports in ${duration}s`, [totalEv, filterEv]);
|
|
1040
|
+
if (s.rst > 0) {
|
|
1041
|
+
const worst = [...s.byRemote].sort((a, c) => c.rst - a.rst)[0];
|
|
1042
|
+
b.finding(s.rst >= 10 ? "high" : "warn", `${s.rst} TCP RST(s), most with ${worst.remote} (${worst.rst}) \u2014 connections are being refused or torn down abruptly`, [totalEv, remoteEv], "observed");
|
|
1043
|
+
}
|
|
1044
|
+
if (s.syn_no_synack > 0) b.finding("high", `${s.syn_no_synack} SYN(s) never got a SYN-ACK \u2014 a remote is unreachable or firewalled`, [totalEv, remoteEv], "observed");
|
|
1045
|
+
if (s.retransmits >= 5) b.finding("warn", `\u2248${s.retransmits} probable retransmissions \u2014 packet loss or a slow peer`, [totalEv], "inferred");
|
|
1046
|
+
const dnsRepeat = s.dns.find((d) => d.count >= 20);
|
|
1047
|
+
if (dnsRepeat) b.finding("warn", `${dnsRepeat.name} resolved ${dnsRepeat.count}\xD7 in ${duration}s \u2014 no DNS caching`, [b.evidenceOf("dns_query", "repeated lookups", dnsRepeat)], "inferred");
|
|
1048
|
+
if (b.findingCount === 0 && s.packets > 0) b.finding("info", `${s.packets} packets, top remote ${s.byRemote[0]?.remote} (${humanBytes(s.byRemote[0]?.bytes ?? 0)})`, [remoteEv]);
|
|
1049
|
+
}
|
|
1050
|
+
async function targetPorts(pids) {
|
|
1051
|
+
const r = await runCapped("lsof", ["-nP", "-w", "-a", "-F", "pcufatsinTP", "-i", "-p", pids.join(",")], { timeoutMs: 15e3 });
|
|
1052
|
+
const ports = [];
|
|
1053
|
+
for (const e of parseLsofFields(r.stdout)) {
|
|
1054
|
+
if (e.type !== "IPv4" && e.type !== "IPv6") continue;
|
|
1055
|
+
const local = e.name.split("->")[0];
|
|
1056
|
+
const port = Number(local.slice(local.lastIndexOf(":") + 1));
|
|
1057
|
+
if (port) ports.push(port);
|
|
1058
|
+
const remote = e.name.split("->")[1];
|
|
1059
|
+
if (remote) {
|
|
1060
|
+
const rp = Number(remote.slice(remote.lastIndexOf(":") + 1));
|
|
1061
|
+
if (rp) ports.push(rp);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
return ports;
|
|
1065
|
+
}
|
|
1066
|
+
async function captureTraffic(ctx, args) {
|
|
1067
|
+
const { session, target } = ctx;
|
|
1068
|
+
const b = new ResultBuilder("capture_traffic", session, target);
|
|
1069
|
+
const pids = targetPids(target);
|
|
1070
|
+
if (!pids.length) return b.fail("no pid; cannot derive the target's ports").build();
|
|
1071
|
+
const duration = clampDuration(args.duration_s, 10, LIMITS.MAX_CAPTURE_WALL_MS);
|
|
1072
|
+
let filter;
|
|
1073
|
+
let ports;
|
|
1074
|
+
try {
|
|
1075
|
+
ports = await targetPorts(pids);
|
|
1076
|
+
filter = buildFilter(ports, args.filter);
|
|
1077
|
+
} catch (err) {
|
|
1078
|
+
b.limitation(err.message);
|
|
1079
|
+
return b.fail("refusing unbounded capture").build();
|
|
1080
|
+
}
|
|
1081
|
+
const pcap = b.artifactPath("capture.pcap");
|
|
1082
|
+
const iface = platform() === "linux" ? "any" : "any";
|
|
1083
|
+
const tdArgs = ["-i", iface, "-nn", "-q", "-U", "-s", "256", "-w", pcap, "-G", String(duration), "-W", "1", filter];
|
|
1084
|
+
const r = await runCapped("tcpdump", tdArgs, { timeoutMs: duration * 1e3 + LIMITS.DURATION_SLACK_MS + 3e3 });
|
|
1085
|
+
if (looksLikePermissionError(r.stderr) || /You don't have permission|Operation not permitted|cannot open BPF device/i.test(r.stderr)) {
|
|
1086
|
+
b.limitation(rootRequiredLimitation("capture_traffic", `tcpdump ${tdArgs.map((a) => a === filter ? `'${a}'` : a).join(" ")}`));
|
|
1087
|
+
if (platform() === "darwin") b.limitation("macOS: add yourself to the access_bpf group (install Wireshark's ChmodBPF) or run with sudo");
|
|
1088
|
+
if (platform() === "linux") b.limitation("Linux: sudo setcap cap_net_raw,cap_net_admin=eip $(which tcpdump) grants capture without root");
|
|
1089
|
+
return b.fail("tcpdump needs permissions").build();
|
|
1090
|
+
}
|
|
1091
|
+
const art = await b.addArtifact(pcap, "pcap", { description: `tcpdump ${duration}s, filter: ${filter}` });
|
|
1092
|
+
const read = await runCapped("tcpdump", ["-nn", "-tt", "-r", pcap], { timeoutMs: 3e4 });
|
|
1093
|
+
const textPath = await b.writeArtifact("capture.txt", read.stdout, "raw_text", "tcpdump -nn -tt -r capture.pcap");
|
|
1094
|
+
summarizeCaptureEvidence(b, parseTcpdump(read.stdout), new Set(ports), { filter, duration, pcapPath: art.path, textPath });
|
|
1095
|
+
b.limitation("retransmits = same flow and TCP seq range seen again >2 ms later (loopback In/Out duplicates collapsed); open the .pcap in Wireshark for exact analysis");
|
|
1096
|
+
b.limitation("TLS SNI and payloads need a deeper snaplen/-v; this capture used -s 256 -q. Raw .pcap may contain secrets and is NOT redacted");
|
|
1097
|
+
if (!isRoot()) b.limitation("capture ran without root \u2014 if it worked, your user has BPF access");
|
|
1098
|
+
b.next("inspect_sockets", "trace_syscalls");
|
|
1099
|
+
return b.build();
|
|
1100
|
+
}
|
|
1101
|
+
var tcpdumpAdapter = {
|
|
1102
|
+
name: "tcpdump",
|
|
1103
|
+
tools: ["capture_traffic"],
|
|
1104
|
+
platforms: ["linux", "darwin"],
|
|
1105
|
+
requires: ["tcpdump", "lsof"],
|
|
1106
|
+
install: { linux: "sudo apt install tcpdump", darwin: "preinstalled (/usr/sbin/tcpdump); needs sudo or access_bpf group" },
|
|
1107
|
+
priority: 10,
|
|
1108
|
+
check(base, env) {
|
|
1109
|
+
if (!env.isRoot) base.notes.push("needs root or CAP_NET_RAW / access_bpf group; requires consent: true");
|
|
1110
|
+
return base;
|
|
1111
|
+
},
|
|
1112
|
+
async run(tool, args, ctx) {
|
|
1113
|
+
if (tool !== "capture_traffic") throw new Error(`tcpdump adapter cannot run ${tool}`);
|
|
1114
|
+
return captureTraffic(ctx, args);
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1118
|
+
// src/adapters/sample_macos.ts
|
|
1119
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
1120
|
+
function sampleToFolded(raw) {
|
|
1121
|
+
const start = raw.indexOf("Call graph:");
|
|
1122
|
+
if (start < 0) return "";
|
|
1123
|
+
const end = raw.indexOf("\nTotal number in stack", start);
|
|
1124
|
+
const section = raw.slice(start, end > 0 ? end : void 0).split("\n").slice(1);
|
|
1125
|
+
const stack = [];
|
|
1126
|
+
const out = [];
|
|
1127
|
+
const flushLeaf = (node, path) => {
|
|
1128
|
+
const self = node.count - node.childSum;
|
|
1129
|
+
if (self > 0) out.push(`${[...path, node.name].join(";")} ${self}`);
|
|
1130
|
+
};
|
|
1131
|
+
for (const line of section) {
|
|
1132
|
+
if (!line.trim()) continue;
|
|
1133
|
+
const m = /^((?:\s|[+!:|])*?)(\d+)\s+(.+?)(?:\s{2}\(in ([^)]+)\))?(?:\s+\+\s+\d+)?(?:\s+\[0x[0-9a-f]+(?:,0x[0-9a-f]+)?\])?\s*$/.exec(line);
|
|
1134
|
+
if (!m) continue;
|
|
1135
|
+
const depth = Math.max(0, Math.floor((m[1].length - 4) / 2));
|
|
1136
|
+
const count = Number(m[2]);
|
|
1137
|
+
let name = m[3].trim();
|
|
1138
|
+
const dso = m[4];
|
|
1139
|
+
if (/^Thread_\d+/.test(name)) name = name.replace(/\s+DispatchQueue.*$/, "").replace(/\s{2,}.*$/, "");
|
|
1140
|
+
if (dso && !/^Thread_/.test(name)) name = `${name} (${dso})`;
|
|
1141
|
+
name = name.replace(/;/g, ",");
|
|
1142
|
+
while (stack.length && stack[stack.length - 1].depth >= depth) {
|
|
1143
|
+
const node = stack.pop();
|
|
1144
|
+
flushLeaf(node, stack.map((s) => s.name));
|
|
1145
|
+
}
|
|
1146
|
+
const parent = stack[stack.length - 1];
|
|
1147
|
+
if (parent) parent.childSum += count;
|
|
1148
|
+
stack.push({ depth, name, count, childSum: 0 });
|
|
1149
|
+
}
|
|
1150
|
+
while (stack.length) {
|
|
1151
|
+
const node = stack.pop();
|
|
1152
|
+
flushLeaf(node, stack.map((s) => s.name));
|
|
1153
|
+
}
|
|
1154
|
+
return out.join("\n") + (out.length ? "\n" : "");
|
|
1155
|
+
}
|
|
1156
|
+
async function profileCpu3(ctx, args) {
|
|
1157
|
+
const { session, target } = ctx;
|
|
1158
|
+
const b = new ResultBuilder("profile_cpu", session, target);
|
|
1159
|
+
if (!target.pid) return b.fail("no pid").build();
|
|
1160
|
+
const duration = clampDuration(args.duration_s, 15);
|
|
1161
|
+
const out = b.artifactPath("sample.txt");
|
|
1162
|
+
const sArgs = [String(target.pid), String(duration), "1", "-mayDie", "-file", out];
|
|
1163
|
+
const r = await runCapped("sample", sArgs, { timeoutMs: duration * 1e3 + LIMITS.DURATION_SLACK_MS + 1e4 });
|
|
1164
|
+
const raw = await readFile3(out, "utf8").catch(() => "");
|
|
1165
|
+
if (!raw) {
|
|
1166
|
+
if (looksLikePermissionError(r.stderr + r.stdout) || /Unable to sample|not permitted|failed to attach|Sampling failed/i.test(r.stderr + r.stdout)) {
|
|
1167
|
+
b.limitation(rootRequiredLimitation("profile_cpu", `sample ${sArgs.join(" ")}`));
|
|
1168
|
+
b.limitation("macOS: sampling another user's or a hardened process needs sudo");
|
|
1169
|
+
return b.fail("sample could not attach").build();
|
|
1170
|
+
}
|
|
1171
|
+
b.limitation(`sample: ${(r.stderr || r.stdout).trim().slice(0, 300)}`);
|
|
1172
|
+
return b.fail("sample produced no output").build();
|
|
1173
|
+
}
|
|
1174
|
+
await b.addArtifact(out, "raw_text", { description: `sample ${duration}s` });
|
|
1175
|
+
const folded = sampleToFolded(raw);
|
|
1176
|
+
await b.writeArtifact("sample-folded.txt", folded, "folded_stacks", "folded stacks derived from sample call graph");
|
|
1177
|
+
b.limitation("`sample` samples every 1 ms across all threads including idle ones; idle frames (kevent, __psynch_cvwait, mach_msg) are expected to dominate for servers");
|
|
1178
|
+
return summarizeProfile(b, folded, duration, "sample", 1e3);
|
|
1179
|
+
}
|
|
1180
|
+
var sampleMacosAdapter = {
|
|
1181
|
+
name: "sample",
|
|
1182
|
+
tools: ["profile_cpu"],
|
|
1183
|
+
platforms: ["darwin"],
|
|
1184
|
+
requires: ["sample"],
|
|
1185
|
+
runtimes: ["native", "node", "go", "java", "unknown"],
|
|
1186
|
+
install: { darwin: "preinstalled (/usr/bin/sample)" },
|
|
1187
|
+
priority: 5,
|
|
1188
|
+
async run(tool, args, ctx) {
|
|
1189
|
+
if (tool !== "profile_cpu") throw new Error(`sample adapter cannot run ${tool}`);
|
|
1190
|
+
return profileCpu3(ctx, args);
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
|
|
1194
|
+
// src/adapters/fs_usage.ts
|
|
1195
|
+
var DARWIN_ERRNO = { 1: "EPERM", 2: "ENOENT", 4: "EINTR", 5: "EIO", 9: "EBADF", 11: "EDEADLK", 12: "ENOMEM", 13: "EACCES", 17: "EEXIST", 20: "ENOTDIR", 21: "EISDIR", 22: "EINVAL", 24: "EMFILE", 28: "ENOSPC", 32: "EPIPE", 35: "EAGAIN", 36: "EINPROGRESS", 54: "ECONNRESET", 57: "ENOTCONN", 60: "ETIMEDOUT", 61: "ECONNREFUSED", 65: "EHOSTUNREACH" };
|
|
1196
|
+
function parseFsUsage(raw) {
|
|
1197
|
+
const out = [];
|
|
1198
|
+
const lines = raw.split("\n");
|
|
1199
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1200
|
+
const line = lines[i].trimEnd();
|
|
1201
|
+
const m = /^(\d{2}:\d{2}:\d{2}\.\d+)\s+(\S+)\s+(.*?)\s+(\d+\.\d{6})(?:\s+(W))?\s+(\S+?)\.(\d+)\s*$/.exec(line);
|
|
1202
|
+
if (!m) continue;
|
|
1203
|
+
const name = m[2].replace(/\[.*$/, "");
|
|
1204
|
+
const args = m[3].trim();
|
|
1205
|
+
const err = /\[\s*(\d+)\]/.exec(args);
|
|
1206
|
+
const errno = err ? DARWIN_ERRNO[Number(err[1])] ?? `E${err[1]}` : void 0;
|
|
1207
|
+
out.push({ pid: Number(m[7]), time: m[1], name, args: args.replace(/\[\s*\d+\]\s*/, "").replace(/^\(\S+\)\s*/, ""), result: errno ? `-1 ${errno}` : m[5] ? "0 (blocked)" : "0", errno, duration_s: Number(m[4]), line: i + 1 });
|
|
1208
|
+
}
|
|
1209
|
+
return out;
|
|
1210
|
+
}
|
|
1211
|
+
var FS_FILTERS = { network: "network", file: "filesys", process: "exec", signal: void 0, memory: void 0, all: "filesys,network,exec" };
|
|
1212
|
+
async function traceSyscalls2(ctx, args) {
|
|
1213
|
+
const { session, target } = ctx;
|
|
1214
|
+
const b = new ResultBuilder("trace_syscalls", session, target);
|
|
1215
|
+
if (!target.pid) return b.fail("no pid").build();
|
|
1216
|
+
const duration = clampDuration(args.duration_s, 10);
|
|
1217
|
+
const cats = args.categories?.length ? args.categories : ["all"];
|
|
1218
|
+
const filters = [...new Set(cats.map((c) => FS_FILTERS[c]).filter(Boolean))].join(",") || "filesys,network";
|
|
1219
|
+
const out = b.artifactPath("fs_usage.txt");
|
|
1220
|
+
const fsArgs = ["-w", "-f", filters, "-p", String(target.pid)];
|
|
1221
|
+
const cmd = `sudo fs_usage ${fsArgs.join(" ")} > ${out}`;
|
|
1222
|
+
if (!isRoot()) {
|
|
1223
|
+
b.limitation(rootRequiredLimitation("trace_syscalls", `fs_usage ${fsArgs.join(" ")}`));
|
|
1224
|
+
b.limitation(`macOS has no strace; fs_usage needs root. Run for ~${duration}s then Ctrl-C: ${cmd} and then: debuganything import fs_usage ${out} --session ${session.id}`);
|
|
1225
|
+
b.limitation("or run the target on Linux (docker/linux-lab) where strace gives the full breakdown");
|
|
1226
|
+
b.next(`import_artifact (fs_usage, ${out})`, "inspect_sockets", "profile_cpu");
|
|
1227
|
+
return b.fail("fs_usage needs root; Debug Anything never escalates").build();
|
|
1228
|
+
}
|
|
1229
|
+
const r = await runCapped("fs_usage", fsArgs, { timeoutMs: duration * 1e3, stdoutFile: out, captureInMemory: false });
|
|
1230
|
+
const { readFile: readFile4 } = await import("fs/promises");
|
|
1231
|
+
const raw = await readFile4(out, "utf8").catch(() => "");
|
|
1232
|
+
const art = await b.addArtifact(out, "raw_text", { description: `fs_usage ${fsArgs.join(" ")} for ${duration}s`, truncated: r.outputCapped });
|
|
1233
|
+
if (!raw.trim()) {
|
|
1234
|
+
if (looksLikePermissionError(r.stderr)) {
|
|
1235
|
+
b.limitation(rootRequiredLimitation("trace_syscalls", `fs_usage ${fsArgs.join(" ")}`));
|
|
1236
|
+
return b.fail("fs_usage could not run (permission denied)").build();
|
|
1237
|
+
}
|
|
1238
|
+
const ev = b.evidenceOf("syscall_stat", `0 syscalls in ${duration}s`, { name: "syscalls", count: 0 });
|
|
1239
|
+
b.finding("info", `no file/network syscalls in ${duration}s \u2014 idle, or spinning in user space`, [ev], "inferred");
|
|
1240
|
+
return b.build();
|
|
1241
|
+
}
|
|
1242
|
+
summarizeSyscallRecords(b, parseFsUsage(raw), duration, art.path);
|
|
1243
|
+
b.limitation(`fs_usage shows file and network calls only (filters: ${filters}); futex/select/kevent waits are not included \u2014 use dump_stacks for those`);
|
|
1244
|
+
if (r.outputCapped) b.limitation(`raw log hit the ${LIMITS.MAX_RAW_BYTES / 1024 / 1024} MB cap`);
|
|
1245
|
+
b.next("profile_cpu", "inspect_sockets", "inspect_files");
|
|
1246
|
+
return b.build();
|
|
1247
|
+
}
|
|
1248
|
+
var fsUsageAdapter = {
|
|
1249
|
+
name: "fs_usage",
|
|
1250
|
+
tools: ["trace_syscalls"],
|
|
1251
|
+
platforms: ["darwin"],
|
|
1252
|
+
requires: ["fs_usage"],
|
|
1253
|
+
install: { darwin: "preinstalled (/usr/bin/fs_usage); needs sudo" },
|
|
1254
|
+
priority: 5,
|
|
1255
|
+
check(base, env) {
|
|
1256
|
+
if (!env.isRoot) base.notes.push("needs root: Debug Anything will print the sudo command and you can `debuganything import fs_usage <file>`");
|
|
1257
|
+
return base;
|
|
1258
|
+
},
|
|
1259
|
+
async run(tool, args, ctx) {
|
|
1260
|
+
if (tool !== "trace_syscalls") throw new Error(`fs_usage adapter cannot run ${tool}`);
|
|
1261
|
+
return traceSyscalls2(ctx, args);
|
|
1262
|
+
}
|
|
1263
|
+
};
|
|
1264
|
+
|
|
1265
|
+
// src/adapters/jvm.ts
|
|
1266
|
+
function parseJstack(raw) {
|
|
1267
|
+
const threads = [];
|
|
1268
|
+
let cur;
|
|
1269
|
+
let deadlock;
|
|
1270
|
+
const lines = raw.split("\n");
|
|
1271
|
+
for (const line of lines) {
|
|
1272
|
+
if (/^Found .* Java-level deadlock/i.test(line)) deadlock = [line.trim()];
|
|
1273
|
+
else if (deadlock && deadlock.length < 40 && line.trim()) deadlock.push(line.trim());
|
|
1274
|
+
const head = line.match(/^"(.+)"\s+(#(\d+)\s+)?(daemon\s+)?prio=/) ?? line.match(/^"(.+)"\s+(#(\d+))?.*(daemon)?.*\bnid=/);
|
|
1275
|
+
if (head) {
|
|
1276
|
+
cur = { name: head[1], id: head[3] ? Number(head[3]) : void 0, daemon: /\bdaemon\b/.test(line), frames: [], holding: [] };
|
|
1277
|
+
threads.push(cur);
|
|
1278
|
+
continue;
|
|
1279
|
+
}
|
|
1280
|
+
if (!cur) continue;
|
|
1281
|
+
const st = line.match(/^\s+java\.lang\.Thread\.State:\s+(\w+)/);
|
|
1282
|
+
if (st) {
|
|
1283
|
+
cur.state = st[1];
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1286
|
+
const at = line.match(/^\s+at\s+([\w.$/<>]+)\((.*?)\)/);
|
|
1287
|
+
if (at) {
|
|
1288
|
+
cur.frames.push({ func: at[1], loc: at[2] && at[2] !== "Native Method" ? at[2] : void 0 });
|
|
1289
|
+
continue;
|
|
1290
|
+
}
|
|
1291
|
+
const wait = line.match(/^\s+-\s+(?:waiting to lock|parking to wait for|waiting on)\s+<(0x[0-9a-f]+)>/);
|
|
1292
|
+
if (wait) {
|
|
1293
|
+
cur.waitingToLock = wait[1];
|
|
1294
|
+
continue;
|
|
1295
|
+
}
|
|
1296
|
+
const locked = line.match(/^\s+-\s+locked\s+<(0x[0-9a-f]+)>/);
|
|
1297
|
+
if (locked) cur.holding.push(locked[1]);
|
|
1298
|
+
}
|
|
1299
|
+
return { threads: threads.filter((t) => t.frames.length || t.state), deadlockText: deadlock?.join("\n") };
|
|
1300
|
+
}
|
|
1301
|
+
async function summarizeJavaStacks(b, dump, artifact) {
|
|
1302
|
+
const ts = dump.threads;
|
|
1303
|
+
if (!ts.length) {
|
|
1304
|
+
b.limitation("no Java threads parsed (expected `jstack <pid>` or `jcmd <pid> Thread.print` output)");
|
|
1305
|
+
return b.fail("jstack: no threads parsed").build();
|
|
1306
|
+
}
|
|
1307
|
+
const byState = /* @__PURE__ */ new Map();
|
|
1308
|
+
for (const t of ts) {
|
|
1309
|
+
const s = t.state ?? "UNKNOWN";
|
|
1310
|
+
byState.set(s, [...byState.get(s) ?? [], t]);
|
|
1311
|
+
}
|
|
1312
|
+
const blocked = byState.get("BLOCKED") ?? [];
|
|
1313
|
+
const runnable = byState.get("RUNNABLE") ?? [];
|
|
1314
|
+
const summaryEv = b.evidenceOf(
|
|
1315
|
+
"stack_summary",
|
|
1316
|
+
`${ts.length} JVM thread(s): ${[...byState].map(([s, v]) => `${s}=${v.length}`).join(" ")}`,
|
|
1317
|
+
ts.slice(0, 200).map((t) => ({ name: t.name, state: t.state, daemon: t.daemon, top: t.frames[0]?.func })),
|
|
1318
|
+
{ artifact }
|
|
1319
|
+
);
|
|
1320
|
+
for (const t of ts.filter((x) => x.state === "BLOCKED" || x.name === "main").slice(0, 10)) {
|
|
1321
|
+
b.evidenceOf("stack", `${t.name} [${t.state}]: ${t.frames.slice(0, 5).map((f) => f.func.split(".").slice(-2).join(".")).join(" \u2190 ")}`, { name: t.name, state: t.state, waiting_to_lock: t.waitingToLock, holding: t.holding, frames: t.frames.slice(0, 20).map((f) => `${f.func}${f.loc ? ` (${f.loc})` : ""}`) }, { artifact });
|
|
1322
|
+
}
|
|
1323
|
+
if (dump.deadlockText) {
|
|
1324
|
+
const dlEv = b.evidenceOf("lock_wait", dump.deadlockText.split("\n")[0], { jstack_deadlock_section: dump.deadlockText }, { artifact });
|
|
1325
|
+
b.finding("high", `the JVM itself reports a deadlock: ${dump.deadlockText.split("\n")[0]}`, [dlEv, summaryEv], "observed", dump.deadlockText);
|
|
1326
|
+
} else if (blocked.length >= 2) {
|
|
1327
|
+
const holders = /* @__PURE__ */ new Map();
|
|
1328
|
+
for (const t of ts) for (const m of t.holding) holders.set(m, t);
|
|
1329
|
+
const cycles = blocked.filter((t) => {
|
|
1330
|
+
const holder = t.waitingToLock ? holders.get(t.waitingToLock) : void 0;
|
|
1331
|
+
return holder && holder.waitingToLock && holders.get(holder.waitingToLock)?.name === t.name;
|
|
1332
|
+
});
|
|
1333
|
+
const lockEv = b.evidenceOf("lock_wait", `${blocked.length} BLOCKED thread(s): ${blocked.slice(0, 6).map((t) => `${t.name} wants <${t.waitingToLock ?? "?"}> at ${t.frames[0]?.func ?? "?"}`).join("; ")}`, blocked.slice(0, 30).map((t) => ({ name: t.name, wants: t.waitingToLock, holds: t.holding, at: t.frames[0]?.func })), { artifact });
|
|
1334
|
+
if (cycles.length) b.finding("high", `deadlock: circular monitor wait between ${cycles.map((t) => t.name).join(" and ")}`, [lockEv, summaryEv], "inferred", "Each thread holds the monitor the other wants.");
|
|
1335
|
+
else {
|
|
1336
|
+
const wantCounts = /* @__PURE__ */ new Map();
|
|
1337
|
+
for (const t of blocked) if (t.waitingToLock) wantCounts.set(t.waitingToLock, (wantCounts.get(t.waitingToLock) ?? 0) + 1);
|
|
1338
|
+
const hot = [...wantCounts].sort((a, c) => c[1] - a[1])[0];
|
|
1339
|
+
if (hot && hot[1] >= 3) {
|
|
1340
|
+
const holder = holders.get(hot[0]);
|
|
1341
|
+
b.finding("high", `monitor contention: ${hot[1]} threads blocked on <${hot[0]}>${holder ? `, held by ${holder.name} at ${holder.frames[0]?.func ?? "?"}` : ""}`, [lockEv], "observed");
|
|
1342
|
+
} else b.finding("warn", `${blocked.length} thread(s) BLOCKED on monitors`, [lockEv], "observed");
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
const main = ts.find((t) => t.name === "main");
|
|
1346
|
+
if (main && (main.state === "WAITING" || main.state === "TIMED_WAITING") && !dump.deadlockText) {
|
|
1347
|
+
const ev = b.evidenceOf("stack", `main is ${main.state} in ${main.frames[0]?.func ?? "?"}`, main.frames.slice(0, 10).map((f) => f.func), { artifact });
|
|
1348
|
+
b.finding("info", `main thread is ${main.state} in ${main.frames[0]?.func ?? "?"} \u2014 normal for servers; relevant if this process should be doing work`, [ev]);
|
|
1349
|
+
}
|
|
1350
|
+
if (ts.length >= 500) b.finding("warn", `${ts.length} threads \u2014 likely an unbounded thread pool or a thread leak`, [summaryEv], "inferred");
|
|
1351
|
+
if (!b.findingCount) b.finding("info", `${ts.length} threads, ${runnable.length} RUNNABLE, no monitor contention visible`, [summaryEv]);
|
|
1352
|
+
b.next("inspect_sockets", "snapshot_memory", "profile_cpu");
|
|
1353
|
+
return b.build();
|
|
1354
|
+
}
|
|
1355
|
+
async function liveDump(ctx) {
|
|
1356
|
+
const { session, target } = ctx;
|
|
1357
|
+
const b = new ResultBuilder("dump_stacks", session, target);
|
|
1358
|
+
const pid = target.pid;
|
|
1359
|
+
if (!pid) return b.fail("no resolved pid for target").build();
|
|
1360
|
+
const bad = (x) => x.exitCode !== 0 || !/java\.lang\.Thread\.State:|Full thread dump/.test(x.stdout);
|
|
1361
|
+
let r = await runCapped("jcmd", [String(pid), "Thread.print", "-l"], { timeoutMs: 2e4 });
|
|
1362
|
+
if (bad(r)) r = await runCapped("jstack", ["-l", String(pid)], { timeoutMs: 2e4 });
|
|
1363
|
+
if (bad(r)) {
|
|
1364
|
+
if (/Operation not permitted|AttachNotSupported|well-known file is not secure/i.test(r.stderr + r.stdout)) {
|
|
1365
|
+
b.limitation(rootRequiredLimitation("dump_stacks", `jstack -l ${pid}`));
|
|
1366
|
+
b.limitation("the JVM attach mechanism requires the same user as the target (or root)");
|
|
1367
|
+
b.next(`import_artifact (tool: jstack) after running it yourself`);
|
|
1368
|
+
return b.fail("jstack/jcmd could not attach").build();
|
|
1369
|
+
}
|
|
1370
|
+
return b.fail(`jcmd/jstack failed: ${(r.stderr || r.stdout).slice(0, 300)}`).build();
|
|
1371
|
+
}
|
|
1372
|
+
const art = await b.writeArtifact("jstack.txt", r.stdout, "raw_text", "JVM thread dump");
|
|
1373
|
+
return summarizeJavaStacks(b, parseJstack(r.stdout), art);
|
|
1374
|
+
}
|
|
1375
|
+
var jvmAdapter = {
|
|
1376
|
+
name: "jstack",
|
|
1377
|
+
tools: ["dump_stacks"],
|
|
1378
|
+
platforms: ["linux", "darwin"],
|
|
1379
|
+
requires: ["jcmd"],
|
|
1380
|
+
runtimes: ["java"],
|
|
1381
|
+
install: { linux: "install a JDK (jcmd/jstack ship with it)", darwin: "brew install openjdk" },
|
|
1382
|
+
priority: 20,
|
|
1383
|
+
async run(tool, _args, ctx) {
|
|
1384
|
+
if (tool !== "dump_stacks") throw new Error(`jstack adapter cannot run ${tool}`);
|
|
1385
|
+
return liveDump(ctx);
|
|
1386
|
+
}
|
|
1387
|
+
};
|
|
1388
|
+
|
|
1389
|
+
export {
|
|
1390
|
+
parsePySpyDump,
|
|
1391
|
+
summarizePySpyDump,
|
|
1392
|
+
summarizeProfile,
|
|
1393
|
+
pyspyAdapter,
|
|
1394
|
+
mergeResults,
|
|
1395
|
+
parseStrace,
|
|
1396
|
+
summarizeSyscallRecords,
|
|
1397
|
+
straceAdapter,
|
|
1398
|
+
parseGdbBacktrace,
|
|
1399
|
+
summarizeNativeStacks,
|
|
1400
|
+
gdbAdapter,
|
|
1401
|
+
parseLldbBacktrace,
|
|
1402
|
+
lldbAdapter,
|
|
1403
|
+
perfScriptToFolded,
|
|
1404
|
+
perfAdapter,
|
|
1405
|
+
parseTcpdump,
|
|
1406
|
+
summarizeCaptureEvidence,
|
|
1407
|
+
tcpdumpAdapter,
|
|
1408
|
+
sampleToFolded,
|
|
1409
|
+
sampleMacosAdapter,
|
|
1410
|
+
parseFsUsage,
|
|
1411
|
+
fsUsageAdapter,
|
|
1412
|
+
parseJstack,
|
|
1413
|
+
summarizeJavaStacks,
|
|
1414
|
+
jvmAdapter
|
|
1415
|
+
};
|
|
1416
|
+
//# sourceMappingURL=chunk-MLDOO673.js.map
|