agent-dbg 0.1.2 → 0.1.4
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/settings.local.json +29 -1
- package/.claude/skills/agent-dbg/SKILL.md +2 -0
- package/.claude/skills/agent-dbg/references/commands.md +2 -1
- package/TODO.md +299 -0
- package/demo/DEMO.md +71 -0
- package/demo/order-processor.js +35 -0
- package/dist/main.js +1480 -256
- package/package.json +3 -1
- package/src/commands/attach.ts +6 -5
- package/src/commands/break-fn.ts +41 -0
- package/src/commands/launch.ts +5 -6
- package/src/commands/logs.ts +58 -16
- package/src/daemon/client.ts +27 -5
- package/src/daemon/entry.ts +94 -46
- package/src/daemon/logger.ts +51 -0
- package/src/daemon/paths.ts +4 -0
- package/src/daemon/server.ts +76 -35
- package/src/daemon/session-breakpoints.ts +2 -1
- package/src/daemon/session-mutation.ts +1 -0
- package/src/daemon/session.ts +50 -10
- package/src/daemon/spawn.ts +47 -8
- package/src/dap/client.ts +252 -0
- package/src/dap/session.ts +1151 -0
- package/src/formatter/logs.ts +15 -0
- package/src/main.ts +1 -0
- package/src/protocol/messages.ts +12 -0
- package/tests/fixtures/dap/hello +0 -0
- package/tests/fixtures/dap/hello.c +8 -0
- package/tests/fixtures/dap/hello.dSYM/Contents/Info.plist +20 -0
- package/tests/fixtures/dap/hello.dSYM/Contents/Resources/DWARF/hello +0 -0
- package/tests/fixtures/dap/hello.dSYM/Contents/Resources/Relocations/aarch64/hello.yml +5 -0
- package/tests/fixtures/hotpatch-active-fn.js +7 -0
- package/tests/integration/daemon-logging.test.ts +155 -0
- package/tests/integration/mutation.test.ts +33 -0
- package/tests/unit/daemon-logger.test.ts +117 -0
- package/tests/unit/daemon.test.ts +60 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import type { DebugProtocol } from "@vscode/debugprotocol";
|
|
2
|
+
import type { Subprocess } from "bun";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
5
|
+
|
|
6
|
+
// biome-ignore lint/suspicious/noExplicitAny: Required for handler map that stores both typed and untyped handlers
|
|
7
|
+
type AnyHandler = (...args: any[]) => void;
|
|
8
|
+
|
|
9
|
+
interface PendingRequest {
|
|
10
|
+
resolve: (result: DebugProtocol.Response) => void;
|
|
11
|
+
reject: (error: Error) => void;
|
|
12
|
+
timer: ReturnType<typeof setTimeout>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* DAP (Debug Adapter Protocol) client that communicates with a debug adapter
|
|
17
|
+
* via stdin/stdout using the DAP wire format (Content-Length headers + JSON).
|
|
18
|
+
* Mirrors CdpClient's pattern for consistency.
|
|
19
|
+
*/
|
|
20
|
+
export class DapClient {
|
|
21
|
+
private proc: Subprocess<"pipe", "pipe", "pipe">;
|
|
22
|
+
private nextSeq = 1;
|
|
23
|
+
private pending = new Map<number, PendingRequest>();
|
|
24
|
+
private listeners = new Map<string, Set<AnyHandler>>();
|
|
25
|
+
private isConnected = false;
|
|
26
|
+
private buffer = "";
|
|
27
|
+
|
|
28
|
+
private constructor(proc: Subprocess<"pipe", "pipe", "pipe">) {
|
|
29
|
+
this.proc = proc;
|
|
30
|
+
this.isConnected = true;
|
|
31
|
+
this.readLoop();
|
|
32
|
+
this.drainStderr();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Spawn a debug adapter process and return a connected DapClient.
|
|
37
|
+
* @param command - The command + args to spawn (e.g. ["lldb-dap"])
|
|
38
|
+
*/
|
|
39
|
+
static spawn(command: string[]): DapClient {
|
|
40
|
+
const [cmd, ...args] = command;
|
|
41
|
+
if (!cmd) {
|
|
42
|
+
throw new Error("DapClient.spawn: command array must not be empty");
|
|
43
|
+
}
|
|
44
|
+
const proc = Bun.spawn([cmd, ...args], {
|
|
45
|
+
stdin: "pipe",
|
|
46
|
+
stdout: "pipe",
|
|
47
|
+
stderr: "pipe",
|
|
48
|
+
});
|
|
49
|
+
return new DapClient(proc);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Send a DAP request and wait for the response.
|
|
54
|
+
*/
|
|
55
|
+
async send(command: string, args?: Record<string, unknown>): Promise<DebugProtocol.Response> {
|
|
56
|
+
if (!this.isConnected) {
|
|
57
|
+
throw new Error("DAP client is not connected");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const seq = this.nextSeq++;
|
|
61
|
+
const request: DebugProtocol.Request = {
|
|
62
|
+
seq,
|
|
63
|
+
type: "request",
|
|
64
|
+
command,
|
|
65
|
+
};
|
|
66
|
+
if (args !== undefined) {
|
|
67
|
+
request.arguments = args;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return new Promise<DebugProtocol.Response>((resolve, reject) => {
|
|
71
|
+
const timer = setTimeout(() => {
|
|
72
|
+
this.pending.delete(seq);
|
|
73
|
+
reject(new Error(`DAP request timed out: ${command} (seq=${seq})`));
|
|
74
|
+
}, DEFAULT_TIMEOUT_MS);
|
|
75
|
+
|
|
76
|
+
this.pending.set(seq, { resolve, reject, timer });
|
|
77
|
+
this.writeMessage(request);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Register an event listener for a DAP event type (e.g. "stopped", "output").
|
|
83
|
+
*/
|
|
84
|
+
on(event: string, handler: (body: unknown) => void): void {
|
|
85
|
+
let handlers = this.listeners.get(event);
|
|
86
|
+
if (!handlers) {
|
|
87
|
+
handlers = new Set();
|
|
88
|
+
this.listeners.set(event, handlers);
|
|
89
|
+
}
|
|
90
|
+
handlers.add(handler);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Remove an event listener.
|
|
95
|
+
*/
|
|
96
|
+
off(event: string, handler: (body: unknown) => void): void {
|
|
97
|
+
const handlers = this.listeners.get(event);
|
|
98
|
+
if (handlers) {
|
|
99
|
+
handlers.delete(handler);
|
|
100
|
+
if (handlers.size === 0) {
|
|
101
|
+
this.listeners.delete(event);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Disconnect from the debug adapter, killing the subprocess.
|
|
108
|
+
*/
|
|
109
|
+
disconnect(): void {
|
|
110
|
+
if (!this.isConnected) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
this.isConnected = false;
|
|
114
|
+
|
|
115
|
+
const error = new Error("DAP client disconnected");
|
|
116
|
+
for (const [id, pending] of this.pending) {
|
|
117
|
+
clearTimeout(pending.timer);
|
|
118
|
+
pending.reject(error);
|
|
119
|
+
this.pending.delete(id);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this.listeners.clear();
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
this.proc.stdin.end();
|
|
126
|
+
} catch {
|
|
127
|
+
// stdin may already be closed
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
this.proc.kill();
|
|
131
|
+
} catch {
|
|
132
|
+
// process may already be dead
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
get connected(): boolean {
|
|
137
|
+
return this.isConnected;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
get pid(): number {
|
|
141
|
+
return this.proc.pid;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── Wire format ────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
private writeMessage(msg: DebugProtocol.ProtocolMessage): void {
|
|
147
|
+
const json = JSON.stringify(msg);
|
|
148
|
+
const header = `Content-Length: ${Buffer.byteLength(json, "utf-8")}\r\n\r\n`;
|
|
149
|
+
try {
|
|
150
|
+
this.proc.stdin.write(header + json);
|
|
151
|
+
} catch {
|
|
152
|
+
this.isConnected = false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private async readLoop(): Promise<void> {
|
|
157
|
+
const reader = this.proc.stdout.getReader();
|
|
158
|
+
const decoder = new TextDecoder();
|
|
159
|
+
try {
|
|
160
|
+
while (this.isConnected) {
|
|
161
|
+
const { done, value } = await reader.read();
|
|
162
|
+
if (done) break;
|
|
163
|
+
this.buffer += decoder.decode(value, { stream: true });
|
|
164
|
+
this.processBuffer();
|
|
165
|
+
}
|
|
166
|
+
} catch {
|
|
167
|
+
// Stream closed or errored
|
|
168
|
+
} finally {
|
|
169
|
+
this.isConnected = false;
|
|
170
|
+
// Reject all pending requests
|
|
171
|
+
const error = new Error("DAP adapter process terminated");
|
|
172
|
+
for (const [id, pending] of this.pending) {
|
|
173
|
+
clearTimeout(pending.timer);
|
|
174
|
+
pending.reject(error);
|
|
175
|
+
this.pending.delete(id);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private processBuffer(): void {
|
|
181
|
+
while (true) {
|
|
182
|
+
// Look for Content-Length header
|
|
183
|
+
const headerEnd = this.buffer.indexOf("\r\n\r\n");
|
|
184
|
+
if (headerEnd === -1) return;
|
|
185
|
+
|
|
186
|
+
const header = this.buffer.slice(0, headerEnd);
|
|
187
|
+
const match = /Content-Length:\s*(\d+)/i.exec(header);
|
|
188
|
+
if (!match?.[1]) {
|
|
189
|
+
// Malformed header, skip past it
|
|
190
|
+
this.buffer = this.buffer.slice(headerEnd + 4);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const contentLength = parseInt(match[1], 10);
|
|
195
|
+
const bodyStart = headerEnd + 4;
|
|
196
|
+
const bodyEnd = bodyStart + contentLength;
|
|
197
|
+
|
|
198
|
+
// Wait for complete body
|
|
199
|
+
if (this.buffer.length < bodyEnd) return;
|
|
200
|
+
|
|
201
|
+
const body = this.buffer.slice(bodyStart, bodyEnd);
|
|
202
|
+
this.buffer = this.buffer.slice(bodyEnd);
|
|
203
|
+
|
|
204
|
+
this.handleMessage(body);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private handleMessage(data: string): void {
|
|
209
|
+
let parsed: DebugProtocol.ProtocolMessage;
|
|
210
|
+
try {
|
|
211
|
+
parsed = JSON.parse(data) as DebugProtocol.ProtocolMessage;
|
|
212
|
+
} catch {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (parsed.type === "response") {
|
|
217
|
+
const response = parsed as DebugProtocol.Response;
|
|
218
|
+
const pending = this.pending.get(response.request_seq);
|
|
219
|
+
if (!pending) return;
|
|
220
|
+
this.pending.delete(response.request_seq);
|
|
221
|
+
clearTimeout(pending.timer);
|
|
222
|
+
|
|
223
|
+
if (!response.success) {
|
|
224
|
+
pending.reject(
|
|
225
|
+
new Error(`DAP error (${response.command}): ${response.message ?? "unknown error"}`),
|
|
226
|
+
);
|
|
227
|
+
} else {
|
|
228
|
+
pending.resolve(response);
|
|
229
|
+
}
|
|
230
|
+
} else if (parsed.type === "event") {
|
|
231
|
+
const event = parsed as DebugProtocol.Event;
|
|
232
|
+
const handlers = this.listeners.get(event.event);
|
|
233
|
+
if (handlers) {
|
|
234
|
+
for (const handler of handlers) {
|
|
235
|
+
handler(event.body);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private async drainStderr(): Promise<void> {
|
|
242
|
+
const reader = this.proc.stderr.getReader();
|
|
243
|
+
try {
|
|
244
|
+
while (true) {
|
|
245
|
+
const { done } = await reader.read();
|
|
246
|
+
if (done) break;
|
|
247
|
+
}
|
|
248
|
+
} catch {
|
|
249
|
+
// Ignore
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|