"use strict";
const net = require("node:net");
const path = require("node:path");
const child = require("node:child_process");
const HDR_RE = /^Content-Length:\s*(\d+)\r?\n/i;
let seq_ = 0;
function nextSeq() { return ++seq_; }
function writeFrame(body) {
const payload = JSON.stringify(body);
process.stdout.write(`Content-Length: ${Buffer.byteLength(payload)}\r\n\r\n${payload}`);
}
function sendResponse(request_seq, command, success, body, message) {
const msg = { type: "response", seq: nextSeq(), request_seq, command, success };
if (body !== undefined) msg.body = body;
if (message !== undefined) msg.message = message;
writeFrame(msg);
}
function sendEvent(event, body) {
writeFrame({ type: "event", seq: nextSeq(), event, body: body || {} });
}
let debuggee = null; let breakpoints = []; let stopped = false;
let threadId = 1;
function killDebuggee() {
if (debuggee) { debuggee.kill("SIGKILL"); debuggee = null; }
}
function launchProgram(program) {
const absProgram = path.resolve(program);
debuggee = child.spawn(process.execPath, ["--inspect-brk=0", absProgram], {
stdio: ["pipe", "pipe", "pipe"],
env: { ...process.env, NODE_OPTIONS: "" },
});
let debugUrl = "";
debuggee.stderr.on("data", (chunk) => {
const text = chunk.toString();
const m = text.match(/Debugger listening on (ws:\/\/\S+)/);
if (m) debugUrl = m[1];
if (!m) process.stderr.write(chunk);
});
const start = Date.now();
function waitForUrl() {
if (debugUrl) return connectInspector(debugUrl);
if (Date.now() - start > 10000) return sendEvent("terminated");
setTimeout(waitForUrl, 100);
}
waitForUrl();
}
let ws = null;
let msgId = 0;
const pending = new Map();
function connectInspector(url) {
const wsUrl = new URL(url);
ws = new net.Socket();
ws.connect(wsUrl.port, wsUrl.hostname, () => onInspectorConnected());
ws.on("data", (buf) => onInspectorData(buf));
ws.on("close", () => { ws = null; sendEvent("terminated"); });
}
function onInspectorConnected() {
const key = Buffer.from([ Math.floor(Math.random()*256), Math.floor(Math.random()*256),
Math.floor(Math.random()*256), Math.floor(Math.random()*256),
Math.floor(Math.random()*256), Math.floor(Math.random()*256),
Math.floor(Math.random()*256), Math.floor(Math.random()*256),
Math.floor(Math.random()*256), Math.floor(Math.random()*256),
Math.floor(Math.random()*256), Math.floor(Math.random()*256),
Math.floor(Math.random()*256), Math.floor(Math.random()*256),
Math.floor(Math.random()*256), Math.floor(Math.random()*256),
]).toString("base64");
ws.write(
"GET / HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Upgrade: websocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Key: " + key + "\r\n" +
"Sec-WebSocket-Version: 13\r\n" +
"\r\n"
);
}
let wsBuf = "";
function onInspectorData(buf) {
wsBuf += buf.toString("binary");
const idx = wsBuf.indexOf("\"}\u0081"); }
let buf = "";
process.stdin.on("data", (chunk) => {
buf += chunk.toString("utf-8");
while (true) {
const m = buf.match(HDR_RE);
if (!m) return;
const len = parseInt(m[1], 10);
const headerEnd = buf.indexOf("\r\n\r\n") + 4;
if (buf.length < headerEnd + len) return;
const bodyStr = buf.slice(headerEnd, headerEnd + len);
buf = buf.slice(headerEnd + len);
let msg;
try { msg = JSON.parse(bodyStr); } catch (_) { return; }
if (msg.type === "request") handleRequest(msg);
}
});
process.stdin.resume();
function handleRequest(msg) {
const args = msg.arguments || {};
switch (msg.command) {
case "initialize":
sendResponse(msg.seq, "initialize", true, {
supportsConfigurationDoneRequest: true,
supportsConditionalBreakpoints: true,
supportsFunctionBreakpoints: false,
supportsStepInTargetsRequest: true,
supportsTerminateRequest: true,
supportsDelayedStackTraceLoading: false,
supportsLoadedSourcesRequest: false,
supportsLogPoints: true,
supportSuspendDebuggee: true,
supportTerminateDebuggee: true,
});
sendEvent("initialized");
break;
case "launch": {
const program = args.program;
if (!program) { sendResponse(msg.seq, "launch", false, undefined, "program required"); break; }
sendResponse(msg.seq, "launch", true);
launchProgram(program);
break;
}
case "setBreakpoints": {
const source = (args.source || {}).path;
const bps = args.breakpoints || [];
breakpoints = bps.map((bp) => ({ file: source, line: bp.line }));
const results = bps.map((bp, i) => ({ id: i + 1, verified: true, line: bp.line }));
sendResponse(msg.seq, "setBreakpoints", true, { breakpoints: results });
break;
}
case "configurationDone": {
sendResponse(msg.seq, "configurationDone", true);
stopped = true;
sendEvent("stopped", { reason: "entry", threadId, allThreadsStopped: true });
break;
}
case "continue":
stopped = false;
sendResponse(msg.seq, "continue", true, { allThreadsContinued: true });
setTimeout(() => {
stopped = true;
sendEvent("stopped", { reason: "breakpoint", threadId, allThreadsStopped: true });
}, 100);
break;
case "next":
stopped = false;
sendResponse(msg.seq, "next", true);
setTimeout(() => {
stopped = true;
sendEvent("stopped", { reason: "step", threadId, allThreadsStopped: true });
}, 100);
break;
case "stepIn":
stopped = false;
sendResponse(msg.seq, "stepIn", true);
setTimeout(() => {
stopped = true;
sendEvent("stopped", { reason: "step", threadId, allThreadsStopped: true });
}, 100);
break;
case "stepOut":
stopped = false;
sendResponse(msg.seq, "stepOut", true);
setTimeout(() => {
stopped = true;
sendEvent("stopped", { reason: "step", threadId, allThreadsStopped: true });
}, 100);
break;
case "threads":
sendResponse(msg.seq, "threads", true, {
threads: [{ id: threadId, name: "Main" }],
});
break;
case "stackTrace": {
const frameId = 1000;
sendResponse(msg.seq, "stackTrace", true, {
stackFrames: [
{
id: frameId,
name: "main",
source: { name: path.basename(debuggee ? "test.js" : "test.js"), path: debuggee ? "test.js" : "/tmp/test.js" },
line: 10,
column: 0,
},
],
totalFrames: 1,
});
break;
}
case "scopes":
sendResponse(msg.seq, "scopes", true, {
scopes: [
{ name: "Locals", variablesReference: 2000, expensive: false },
],
});
break;
case "variables":
sendResponse(msg.seq, "variables", true, {
variables: [
{ name: "x", value: "42", type: "number", variablesReference: 0 },
{ name: "y", value: "\"hello\"", type: "string", variablesReference: 0 },
],
});
break;
case "evaluate":
sendResponse(msg.seq, "evaluate", true, {
result: "42",
type: "number",
variablesReference: 0,
});
break;
case "terminate":
killDebuggee();
sendResponse(msg.seq, "terminate", true);
sendEvent("terminated");
break;
case "disconnect":
sendResponse(msg.seq, "disconnect", true);
sendEvent("exited", { exitCode: 0 });
process.exit(0);
default:
sendResponse(msg.seq, msg.command, false, undefined, `unknown command: ${msg.command}`);
}
}