autodebug 0.5.1__py3-none-any.whl
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.
- autodebug/__init__.py +3 -0
- autodebug/__main__.py +11 -0
- autodebug/cli.py +432 -0
- autodebug/connection.py +219 -0
- autodebug/extension/README.md +16 -0
- autodebug/extension/extension.js +40 -0
- autodebug/extension/mcp.js +98 -0
- autodebug/extension/package.json +28 -0
- autodebug/extension/relay.js +570 -0
- autodebug/hub.py +267 -0
- autodebug/install.py +346 -0
- autodebug/mcp_server.py +270 -0
- autodebug/redact.py +38 -0
- autodebug/report.py +107 -0
- autodebug/session.py +828 -0
- autodebug/tools.py +472 -0
- autodebug-0.5.1.dist-info/METADATA +251 -0
- autodebug-0.5.1.dist-info/RECORD +20 -0
- autodebug-0.5.1.dist-info/WHEEL +4 -0
- autodebug-0.5.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// AutoDebug extension: a thin relay between the AutoDebug server (Python) and
|
|
3
|
+
// this IDE window. The server decides what to debug. This file shows the Approve
|
|
4
|
+
// dialog, runs only what the dialog showed, and passes Debug Adapter Protocol
|
|
5
|
+
// requests and events through. See docs/design.md ("Extension protocol").
|
|
6
|
+
// The `vscode` API is passed in so this module can be tested without an IDE.
|
|
7
|
+
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const net = require('net');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
const PROTOCOL = 4;
|
|
15
|
+
const VERSION = '0.5.0';
|
|
16
|
+
const MAX_LINE = 1024 * 1024;
|
|
17
|
+
const APPROVAL_SECONDS = 120;
|
|
18
|
+
const MAX_EVENTS = 5000;
|
|
19
|
+
const MAX_CONFIG = 4000;
|
|
20
|
+
// The only debugger requests the server may send. No evaluate, no setVariable.
|
|
21
|
+
const DAP_COMMANDS = new Set(['threads', 'stackTrace', 'scopes', 'variables', 'next', 'stepIn', 'stepOut', 'continue',
|
|
22
|
+
'pause', 'exceptionInfo', 'setExceptionBreakpoints']);
|
|
23
|
+
const STEPS = new Set(['next', 'stepIn', 'stepOut']);
|
|
24
|
+
// IDE-to-debugger requests and debugger responses the server needs to see.
|
|
25
|
+
const TO_ADAPTER = new Set(['setBreakpoints', 'setFunctionBreakpoints']);
|
|
26
|
+
const RESPONSES = new Set(['initialize', 'setBreakpoints', 'setFunctionBreakpoints']);
|
|
27
|
+
const PLATFORM_KEY = { win32: 'windows', darwin: 'osx', linux: 'linux' };
|
|
28
|
+
const OPEN = ['awaiting_approval', 'building', 'starting', 'debugging'];
|
|
29
|
+
|
|
30
|
+
class RelayError extends Error {
|
|
31
|
+
constructor(code, message) { super(message); this.code = code; }
|
|
32
|
+
}
|
|
33
|
+
const fail = (code, message) => { throw new RelayError(code, message); };
|
|
34
|
+
const clamp = (value, low, fallback, high) => Math.max(low, Math.min(Number(value) || fallback, high));
|
|
35
|
+
|
|
36
|
+
function stateFolder(env = process.env, platform = process.platform, home = os.homedir()) {
|
|
37
|
+
if (env.AUTODEBUG_HOME) return env.AUTODEBUG_HOME;
|
|
38
|
+
if (platform === 'win32') return path.join(env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'), 'autodebug');
|
|
39
|
+
if (platform === 'darwin') return path.join(home, 'Library', 'Application Support', 'autodebug');
|
|
40
|
+
return path.join(env.XDG_STATE_HOME || path.join(home, '.local', 'state'), 'autodebug');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function within(root, full) {
|
|
44
|
+
const rel = path.relative(root, full);
|
|
45
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function applyPlatform(item, platform) {
|
|
49
|
+
const copy = JSON.parse(JSON.stringify(item));
|
|
50
|
+
const block = copy[PLATFORM_KEY[platform] || 'linux'];
|
|
51
|
+
delete copy.windows; delete copy.osx; delete copy.linux;
|
|
52
|
+
if (block && typeof block === 'object') Object.assign(copy, block);
|
|
53
|
+
return copy;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function taskCommand(task) {
|
|
57
|
+
const args = (task.args || []).map((arg) => (arg && typeof arg === 'object' && 'value' in arg ? arg.value : arg));
|
|
58
|
+
return [task.command, ...args].filter((part) => part != null && part !== '').join(' ');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// A task as the dialog shows it, with everything it depends on.
|
|
62
|
+
function describeTask(label, tasks, seen = new Set()) {
|
|
63
|
+
const task = tasks.find((item) => item.label === label);
|
|
64
|
+
if (!task) return `"${label}" (provided by an extension, command not in tasks.json)`;
|
|
65
|
+
const self = `"${label}": ${taskCommand(task)}`;
|
|
66
|
+
if (seen.has(label)) return self;
|
|
67
|
+
seen.add(label);
|
|
68
|
+
const deps = [].concat(task.dependsOn || []).map((dep) => (typeof dep === 'string' ? dep : dep && dep.task)).filter(Boolean);
|
|
69
|
+
return deps.length ? `${self} (first: ${deps.map((dep) => describeTask(dep, tasks, seen)).join('; ')})` : self;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class Relay {
|
|
73
|
+
constructor(vscode, { folder, log, platform = process.platform, buildMinuteMs = 60000, keepMs = 10000 } = {}) {
|
|
74
|
+
this.vscode = vscode;
|
|
75
|
+
this.folder = folder || path.join(stateFolder(), 'bridges');
|
|
76
|
+
this.log = log || (() => {});
|
|
77
|
+
this.platform = platform;
|
|
78
|
+
this.buildMinuteMs = buildMinuteMs;
|
|
79
|
+
this.keepMs = keepMs;
|
|
80
|
+
this.ide = /cursor/i.test(vscode.env.appName || '') ? 'cursor' : 'vscode';
|
|
81
|
+
const id = String(vscode.env.sessionId || crypto.randomBytes(6).toString('hex')).replace(/[^A-Za-z0-9]/g, '');
|
|
82
|
+
this.window = `${this.ide}-${id.slice(0, 12)}-${process.pid}`;
|
|
83
|
+
this.token = crypto.randomBytes(32).toString('hex');
|
|
84
|
+
this.server = null;
|
|
85
|
+
this.session = null;
|
|
86
|
+
this.sessions = 0;
|
|
87
|
+
this.queue = [];
|
|
88
|
+
this.seq = 0;
|
|
89
|
+
this.droppedOutput = 0;
|
|
90
|
+
this.waiters = [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---- lifecycle -----------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
get file() { return path.join(this.folder, `${this.window}.json`); }
|
|
96
|
+
|
|
97
|
+
folders() { return (this.vscode.workspace.workspaceFolders || []).map((f) => f.uri.fsPath); }
|
|
98
|
+
|
|
99
|
+
async start() {
|
|
100
|
+
this.server = net.createServer((socket) => this.accept(socket));
|
|
101
|
+
await new Promise((resolve, reject) => {
|
|
102
|
+
this.server.once('error', reject);
|
|
103
|
+
this.server.listen(0, '127.0.0.1', resolve);
|
|
104
|
+
});
|
|
105
|
+
this.announce();
|
|
106
|
+
this.log(`listening on 127.0.0.1:${this.server.address().port}`);
|
|
107
|
+
// If something deletes the connection file while the window is open, write it again,
|
|
108
|
+
// or the server could never find this window.
|
|
109
|
+
this.keeper = setInterval(() => {
|
|
110
|
+
if (this.server && !fs.existsSync(this.file)) {
|
|
111
|
+
this.announce();
|
|
112
|
+
this.log('the connection file was deleted; wrote it again');
|
|
113
|
+
}
|
|
114
|
+
}, this.keepMs);
|
|
115
|
+
if (this.keeper.unref) this.keeper.unref();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
announce() {
|
|
119
|
+
if (!this.server) return;
|
|
120
|
+
fs.mkdirSync(this.folder, { recursive: true, mode: 0o700 });
|
|
121
|
+
const info = {
|
|
122
|
+
protocol: PROTOCOL, port: this.server.address().port, token: this.token, window: this.window,
|
|
123
|
+
ide: this.ide, folders: this.folders(), version: VERSION,
|
|
124
|
+
};
|
|
125
|
+
const temporary = `${this.file}.tmp`;
|
|
126
|
+
fs.writeFileSync(temporary, JSON.stringify(info), { mode: 0o600 });
|
|
127
|
+
fs.renameSync(temporary, this.file);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async stop() {
|
|
131
|
+
clearInterval(this.keeper);
|
|
132
|
+
await this.end('the IDE window closed');
|
|
133
|
+
try { fs.unlinkSync(this.file); } catch (_) { /* already removed */ }
|
|
134
|
+
if (this.server) this.server.close();
|
|
135
|
+
this.server = null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---- connection ----------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
accept(socket) {
|
|
141
|
+
let buffer = '';
|
|
142
|
+
let trusted = false;
|
|
143
|
+
let queue = Promise.resolve();
|
|
144
|
+
socket.setEncoding('utf8');
|
|
145
|
+
socket.on('error', () => socket.destroy());
|
|
146
|
+
socket.on('data', (chunk) => {
|
|
147
|
+
buffer += chunk;
|
|
148
|
+
if (buffer.length > MAX_LINE) { socket.destroy(); return; }
|
|
149
|
+
let end;
|
|
150
|
+
while ((end = buffer.indexOf('\n')) >= 0) {
|
|
151
|
+
const line = buffer.slice(0, end);
|
|
152
|
+
buffer = buffer.slice(end + 1);
|
|
153
|
+
let message;
|
|
154
|
+
try { message = JSON.parse(line); } catch (_) { socket.destroy(); return; }
|
|
155
|
+
if (!trusted) {
|
|
156
|
+
const given = Buffer.from(String(message && message.hello));
|
|
157
|
+
const expected = Buffer.from(this.token);
|
|
158
|
+
if (given.length !== expected.length || !crypto.timingSafeEqual(given, expected)) { socket.destroy(); return; }
|
|
159
|
+
trusted = true;
|
|
160
|
+
socket.write(`${JSON.stringify({ hello: 'ok', protocol: PROTOCOL, window: this.window })}\n`);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
queue = queue.then(() => this.answer(socket, message));
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async answer(socket, message) {
|
|
169
|
+
let reply;
|
|
170
|
+
try {
|
|
171
|
+
reply = { id: message.id, ok: true, result: await this.handle(String(message.op), message.args || {}) };
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (!(error instanceof RelayError)) this.log(`error in ${message.op}: ${error && error.stack}`);
|
|
174
|
+
const code = error instanceof RelayError ? error.code : 'IDE_ERROR';
|
|
175
|
+
reply = { id: message.id, ok: false, error: { code, message: String(error && error.message) } };
|
|
176
|
+
}
|
|
177
|
+
if (!socket.destroyed) socket.write(`${JSON.stringify(reply)}\n`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
handle(op, args) {
|
|
181
|
+
switch (op) {
|
|
182
|
+
case 'info': return {
|
|
183
|
+
window: this.window, ide: this.ide, ide_version: this.vscode.version, version: VERSION, folders: this.folders(),
|
|
184
|
+
platform: this.platform, debugger_types: this.debuggerTypes(),
|
|
185
|
+
};
|
|
186
|
+
case 'workspace': return this.workspace(args);
|
|
187
|
+
case 'approve': return this.approve(args);
|
|
188
|
+
case 'events': return this.events(args);
|
|
189
|
+
case 'dap': return this.dap(args);
|
|
190
|
+
case 'breakpoints': return this.breakpoints(args);
|
|
191
|
+
case 'editor_breakpoints': return { breakpoints: this.editorBreakpoints() };
|
|
192
|
+
case 'notify': return this.notify(args);
|
|
193
|
+
case 'stop': return this.end(String(args.reason || 'stopped by the agent')).then(() => ({}));
|
|
194
|
+
default: return fail('BAD_REQUEST', `unknown operation ${op}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ---- workspace -------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
workspaceFolder(fsPath) {
|
|
201
|
+
const all = this.vscode.workspace.workspaceFolders || [];
|
|
202
|
+
if (!fsPath) {
|
|
203
|
+
if (all.length === 1) return all[0];
|
|
204
|
+
fail('BAD_REQUEST', 'this window has several folders; name one');
|
|
205
|
+
}
|
|
206
|
+
const norm = (p) => { const r = path.resolve(p); return this.platform === 'win32' ? r.toLowerCase() : r; };
|
|
207
|
+
const folder = all.find((f) => norm(f.uri.fsPath) === norm(fsPath));
|
|
208
|
+
if (!folder) fail('NOT_FOUND', `${fsPath} is not open in this window`);
|
|
209
|
+
return folder;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
launchConfigs(folder) {
|
|
213
|
+
return (this.vscode.workspace.getConfiguration('launch', folder.uri).get('configurations') || []).filter((c) => c && c.name);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
taskDefinitions(folder) {
|
|
217
|
+
return (this.vscode.workspace.getConfiguration('tasks', folder.uri).get('tasks') || []).filter((t) => t && t.label);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
workspace(args) {
|
|
221
|
+
const folder = this.workspaceFolder(args.folder);
|
|
222
|
+
return { folder: folder.uri.fsPath, launch: this.launchConfigs(folder), tasks: this.taskDefinitions(folder) };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Debug types that installed extensions provide; null if the IDE does not say.
|
|
226
|
+
debuggerTypes() {
|
|
227
|
+
const all = this.vscode.extensions && this.vscode.extensions.all;
|
|
228
|
+
if (!Array.isArray(all)) return null;
|
|
229
|
+
const types = new Set();
|
|
230
|
+
for (const extension of all) {
|
|
231
|
+
const debuggers = (extension.packageJSON && extension.packageJSON.contributes && extension.packageJSON.contributes.debuggers) || [];
|
|
232
|
+
debuggers.forEach((d) => d && d.type && types.add(d.type));
|
|
233
|
+
}
|
|
234
|
+
return [...types];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ---- events: everything the server needs to follow the session ------------------
|
|
238
|
+
|
|
239
|
+
emit(event) {
|
|
240
|
+
this.seq += 1;
|
|
241
|
+
this.queue.push({ seq: this.seq, ...event });
|
|
242
|
+
if (this.queue.length > MAX_EVENTS) {
|
|
243
|
+
// Program output goes first, so a flood of it never pushes out a stop or the end.
|
|
244
|
+
const output = this.queue.findIndex((e) => e.kind === 'dap' && e.message && e.message.event === 'output');
|
|
245
|
+
if (output >= 0) { this.queue.splice(output, 1); this.droppedOutput += 1; } else this.queue.shift();
|
|
246
|
+
}
|
|
247
|
+
this.waiters.splice(0).forEach((wake) => wake());
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async events(args) {
|
|
251
|
+
const since = Number(args.since) || 0;
|
|
252
|
+
const ms = clamp(args.wait_ms, 0, 0, 60000);
|
|
253
|
+
if (this.seq <= since && ms > 0) {
|
|
254
|
+
await new Promise((resolve) => {
|
|
255
|
+
const timer = setTimeout(resolve, ms);
|
|
256
|
+
this.waiters.push(() => { clearTimeout(timer); resolve(); });
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
const events = this.queue.filter((e) => e.seq > since).slice(0, 500);
|
|
260
|
+
return {
|
|
261
|
+
events, next: events.length ? events[events.length - 1].seq : Math.min(since, this.seq), latest: this.seq,
|
|
262
|
+
dropped: this.queue.length > 0 && this.queue[0].seq > since + 1 && since < this.seq
|
|
263
|
+
&& !(this.queue[0].kind === 'dap' && this.droppedOutput > 0),
|
|
264
|
+
dropped_output: this.droppedOutput,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ---- approve: the only way anything runs ------------------------------------------
|
|
269
|
+
|
|
270
|
+
approve(args) {
|
|
271
|
+
if (this.session && OPEN.includes(this.session.phase)) {
|
|
272
|
+
fail('BUSY', 'a debug session is already active in this window; stop it first');
|
|
273
|
+
}
|
|
274
|
+
const folder = this.workspaceFolder(args.folder);
|
|
275
|
+
const launch = args.launch;
|
|
276
|
+
if (!launch || typeof launch !== 'object' || !launch.name || !launch.type) fail('BAD_REQUEST', 'a launch configuration is needed');
|
|
277
|
+
if (!['launch', 'attach'].includes(launch.request)) fail('BAD_REQUEST', 'request must be launch or attach');
|
|
278
|
+
const shown = { ...launch };
|
|
279
|
+
delete shown.name;
|
|
280
|
+
const json = JSON.stringify(shown, null, 2);
|
|
281
|
+
if (json.length > MAX_CONFIG) fail('BAD_REQUEST', `the launch configuration is longer than ${MAX_CONFIG} characters`);
|
|
282
|
+
const tasks = this.taskDefinitions(folder).map((task) => applyPlatform(task, this.platform));
|
|
283
|
+
const task = args.task ? tasks.find((t) => t.label === args.task) : null;
|
|
284
|
+
if (args.task && !task) fail('NOT_FOUND', `no task labelled "${args.task}" in .vscode/tasks.json`);
|
|
285
|
+
const roots = [folder.uri.fsPath, ...[].concat(args.source_folders || []).map((f) => {
|
|
286
|
+
if (typeof f !== 'string' || !path.isAbsolute(f)) fail('BAD_REQUEST', 'source_folders must be absolute paths');
|
|
287
|
+
return path.resolve(f);
|
|
288
|
+
})];
|
|
289
|
+
const s = {
|
|
290
|
+
id: ++this.sessions, phase: 'awaiting_approval', folder, launch: JSON.parse(JSON.stringify(launch)), json, task, tasks,
|
|
291
|
+
roots, breakpoints: [], functions: [], nextId: 1, stepsUsed: 0,
|
|
292
|
+
maxSteps: clamp(args.max_steps, 1, 100, 1000), minutes: clamp(args.max_minutes, 1, 30, 480),
|
|
293
|
+
buildMinutes: clamp(args.build_minutes, 1, 30, 600),
|
|
294
|
+
notes: [].concat(args.notes || []).map((note) => String(note).slice(0, 500)),
|
|
295
|
+
label: String(args.label || launch.name).slice(0, 200), dap: null, timer: null, buildTimer: null, execution: null,
|
|
296
|
+
};
|
|
297
|
+
const ids = this.addBreakpoints(s, args.breakpoints || [], args.functions || []);
|
|
298
|
+
this.session = s;
|
|
299
|
+
this.ask(s);
|
|
300
|
+
return { session: s.id, ...ids };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Record breakpoints for a session; they reach the IDE when the session runs.
|
|
304
|
+
addBreakpoints(s, lines, functions) {
|
|
305
|
+
const added = { breakpoint_ids: [], function_ids: [] };
|
|
306
|
+
for (const b of lines) {
|
|
307
|
+
const line = Number(b && b.line);
|
|
308
|
+
if (!Number.isInteger(line) || line < 1) fail('BAD_REQUEST', 'breakpoint lines are positive integers');
|
|
309
|
+
if (typeof b.file !== 'string' || !path.isAbsolute(b.file)) fail('BAD_REQUEST', 'breakpoint files are absolute paths');
|
|
310
|
+
const full = path.resolve(b.file);
|
|
311
|
+
if (!s.roots.some((root) => within(root, full))) fail('OUTSIDE_WORKSPACE', `${b.file} is outside the approved folders`);
|
|
312
|
+
const item = { id: `b${s.nextId++}`, full, line, condition: b.condition ? String(b.condition).slice(0, 500) : undefined, handle: null };
|
|
313
|
+
s.breakpoints.push(item);
|
|
314
|
+
added.breakpoint_ids.push(item.id);
|
|
315
|
+
}
|
|
316
|
+
for (const name of functions) {
|
|
317
|
+
const item = { id: `f${s.nextId++}`, name: String(name).slice(0, 300), handle: null };
|
|
318
|
+
s.functions.push(item);
|
|
319
|
+
added.function_ids.push(item.id);
|
|
320
|
+
}
|
|
321
|
+
return added;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
relative(s, full) {
|
|
325
|
+
const root = s.folder.uri.fsPath;
|
|
326
|
+
return within(root, full) ? path.relative(root, full).split(path.sep).join('/') : full;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
dialogText(s) {
|
|
330
|
+
const l = s.launch;
|
|
331
|
+
const program = [l.program || l.module || '', ...[].concat(l.args || [])].filter(Boolean).join(' ');
|
|
332
|
+
return [
|
|
333
|
+
...s.notes,
|
|
334
|
+
`Folder: ${s.folder.uri.fsPath}`,
|
|
335
|
+
s.task ? `Build first: ${describeTask(s.task.label, s.tasks)}` : 'Build: none',
|
|
336
|
+
l.preLaunchTask ? `Before the debugger starts (preLaunchTask): ${describeTask(l.preLaunchTask, s.tasks)}` : null,
|
|
337
|
+
l.postDebugTask ? `After the session (postDebugTask): ${describeTask(l.postDebugTask, s.tasks)}` : null,
|
|
338
|
+
`Run: "${l.name}" (${l.type}) ${program}`.trim(),
|
|
339
|
+
`Breakpoints: ${s.breakpoints.map((b) => `${this.relative(s, b.full)}:${b.line}${b.condition ? ` if ${b.condition}` : ''}`).join(', ') || 'none'}`,
|
|
340
|
+
s.functions.length ? `Function breakpoints: ${s.functions.map((b) => b.name).join(', ')}` : null,
|
|
341
|
+
s.roots.length > 1 ? `Breakpoints may also be set in: ${s.roots.slice(1).join(', ')}` : null,
|
|
342
|
+
`Limits: ${s.maxSteps} steps, ${s.minutes} minutes`,
|
|
343
|
+
l.request === 'attach'
|
|
344
|
+
? 'When the session ends: the debugger detaches and the program keeps running'
|
|
345
|
+
: 'When the session ends: the program is stopped',
|
|
346
|
+
'',
|
|
347
|
+
'The AI agent can then read the stack and variables, step and continue.',
|
|
348
|
+
'It cannot evaluate code or change variables. Secret-looking values are hidden from it.',
|
|
349
|
+
'',
|
|
350
|
+
'Launch configuration, exactly as it will be used:',
|
|
351
|
+
s.json,
|
|
352
|
+
].filter((line) => line !== null).join('\n');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
ask(s) {
|
|
356
|
+
const answered = (approved, reason) => {
|
|
357
|
+
if (this.session !== s || s.phase !== 'awaiting_approval') return false;
|
|
358
|
+
if (!approved) s.phase = 'denied';
|
|
359
|
+
this.emit({ kind: 'approval', approved, reason });
|
|
360
|
+
return true;
|
|
361
|
+
};
|
|
362
|
+
const timeout = setTimeout(() => answered(false, 'no answer within 2 minutes'), APPROVAL_SECONDS * 1000);
|
|
363
|
+
if (timeout.unref) timeout.unref();
|
|
364
|
+
const title = s.launch.request === 'attach'
|
|
365
|
+
? `AutoDebug: let the AI agent attach the debugger for "${s.label}"?`
|
|
366
|
+
: `AutoDebug: let the AI agent debug "${s.launch.name}"?`;
|
|
367
|
+
Promise.resolve(this.vscode.window.showWarningMessage(title, { modal: true, detail: this.dialogText(s) }, 'Approve'))
|
|
368
|
+
.then((choice) => {
|
|
369
|
+
clearTimeout(timeout);
|
|
370
|
+
if (choice !== 'Approve') { answered(false, 'the developer did not approve'); return; }
|
|
371
|
+
if (!answered(true, 'the developer clicked Approve')) return;
|
|
372
|
+
this.log(`approved: ${s.launch.name}`);
|
|
373
|
+
this.run(s).catch((error) => {
|
|
374
|
+
this.emit({ kind: 'error', message: String((error && error.message) || error) });
|
|
375
|
+
this.end('could not start');
|
|
376
|
+
});
|
|
377
|
+
}, () => { clearTimeout(timeout); answered(false, 'the dialog was dismissed'); });
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async run(s) {
|
|
381
|
+
const vs = this.vscode;
|
|
382
|
+
if (s.task) {
|
|
383
|
+
s.phase = 'building';
|
|
384
|
+
this.emit({ kind: 'build', task: s.task.label, status: 'running', exit_code: null });
|
|
385
|
+
const all = await vs.tasks.fetchTasks();
|
|
386
|
+
const task = all.find((t) => t.name === s.task.label && t.scope && t.scope.uri && t.scope.uri.fsPath === s.folder.uri.fsPath);
|
|
387
|
+
if (!task) { s.phase = 'build_failed'; this.emit({ kind: 'build', task: s.task.label, status: 'unavailable', exit_code: null }); return; }
|
|
388
|
+
const code = await new Promise((resolve) => {
|
|
389
|
+
const timer = setTimeout(() => {
|
|
390
|
+
try { if (s.execution) s.execution.terminate(); } catch (_) { /* already finished */ }
|
|
391
|
+
resolve('timeout');
|
|
392
|
+
}, s.buildMinutes * this.buildMinuteMs);
|
|
393
|
+
if (timer.unref) timer.unref();
|
|
394
|
+
s.buildTimer = timer;
|
|
395
|
+
const listener = vs.tasks.onDidEndTaskProcess((e) => {
|
|
396
|
+
if (s.execution && e.execution === s.execution) { listener.dispose(); clearTimeout(timer); resolve(e.exitCode); }
|
|
397
|
+
});
|
|
398
|
+
Promise.resolve(vs.tasks.executeTask(task)).then((execution) => { s.execution = execution; }, () => {
|
|
399
|
+
listener.dispose(); clearTimeout(timer); resolve(null);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
if (this.session !== s || s.phase !== 'building') return;
|
|
403
|
+
if (code !== 0) {
|
|
404
|
+
s.phase = 'build_failed';
|
|
405
|
+
this.emit({ kind: 'build', task: s.task.label, status: code === 'timeout' ? 'timeout' : 'failed',
|
|
406
|
+
exit_code: code === 'timeout' || code === undefined ? null : code, minutes: s.buildMinutes });
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
this.emit({ kind: 'build', task: s.task.label, status: 'succeeded', exit_code: 0 });
|
|
410
|
+
}
|
|
411
|
+
this.place(s, s.breakpoints, s.functions);
|
|
412
|
+
s.timer = setTimeout(() => this.end('the session time limit was reached'), s.minutes * 60000);
|
|
413
|
+
if (s.timer.unref) s.timer.unref();
|
|
414
|
+
s.phase = 'starting';
|
|
415
|
+
this.emit({ kind: 'starting' });
|
|
416
|
+
s.expecting = s.launch.name;
|
|
417
|
+
const started = await vs.debug.startDebugging(s.folder, JSON.parse(JSON.stringify(s.launch)));
|
|
418
|
+
s.expecting = null;
|
|
419
|
+
if (this.session !== s || s.phase !== 'starting') return;
|
|
420
|
+
if (!started || !s.dap) {
|
|
421
|
+
this.emit({ kind: 'not_started' });
|
|
422
|
+
this.finish(s, 'the debugger did not start', null);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
s.phase = 'debugging';
|
|
426
|
+
this.emit({ kind: 'started' });
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
place(s, lines, functions) {
|
|
430
|
+
const vs = this.vscode;
|
|
431
|
+
for (const b of lines) {
|
|
432
|
+
b.handle = new vs.SourceBreakpoint(new vs.Location(vs.Uri.file(b.full), new vs.Position(b.line - 1, 0)), true, b.condition);
|
|
433
|
+
}
|
|
434
|
+
for (const f of functions) f.handle = new vs.FunctionBreakpoint(f.name);
|
|
435
|
+
const handles = [...lines, ...functions].map((b) => b.handle);
|
|
436
|
+
if (handles.length) vs.debug.addBreakpoints(handles);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// ---- the debug session ---------------------------------------------------------
|
|
440
|
+
|
|
441
|
+
tracker(dap) {
|
|
442
|
+
this.sessionStarted(dap); // claim it before its first message
|
|
443
|
+
const mine = () => this.session && this.session.dap === dap && this.session.phase !== 'ended';
|
|
444
|
+
return {
|
|
445
|
+
onWillReceiveMessage: (m) => {
|
|
446
|
+
if (mine() && m && m.type === 'request' && TO_ADAPTER.has(m.command)) {
|
|
447
|
+
this.emit({ kind: 'dap', direction: 'to_adapter', message: m });
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
onDidSendMessage: (m) => {
|
|
451
|
+
if (!mine() || !m) return;
|
|
452
|
+
if (m.type === 'event' || (m.type === 'response' && RESPONSES.has(m.command))) {
|
|
453
|
+
if (m.type === 'event' && m.event === 'output' && m.body && typeof m.body.output === 'string' && m.body.output.length > 4000) {
|
|
454
|
+
m = { ...m, body: { ...m.body, output: `${m.body.output.slice(0, 4000)}…` } };
|
|
455
|
+
}
|
|
456
|
+
this.emit({ kind: 'dap', direction: 'from_adapter', message: m });
|
|
457
|
+
}
|
|
458
|
+
},
|
|
459
|
+
onExit: (code) => { if (mine()) this.finish(this.session, 'the program exited', code == null ? null : code); },
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
sessionStarted(dap) {
|
|
464
|
+
const s = this.session;
|
|
465
|
+
if (s && s.expecting && !s.dap && dap.configuration && dap.configuration.name === s.expecting) s.dap = dap;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
sessionEnded(dap) {
|
|
469
|
+
const s = this.session;
|
|
470
|
+
if (s && s.dap === dap) this.finish(s, 'the debug session ended', null);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
finish(s, reason, exitCode) {
|
|
474
|
+
if (s.phase === 'ended') return;
|
|
475
|
+
clearTimeout(s.timer);
|
|
476
|
+
clearTimeout(s.buildTimer);
|
|
477
|
+
this.removeAll(s);
|
|
478
|
+
s.phase = 'ended';
|
|
479
|
+
this.emit({ kind: 'ended', reason, exit_code: exitCode });
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
removeAll(s) {
|
|
483
|
+
const handles = [...s.breakpoints, ...s.functions].map((b) => b.handle).filter(Boolean);
|
|
484
|
+
if (handles.length) this.vscode.debug.removeBreakpoints(handles);
|
|
485
|
+
[...s.breakpoints, ...s.functions].forEach((b) => { b.handle = null; });
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
current() {
|
|
489
|
+
const s = this.session;
|
|
490
|
+
if (!s || !['starting', 'debugging'].includes(s.phase) || !s.dap) fail('NOT_FOUND', 'no debug session is running');
|
|
491
|
+
return s;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async dap(args) {
|
|
495
|
+
const s = this.current();
|
|
496
|
+
const command = String(args.command);
|
|
497
|
+
if (!DAP_COMMANDS.has(command)) fail('BAD_REQUEST', `the debugger request ${command} is not allowed`);
|
|
498
|
+
const step = STEPS.has(command);
|
|
499
|
+
if (step && s.stepsUsed >= s.maxSteps) fail('LIMIT', `the approved limit of ${s.maxSteps} steps is used up`);
|
|
500
|
+
if (step) s.stepsUsed += 1;
|
|
501
|
+
const ms = clamp(args.timeout_ms, 100, 10000, 60000);
|
|
502
|
+
let timer;
|
|
503
|
+
try {
|
|
504
|
+
const answer = await Promise.race([
|
|
505
|
+
s.dap.customRequest(command, args.arguments || {}),
|
|
506
|
+
new Promise((_, reject) => { timer = setTimeout(() => reject(new RelayError('IDE_ERROR', `the debugger did not answer ${command} in time`)), ms); }),
|
|
507
|
+
]);
|
|
508
|
+
return { body: answer || {} };
|
|
509
|
+
} catch (error) {
|
|
510
|
+
if (step) s.stepsUsed -= 1;
|
|
511
|
+
throw error instanceof RelayError ? error : new RelayError('IDE_ERROR', String((error && error.message) || error));
|
|
512
|
+
} finally {
|
|
513
|
+
clearTimeout(timer);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
breakpoints(args) {
|
|
518
|
+
const s = this.session;
|
|
519
|
+
if (!s || !['starting', 'debugging'].includes(s.phase)) fail('NOT_FOUND', 'no debug session is running');
|
|
520
|
+
const remove = new Set([].concat(args.remove || []).map(String));
|
|
521
|
+
const gone = [...s.breakpoints, ...s.functions].filter((b) => remove.has(b.id));
|
|
522
|
+
const handles = gone.map((b) => b.handle).filter(Boolean);
|
|
523
|
+
if (handles.length) this.vscode.debug.removeBreakpoints(handles);
|
|
524
|
+
s.breakpoints = s.breakpoints.filter((b) => !remove.has(b.id));
|
|
525
|
+
s.functions = s.functions.filter((b) => !remove.has(b.id));
|
|
526
|
+
const before = { lines: s.breakpoints.length, functions: s.functions.length };
|
|
527
|
+
const ids = this.addBreakpoints(s, args.add || [], args.add_functions || []);
|
|
528
|
+
this.place(s, s.breakpoints.slice(before.lines), s.functions.slice(before.functions));
|
|
529
|
+
return ids;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Breakpoints the developer set in the editor; they stop the program too.
|
|
533
|
+
editorBreakpoints() {
|
|
534
|
+
const s = this.session;
|
|
535
|
+
const mine = new Set(s ? [...s.breakpoints, ...s.functions].map((b) => b.handle).filter(Boolean) : []);
|
|
536
|
+
return (this.vscode.debug.breakpoints || []).filter((b) => !mine.has(b) && b.enabled !== false).map((b) => {
|
|
537
|
+
if (b.location && b.location.uri) return { file: b.location.uri.fsPath, line: b.location.range.start.line + 1 };
|
|
538
|
+
if (b.functionName) return { function: b.functionName };
|
|
539
|
+
return null;
|
|
540
|
+
}).filter(Boolean);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
notify(args) {
|
|
544
|
+
const text = String(args.text || '');
|
|
545
|
+
if (!text || text.length > 300) fail('BAD_REQUEST', 'the note is 1 to 300 characters');
|
|
546
|
+
if (!this.session || !OPEN.includes(this.session.phase)) fail('NOT_FOUND', 'no debug session is running');
|
|
547
|
+
this.log(`asked the developer: ${text}`);
|
|
548
|
+
const shown = this.vscode.window.showInformationMessage(`AutoDebug: the AI agent asks you to: ${text}`);
|
|
549
|
+
if (shown && typeof shown.then === 'function') shown.then(() => {}, () => {});
|
|
550
|
+
return {};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async end(reason) {
|
|
554
|
+
const s = this.session;
|
|
555
|
+
if (!s || ['ended', 'denied', 'build_failed'].includes(s.phase)) return;
|
|
556
|
+
const dap = s.dap;
|
|
557
|
+
if (s.execution && s.phase === 'building') {
|
|
558
|
+
try { s.execution.terminate(); } catch (_) { /* the build already finished */ }
|
|
559
|
+
}
|
|
560
|
+
this.finish(s, reason, null);
|
|
561
|
+
if (dap) {
|
|
562
|
+
if (s.launch.request === 'attach') {
|
|
563
|
+
try { await dap.customRequest('disconnect', { restart: false, terminateDebuggee: false }); } catch (_) { /* already gone */ }
|
|
564
|
+
}
|
|
565
|
+
try { await this.vscode.debug.stopDebugging(dap); } catch (_) { /* already stopped */ }
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
module.exports = { Relay, RelayError, stateFolder, PROTOCOL, VERSION };
|