aide-sovereign-workbench / daemon /dap-manager.mjs
Ferrell Synthetic Intelligence
Synchronize release candidate hardening
0d13723
Raw
History Blame Contribute Delete
3.52 kB
import { promises as fs } from 'node:fs';
import path from 'node:path';
import { spawn } from 'node:child_process';
export class DapManager {
constructor({ manifestPath, workspace, pythonPath = '', spawnProcess = spawn } = {}) {
this.manifestPath = manifestPath;
this.workspace = workspace;
this.spawnProcess = spawnProcess;
this.pythonPath = pythonPath;
this.adapters = new Map();
this.processes = new Map();
this.pending = new Map();
this.nextSeq = 1;
}
async load() {
const manifest = JSON.parse(await fs.readFile(this.manifestPath, 'utf8'));
this.adapters = new Map(manifest.adapters.map(adapter => [adapter.id, adapter]));
return manifest;
}
status() {
return [...this.adapters.values()].map(adapter => ({
id: adapter.id,
name: adapter.name,
languages: adapter.languages,
status: this.processes.has(adapter.id) ? 'running' : adapter.status
}));
}
async start(id) {
const adapter = this.adapters.get(id);
if (!adapter) throw new Error('debug adapter is not allowlisted');
if (this.processes.has(id)) return { id, status: 'running' };
const command = adapter.command === '.venv/bin/python' && this.pythonPath ? this.pythonPath : path.resolve(this.workspace, adapter.command);
await fs.access(command).catch(() => { throw new Error(`debug adapter is unavailable: ${command}`); });
const child = this.spawnProcess(command, adapter.args, { cwd: this.workspace, stdio: ['pipe', 'pipe', 'pipe'] });
this.processes.set(id, child);
child.stdout?.on('data', data => this.#consume(id, data));
child.once('exit', () => this.processes.delete(id));
return { id, status: 'starting', languages: adapter.languages, protocol: 'DAP' };
}
request(id, request) {
const child = this.processes.get(id);
if (!child) return Promise.reject(new Error('debug adapter is not running'));
const seq = request.seq ?? this.nextSeq++;
const payload = JSON.stringify({ ...request, seq, type: 'request' });
child.stdin.write(`Content-Length: ${Buffer.byteLength(payload)}\r\n\r\n${payload}`);
return new Promise((resolve, reject) => {
const timer = setTimeout(() => { this.pending.delete(`${id}:${seq}`); reject(new Error('DAP request timed out')); }, 15000);
this.pending.set(`${id}:${seq}`, { resolve, reject, timer });
});
}
#consume(id, data) {
let buffer = this[`buffer_${id}`] = `${this[`buffer_${id}`] || ''}${data}`;
while (true) {
const split = buffer.indexOf('\r\n\r\n');
if (split < 0) break;
const match = /Content-Length:\s*(\d+)/i.exec(buffer.slice(0, split));
if (!match) { buffer = buffer.slice(split + 4); continue; }
const length = Number(match[1]); const start = split + 4;
if (Buffer.byteLength(buffer.slice(start)) < length) break;
const raw = buffer.slice(start, start + length); buffer = buffer.slice(start + length);
try {
const message = JSON.parse(raw);
const key = `${id}:${message.request_seq}`;
const pending = this.pending.get(key);
if (pending) { clearTimeout(pending.timer); this.pending.delete(key); pending.resolve(message); }
} catch { /* ignore malformed adapter frames */ }
}
this[`buffer_${id}`] = buffer;
}
async stop(id) {
const child = this.processes.get(id);
if (!child) return { id, status: 'stopped' };
child.kill('SIGTERM');
this.processes.delete(id);
return { id, status: 'stopped' };
}
}