agent-dbg 0.1.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/.bin/ndbg +0 -0
- package/.claude/settings.local.json +21 -0
- package/.claude/skills/ndbg-debugger/ndbg-debugger/SKILL.md +116 -0
- package/.claude/skills/ndbg-debugger/ndbg-debugger/references/commands.md +173 -0
- package/CLAUDE.md +43 -0
- package/PROGRESS.md +261 -0
- package/README.md +67 -0
- package/biome.json +41 -0
- package/ndbg-spec.md +958 -0
- package/package.json +30 -0
- package/src/cdp/client.ts +198 -0
- package/src/cdp/types.ts +16 -0
- package/src/cli/parser.ts +287 -0
- package/src/cli/registry.ts +7 -0
- package/src/cli/types.ts +24 -0
- package/src/commands/attach.ts +47 -0
- package/src/commands/blackbox-ls.ts +38 -0
- package/src/commands/blackbox-rm.ts +57 -0
- package/src/commands/blackbox.ts +48 -0
- package/src/commands/break-ls.ts +57 -0
- package/src/commands/break-rm.ts +40 -0
- package/src/commands/break-toggle.ts +42 -0
- package/src/commands/break.ts +145 -0
- package/src/commands/breakable.ts +69 -0
- package/src/commands/catch.ts +38 -0
- package/src/commands/console.ts +61 -0
- package/src/commands/continue.ts +46 -0
- package/src/commands/eval.ts +70 -0
- package/src/commands/exceptions.ts +61 -0
- package/src/commands/hotpatch.ts +67 -0
- package/src/commands/launch.ts +69 -0
- package/src/commands/logpoint.ts +78 -0
- package/src/commands/pause.ts +46 -0
- package/src/commands/props.ts +77 -0
- package/src/commands/restart-frame.ts +36 -0
- package/src/commands/run-to.ts +70 -0
- package/src/commands/scripts.ts +57 -0
- package/src/commands/search.ts +73 -0
- package/src/commands/sessions.ts +71 -0
- package/src/commands/set-return.ts +49 -0
- package/src/commands/set.ts +61 -0
- package/src/commands/source.ts +59 -0
- package/src/commands/sourcemap.ts +66 -0
- package/src/commands/stack.ts +64 -0
- package/src/commands/state.ts +124 -0
- package/src/commands/status.ts +57 -0
- package/src/commands/step.ts +50 -0
- package/src/commands/stop.ts +27 -0
- package/src/commands/vars.ts +71 -0
- package/src/daemon/client.ts +147 -0
- package/src/daemon/entry.ts +242 -0
- package/src/daemon/paths.ts +26 -0
- package/src/daemon/server.ts +185 -0
- package/src/daemon/session-blackbox.ts +41 -0
- package/src/daemon/session-breakpoints.ts +492 -0
- package/src/daemon/session-execution.ts +121 -0
- package/src/daemon/session-inspection.ts +701 -0
- package/src/daemon/session-mutation.ts +197 -0
- package/src/daemon/session-state.ts +258 -0
- package/src/daemon/session.ts +938 -0
- package/src/daemon/spawn.ts +53 -0
- package/src/formatter/errors.ts +15 -0
- package/src/formatter/source.ts +74 -0
- package/src/formatter/stack.ts +70 -0
- package/src/formatter/values.ts +269 -0
- package/src/formatter/variables.ts +20 -0
- package/src/main.ts +45 -0
- package/src/protocol/messages.ts +316 -0
- package/src/refs/ref-table.ts +120 -0
- package/src/refs/resolver.ts +24 -0
- package/src/sourcemap/resolver.ts +318 -0
- package/tests/fixtures/async-app.js +34 -0
- package/tests/fixtures/console-app.js +12 -0
- package/tests/fixtures/error-app.js +28 -0
- package/tests/fixtures/exception-app.js +6 -0
- package/tests/fixtures/inspect-app.js +10 -0
- package/tests/fixtures/mutation-app.js +9 -0
- package/tests/fixtures/simple-app.js +50 -0
- package/tests/fixtures/step-app.js +13 -0
- package/tests/fixtures/ts-app/src/app.ts +21 -0
- package/tests/fixtures/ts-app/tsconfig.json +14 -0
- package/tests/integration/blackbox.test.ts +135 -0
- package/tests/integration/break-extras.test.ts +241 -0
- package/tests/integration/breakpoint.test.ts +217 -0
- package/tests/integration/console.test.ts +275 -0
- package/tests/integration/execution.test.ts +247 -0
- package/tests/integration/inspection.test.ts +311 -0
- package/tests/integration/mutation.test.ts +178 -0
- package/tests/integration/session.test.ts +223 -0
- package/tests/integration/source.test.ts +209 -0
- package/tests/integration/sourcemap.test.ts +214 -0
- package/tests/integration/state.test.ts +208 -0
- package/tests/unit/cdp-client.test.ts +422 -0
- package/tests/unit/daemon.test.ts +286 -0
- package/tests/unit/formatter.test.ts +716 -0
- package/tests/unit/parser.test.ts +105 -0
- package/tests/unit/refs.test.ts +383 -0
- package/tests/unit/sourcemap.test.ts +236 -0
- package/tsconfig.json +32 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { type DaemonResponse, DaemonResponseSchema } from "../protocol/messages.ts";
|
|
3
|
+
import { getSocketDir, getSocketPath } from "./paths.ts";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
6
|
+
|
|
7
|
+
export class DaemonClient {
|
|
8
|
+
private session: string;
|
|
9
|
+
private socketPath: string;
|
|
10
|
+
|
|
11
|
+
constructor(session: string) {
|
|
12
|
+
this.session = session;
|
|
13
|
+
this.socketPath = getSocketPath(session);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async request(cmd: string, args: Record<string, unknown> = {}): Promise<DaemonResponse> {
|
|
17
|
+
const message = `${JSON.stringify({ cmd, args })}\n`;
|
|
18
|
+
const sessionName = this.session;
|
|
19
|
+
const socketPath = this.socketPath;
|
|
20
|
+
|
|
21
|
+
return new Promise<DaemonResponse>((resolve, reject) => {
|
|
22
|
+
let buffer = "";
|
|
23
|
+
let settled = false;
|
|
24
|
+
|
|
25
|
+
const timer = setTimeout(() => {
|
|
26
|
+
if (!settled) {
|
|
27
|
+
settled = true;
|
|
28
|
+
reject(new Error(`Request timed out after ${DEFAULT_TIMEOUT_MS}ms`));
|
|
29
|
+
}
|
|
30
|
+
}, DEFAULT_TIMEOUT_MS);
|
|
31
|
+
|
|
32
|
+
Bun.connect<undefined>({
|
|
33
|
+
unix: socketPath,
|
|
34
|
+
socket: {
|
|
35
|
+
open(socket) {
|
|
36
|
+
socket.write(message);
|
|
37
|
+
},
|
|
38
|
+
data(_socket, data) {
|
|
39
|
+
buffer += data.toString();
|
|
40
|
+
const newlineIdx = buffer.indexOf("\n");
|
|
41
|
+
if (newlineIdx !== -1) {
|
|
42
|
+
const line = buffer.slice(0, newlineIdx);
|
|
43
|
+
if (!settled) {
|
|
44
|
+
settled = true;
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
try {
|
|
47
|
+
const parsed = DaemonResponseSchema.safeParse(JSON.parse(line));
|
|
48
|
+
if (!parsed.success) {
|
|
49
|
+
reject(new Error("Invalid response from daemon"));
|
|
50
|
+
} else {
|
|
51
|
+
resolve(parsed.data);
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
reject(new Error("Invalid JSON response from daemon"));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
close() {
|
|
60
|
+
if (!settled) {
|
|
61
|
+
settled = true;
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
if (buffer.trim()) {
|
|
64
|
+
try {
|
|
65
|
+
const parsed = DaemonResponseSchema.safeParse(JSON.parse(buffer.trim()));
|
|
66
|
+
if (!parsed.success) {
|
|
67
|
+
reject(new Error("Invalid response from daemon"));
|
|
68
|
+
} else {
|
|
69
|
+
resolve(parsed.data);
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
reject(new Error("Invalid JSON response from daemon"));
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
reject(new Error("Connection closed without response"));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
error(_socket, error) {
|
|
80
|
+
if (!settled) {
|
|
81
|
+
settled = true;
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
reject(error);
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
connectError(_socket, error) {
|
|
87
|
+
if (!settled) {
|
|
88
|
+
settled = true;
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
reject(
|
|
91
|
+
new Error(`Daemon not running for session "${sessionName}": ${error.message}`),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
}).catch((err) => {
|
|
97
|
+
if (!settled) {
|
|
98
|
+
settled = true;
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
reject(
|
|
101
|
+
new Error(
|
|
102
|
+
`Cannot connect to daemon for session "${sessionName}": ${err instanceof Error ? err.message : String(err)}`,
|
|
103
|
+
),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
static isRunning(session: string): boolean {
|
|
111
|
+
const socketPath = getSocketPath(session);
|
|
112
|
+
if (!existsSync(socketPath)) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
// Try connecting to verify the daemon is actually alive
|
|
116
|
+
try {
|
|
117
|
+
// Use a sync approach: check if socket file exists
|
|
118
|
+
// A true liveness check requires async connection, so we check the file
|
|
119
|
+
return true;
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
static async isAlive(session: string): Promise<boolean> {
|
|
126
|
+
const socketPath = getSocketPath(session);
|
|
127
|
+
if (!existsSync(socketPath)) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const client = new DaemonClient(session);
|
|
132
|
+
const response = await client.request("ping");
|
|
133
|
+
return response.ok === true;
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
static listSessions(): string[] {
|
|
140
|
+
const dir = getSocketDir();
|
|
141
|
+
if (!existsSync(dir)) {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
const files = readdirSync(dir);
|
|
145
|
+
return files.filter((f) => f.endsWith(".sock")).map((f) => f.slice(0, -5));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import type { DaemonRequest, DaemonResponse } from "../protocol/messages.ts";
|
|
2
|
+
import { DaemonServer } from "./server.ts";
|
|
3
|
+
import { DebugSession } from "./session.ts";
|
|
4
|
+
|
|
5
|
+
// Session name follows --daemon in argv
|
|
6
|
+
const daemonIdx = process.argv.indexOf("--daemon");
|
|
7
|
+
const session = daemonIdx !== -1 ? process.argv[daemonIdx + 1] : process.argv[2];
|
|
8
|
+
if (!session) {
|
|
9
|
+
console.error("Usage: ndbg --daemon <session> [--timeout <seconds>]");
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let timeout = 300; // default 5 minutes
|
|
14
|
+
const timeoutIdx = process.argv.indexOf("--timeout");
|
|
15
|
+
if (timeoutIdx !== -1) {
|
|
16
|
+
const val = process.argv[timeoutIdx + 1];
|
|
17
|
+
if (val) {
|
|
18
|
+
timeout = parseInt(val, 10);
|
|
19
|
+
if (Number.isNaN(timeout) || timeout < 0) {
|
|
20
|
+
timeout = 300;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const server = new DaemonServer(session, { idleTimeout: timeout });
|
|
26
|
+
const debugSession = new DebugSession(session);
|
|
27
|
+
|
|
28
|
+
server.onRequest(async (req: DaemonRequest): Promise<DaemonResponse> => {
|
|
29
|
+
switch (req.cmd) {
|
|
30
|
+
case "ping":
|
|
31
|
+
return { ok: true, data: "pong" };
|
|
32
|
+
|
|
33
|
+
case "launch": {
|
|
34
|
+
const { command, brk = true, port } = req.args;
|
|
35
|
+
const result = await debugSession.launch(command, { brk, port });
|
|
36
|
+
return { ok: true, data: result };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
case "attach": {
|
|
40
|
+
const { target } = req.args;
|
|
41
|
+
const result = await debugSession.attach(target);
|
|
42
|
+
return { ok: true, data: result };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
case "status":
|
|
46
|
+
return { ok: true, data: debugSession.getStatus() };
|
|
47
|
+
|
|
48
|
+
case "state": {
|
|
49
|
+
const stateResult = await debugSession.buildState(req.args);
|
|
50
|
+
return { ok: true, data: stateResult };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
case "continue": {
|
|
54
|
+
await debugSession.continue();
|
|
55
|
+
return { ok: true, data: debugSession.getStatus() };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
case "step": {
|
|
59
|
+
const { mode = "over" } = req.args;
|
|
60
|
+
await debugSession.step(mode);
|
|
61
|
+
return { ok: true, data: debugSession.getStatus() };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
case "pause": {
|
|
65
|
+
await debugSession.pause();
|
|
66
|
+
return { ok: true, data: debugSession.getStatus() };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
case "run-to": {
|
|
70
|
+
const { file, line } = req.args;
|
|
71
|
+
await debugSession.runTo(file, line);
|
|
72
|
+
return { ok: true, data: debugSession.getStatus() };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
case "break": {
|
|
76
|
+
const { file, line, condition, hitCount, urlRegex } = req.args;
|
|
77
|
+
const bpResult = await debugSession.setBreakpoint(file, line, {
|
|
78
|
+
condition,
|
|
79
|
+
hitCount,
|
|
80
|
+
urlRegex,
|
|
81
|
+
});
|
|
82
|
+
return { ok: true, data: bpResult };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
case "break-rm": {
|
|
86
|
+
const { ref } = req.args;
|
|
87
|
+
if (ref === "all") {
|
|
88
|
+
await debugSession.removeAllBreakpoints();
|
|
89
|
+
return { ok: true, data: "all removed" };
|
|
90
|
+
}
|
|
91
|
+
await debugSession.removeBreakpoint(ref);
|
|
92
|
+
return { ok: true, data: "removed" };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
case "break-ls":
|
|
96
|
+
return { ok: true, data: debugSession.listBreakpoints() };
|
|
97
|
+
|
|
98
|
+
case "logpoint": {
|
|
99
|
+
const { file, line, template, condition, maxEmissions } = req.args;
|
|
100
|
+
const lpResult = await debugSession.setLogpoint(file, line, template, {
|
|
101
|
+
condition,
|
|
102
|
+
maxEmissions,
|
|
103
|
+
});
|
|
104
|
+
return { ok: true, data: lpResult };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
case "catch": {
|
|
108
|
+
const { mode } = req.args;
|
|
109
|
+
await debugSession.setExceptionPause(mode);
|
|
110
|
+
return { ok: true, data: mode };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
case "source": {
|
|
114
|
+
const sourceResult = await debugSession.getSource(req.args);
|
|
115
|
+
return { ok: true, data: sourceResult };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
case "scripts": {
|
|
119
|
+
const { filter } = req.args;
|
|
120
|
+
const scriptsResult = debugSession.getScripts(filter);
|
|
121
|
+
return { ok: true, data: scriptsResult };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
case "stack": {
|
|
125
|
+
const stackResult = debugSession.getStack(req.args);
|
|
126
|
+
return { ok: true, data: stackResult };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
case "search": {
|
|
130
|
+
const { query, ...searchOptions } = req.args;
|
|
131
|
+
const searchResult = await debugSession.searchInScripts(query, searchOptions);
|
|
132
|
+
return { ok: true, data: searchResult };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
case "console": {
|
|
136
|
+
const consoleResult = debugSession.getConsoleMessages(req.args);
|
|
137
|
+
return { ok: true, data: consoleResult };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
case "exceptions": {
|
|
141
|
+
const exceptionsResult = debugSession.getExceptions(req.args);
|
|
142
|
+
return { ok: true, data: exceptionsResult };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
case "eval": {
|
|
146
|
+
const { expression, ...evalOptions } = req.args;
|
|
147
|
+
const evalResult = await debugSession.eval(expression, evalOptions);
|
|
148
|
+
return { ok: true, data: evalResult };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
case "vars": {
|
|
152
|
+
const varsResult = await debugSession.getVars(req.args);
|
|
153
|
+
return { ok: true, data: varsResult };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
case "props": {
|
|
157
|
+
const { ref, ...propsOptions } = req.args;
|
|
158
|
+
const propsResult = await debugSession.getProps(ref, propsOptions);
|
|
159
|
+
return { ok: true, data: propsResult };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
case "blackbox": {
|
|
163
|
+
const { patterns } = req.args;
|
|
164
|
+
const result = await debugSession.addBlackbox(patterns);
|
|
165
|
+
return { ok: true, data: result };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
case "blackbox-ls": {
|
|
169
|
+
return { ok: true, data: debugSession.listBlackbox() };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
case "blackbox-rm": {
|
|
173
|
+
const { patterns } = req.args;
|
|
174
|
+
const result = await debugSession.removeBlackbox(patterns);
|
|
175
|
+
return { ok: true, data: result };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
case "set": {
|
|
179
|
+
const { name, value, frame } = req.args;
|
|
180
|
+
const result = await debugSession.setVariable(name, value, { frame });
|
|
181
|
+
return { ok: true, data: result };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
case "set-return": {
|
|
185
|
+
const { value } = req.args;
|
|
186
|
+
const result = await debugSession.setReturnValue(value);
|
|
187
|
+
return { ok: true, data: result };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
case "hotpatch": {
|
|
191
|
+
const { file, source, dryRun } = req.args;
|
|
192
|
+
const result = await debugSession.hotpatch(file, source, { dryRun });
|
|
193
|
+
return { ok: true, data: result };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
case "break-toggle": {
|
|
197
|
+
const { ref } = req.args;
|
|
198
|
+
const toggleResult = await debugSession.toggleBreakpoint(ref);
|
|
199
|
+
return { ok: true, data: toggleResult };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
case "breakable": {
|
|
203
|
+
const { file, startLine, endLine } = req.args;
|
|
204
|
+
const breakableResult = await debugSession.getBreakableLocations(file, startLine, endLine);
|
|
205
|
+
return { ok: true, data: breakableResult };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
case "restart-frame": {
|
|
209
|
+
const { frameRef } = req.args;
|
|
210
|
+
const restartResult = await debugSession.restartFrame(frameRef);
|
|
211
|
+
return { ok: true, data: restartResult };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
case "sourcemap": {
|
|
215
|
+
const { file: smFile } = req.args;
|
|
216
|
+
if (smFile) {
|
|
217
|
+
const match = debugSession.sourceMapResolver.findScriptForSource(smFile);
|
|
218
|
+
if (match) {
|
|
219
|
+
const info = debugSession.sourceMapResolver.getInfo(match.scriptId);
|
|
220
|
+
return { ok: true, data: info ? [info] : [] };
|
|
221
|
+
}
|
|
222
|
+
return { ok: true, data: [] };
|
|
223
|
+
}
|
|
224
|
+
return { ok: true, data: debugSession.sourceMapResolver.getAllInfos() };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
case "sourcemap-disable": {
|
|
228
|
+
debugSession.sourceMapResolver.setDisabled(true);
|
|
229
|
+
return { ok: true, data: "disabled" };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
case "stop":
|
|
233
|
+
await debugSession.stop();
|
|
234
|
+
setTimeout(() => {
|
|
235
|
+
server.stop();
|
|
236
|
+
process.exit(0);
|
|
237
|
+
}, 50);
|
|
238
|
+
return { ok: true, data: "stopped" };
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
await server.start();
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export function getSocketDir(): string {
|
|
5
|
+
const xdgRuntime = process.env.XDG_RUNTIME_DIR;
|
|
6
|
+
if (xdgRuntime) {
|
|
7
|
+
return join(xdgRuntime, "ndbg");
|
|
8
|
+
}
|
|
9
|
+
const tmpdir = process.env.TMPDIR || "/tmp";
|
|
10
|
+
return join(tmpdir, `ndbg-${process.getuid?.() ?? 0}`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getSocketPath(session: string): string {
|
|
14
|
+
return join(getSocketDir(), `${session}.sock`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getLockPath(session: string): string {
|
|
18
|
+
return join(getSocketDir(), `${session}.lock`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ensureSocketDir(): void {
|
|
22
|
+
const dir = getSocketDir();
|
|
23
|
+
if (!existsSync(dir)) {
|
|
24
|
+
mkdirSync(dir, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { existsSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
type DaemonRequest,
|
|
4
|
+
DaemonRequestSchema,
|
|
5
|
+
type DaemonResponse,
|
|
6
|
+
} from "../protocol/messages.ts";
|
|
7
|
+
import { ensureSocketDir, getLockPath, getSocketPath } from "./paths.ts";
|
|
8
|
+
|
|
9
|
+
type RequestHandler = (req: DaemonRequest) => Promise<DaemonResponse>;
|
|
10
|
+
|
|
11
|
+
export class DaemonServer {
|
|
12
|
+
private session: string;
|
|
13
|
+
private idleTimeout: number;
|
|
14
|
+
private idleTimer: ReturnType<typeof setTimeout> | null = null;
|
|
15
|
+
private handler: RequestHandler | null = null;
|
|
16
|
+
private listener: ReturnType<typeof Bun.listen> | null = null;
|
|
17
|
+
private socketPath: string;
|
|
18
|
+
private lockPath: string;
|
|
19
|
+
|
|
20
|
+
constructor(session: string, options: { idleTimeout: number }) {
|
|
21
|
+
this.session = session;
|
|
22
|
+
this.idleTimeout = options.idleTimeout;
|
|
23
|
+
this.socketPath = getSocketPath(session);
|
|
24
|
+
this.lockPath = getLockPath(session);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
onRequest(handler: RequestHandler): void {
|
|
28
|
+
this.handler = handler;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async start(): Promise<void> {
|
|
32
|
+
ensureSocketDir();
|
|
33
|
+
|
|
34
|
+
// Check for existing lock file with a running process
|
|
35
|
+
if (existsSync(this.lockPath)) {
|
|
36
|
+
const existingPid = parseInt(await Bun.file(this.lockPath).text(), 10);
|
|
37
|
+
if (!Number.isNaN(existingPid) && isProcessRunning(existingPid)) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Daemon already running for session "${this.session}" (pid ${existingPid})`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
// Stale lock file, clean up
|
|
43
|
+
unlinkSync(this.lockPath);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Remove stale socket file
|
|
47
|
+
if (existsSync(this.socketPath)) {
|
|
48
|
+
unlinkSync(this.socketPath);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Write lock file with our PID
|
|
52
|
+
writeFileSync(this.lockPath, String(process.pid));
|
|
53
|
+
|
|
54
|
+
const server = this;
|
|
55
|
+
|
|
56
|
+
this.listener = Bun.listen<{ buffer: string }>({
|
|
57
|
+
unix: this.socketPath,
|
|
58
|
+
socket: {
|
|
59
|
+
open(socket) {
|
|
60
|
+
socket.data = { buffer: "" };
|
|
61
|
+
server.resetIdleTimer();
|
|
62
|
+
},
|
|
63
|
+
data(socket, data) {
|
|
64
|
+
socket.data.buffer += data.toString();
|
|
65
|
+
const newlineIdx = socket.data.buffer.indexOf("\n");
|
|
66
|
+
if (newlineIdx === -1) return;
|
|
67
|
+
|
|
68
|
+
const line = socket.data.buffer.slice(0, newlineIdx);
|
|
69
|
+
socket.data.buffer = socket.data.buffer.slice(newlineIdx + 1);
|
|
70
|
+
|
|
71
|
+
server.handleMessage(socket, line);
|
|
72
|
+
},
|
|
73
|
+
close() {},
|
|
74
|
+
error(_socket, error) {
|
|
75
|
+
console.error(`[daemon] socket error: ${error.message}`);
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
this.resetIdleTimer();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private handleMessage(
|
|
84
|
+
socket: { write(data: string | Buffer | Uint8Array): number; end(): void },
|
|
85
|
+
line: string,
|
|
86
|
+
): void {
|
|
87
|
+
let json: unknown;
|
|
88
|
+
try {
|
|
89
|
+
json = JSON.parse(line);
|
|
90
|
+
} catch {
|
|
91
|
+
const errResponse: DaemonResponse = {
|
|
92
|
+
ok: false,
|
|
93
|
+
error: "Invalid JSON",
|
|
94
|
+
};
|
|
95
|
+
socket.write(`${JSON.stringify(errResponse)}\n`);
|
|
96
|
+
socket.end();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const parsed = DaemonRequestSchema.safeParse(json);
|
|
101
|
+
if (!parsed.success) {
|
|
102
|
+
const obj = json as Record<string, unknown> | null;
|
|
103
|
+
const cmd =
|
|
104
|
+
obj && typeof obj === "object" && typeof obj.cmd === "string" ? obj.cmd : undefined;
|
|
105
|
+
const errResponse: DaemonResponse = cmd
|
|
106
|
+
? {
|
|
107
|
+
ok: false,
|
|
108
|
+
error: `Unknown command: ${cmd}`,
|
|
109
|
+
suggestion: "-> Try: ndbg --help",
|
|
110
|
+
}
|
|
111
|
+
: {
|
|
112
|
+
ok: false,
|
|
113
|
+
error: "Invalid request: must have { cmd: string, args: object }",
|
|
114
|
+
};
|
|
115
|
+
socket.write(`${JSON.stringify(errResponse)}\n`);
|
|
116
|
+
socket.end();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const request: DaemonRequest = parsed.data;
|
|
120
|
+
|
|
121
|
+
if (!this.handler) {
|
|
122
|
+
const errResponse: DaemonResponse = {
|
|
123
|
+
ok: false,
|
|
124
|
+
error: "No request handler registered",
|
|
125
|
+
};
|
|
126
|
+
socket.write(`${JSON.stringify(errResponse)}\n`);
|
|
127
|
+
socket.end();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.handler(request)
|
|
132
|
+
.then((response) => {
|
|
133
|
+
socket.write(`${JSON.stringify(response)}\n`);
|
|
134
|
+
socket.end();
|
|
135
|
+
})
|
|
136
|
+
.catch((err) => {
|
|
137
|
+
const errResponse: DaemonResponse = {
|
|
138
|
+
ok: false,
|
|
139
|
+
error: err instanceof Error ? err.message : String(err),
|
|
140
|
+
};
|
|
141
|
+
socket.write(`${JSON.stringify(errResponse)}\n`);
|
|
142
|
+
socket.end();
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
resetIdleTimer(): void {
|
|
147
|
+
if (this.idleTimer) {
|
|
148
|
+
clearTimeout(this.idleTimer);
|
|
149
|
+
}
|
|
150
|
+
if (this.idleTimeout > 0) {
|
|
151
|
+
this.idleTimer = setTimeout(() => {
|
|
152
|
+
this.stop();
|
|
153
|
+
}, this.idleTimeout * 1000);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async stop(): Promise<void> {
|
|
158
|
+
if (this.idleTimer) {
|
|
159
|
+
clearTimeout(this.idleTimer);
|
|
160
|
+
this.idleTimer = null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (this.listener) {
|
|
164
|
+
this.listener.stop(true);
|
|
165
|
+
this.listener = null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Clean up socket and lock files
|
|
169
|
+
if (existsSync(this.socketPath)) {
|
|
170
|
+
unlinkSync(this.socketPath);
|
|
171
|
+
}
|
|
172
|
+
if (existsSync(this.lockPath)) {
|
|
173
|
+
unlinkSync(this.lockPath);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isProcessRunning(pid: number): boolean {
|
|
179
|
+
try {
|
|
180
|
+
process.kill(pid, 0);
|
|
181
|
+
return true;
|
|
182
|
+
} catch {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { DebugSession } from "./session.ts";
|
|
2
|
+
|
|
3
|
+
export async function addBlackbox(session: DebugSession, patterns: string[]): Promise<string[]> {
|
|
4
|
+
if (!session.cdp) {
|
|
5
|
+
throw new Error("No active debug session");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
for (const p of patterns) {
|
|
9
|
+
if (!session.blackboxPatterns.includes(p)) {
|
|
10
|
+
session.blackboxPatterns.push(p);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
await session.cdp.send("Debugger.setBlackboxPatterns", {
|
|
15
|
+
patterns: session.blackboxPatterns,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
return [...session.blackboxPatterns];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function listBlackbox(session: DebugSession): string[] {
|
|
22
|
+
return [...session.blackboxPatterns];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function removeBlackbox(session: DebugSession, patterns: string[]): Promise<string[]> {
|
|
26
|
+
if (!session.cdp) {
|
|
27
|
+
throw new Error("No active debug session");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (patterns.includes("all")) {
|
|
31
|
+
session.blackboxPatterns = [];
|
|
32
|
+
} else {
|
|
33
|
+
session.blackboxPatterns = session.blackboxPatterns.filter((p) => !patterns.includes(p));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
await session.cdp.send("Debugger.setBlackboxPatterns", {
|
|
37
|
+
patterns: session.blackboxPatterns,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return [...session.blackboxPatterns];
|
|
41
|
+
}
|