More fixes for dap

This commit is contained in:
Ashcon Partovi
2023-08-25 12:03:00 -07:00
parent 21b2d5c3a5
commit 824655e1cb
37 changed files with 35482 additions and 183 deletions

View File

@@ -1,2 +1,2 @@
export type * from "./protocol";
export * from "./debugger/adapter";
export type * from "./src/protocol";
export * from "./src/debugger/adapter";

View File

@@ -1,8 +1,8 @@
{
"name": "bun-debug-adapter-protocol",
"version": "0.0.1",
"dependencies": {
"semver": "^7.5.4",
"ws": "^8.13.0",
"source-map-js": "^1.0.2"
}
}

View File

@@ -1,4 +1,4 @@
import type { Protocol, Type } from "../protocol/schema.d.ts";
import type { Protocol, Type } from "../src/protocol/schema";
import { writeFileSync } from "node:fs";
import { spawnSync } from "node:child_process";

View File

@@ -1,12 +1,11 @@
import type { DAP } from "..";
// @ts-ignore: FIXME - there is something wrong with the types
import type { JSC, InspectorListener } from "../../bun-inspector-protocol";
import { WebSocketInspector } from "../../bun-inspector-protocol";
import type { DAP } from "../protocol";
// @ts-ignore
import type { JSC, InspectorListener, WebSocketInspectorOptions } from "../../../bun-inspector-protocol";
import { WebSocketInspector, remoteObjectToString } from "../../../bun-inspector-protocol/index";
import type { ChildProcess } from "node:child_process";
import { spawn, spawnSync } from "node:child_process";
import capabilities from "./capabilities";
import { Location, SourceMap } from "./sourcemap";
import { remoteObjectToString } from "./preview";
import { compare, parse } from "semver";
type InitializeRequest = DAP.InitializeRequest & {
@@ -71,23 +70,28 @@ type Variable = DAP.Variable & {
};
type IDebugAdapter = {
[E in keyof DAP.EventMap]?: (event: DAP.EventMap[E]) => void;
[E in keyof DAP.EventMap]?: (event: DAP.EventMap[E]) => void | Promise<void>;
} & {
[R in keyof DAP.RequestMap]?: (
request: DAP.RequestMap[R],
) => void | DAP.ResponseMap[R] | Promise<void | DAP.ResponseMap[R]>;
) => void | DAP.ResponseMap[R] | Promise<DAP.ResponseMap[R]> | Promise<void>;
};
export type DebugAdapterOptions = {
sendToAdapter(message: DAP.Request | DAP.Response | DAP.Event): Promise<void>;
export type DebugAdapterOptions = WebSocketInspectorOptions & {
send(message: DAP.Request | DAP.Response | DAP.Event): Promise<void>;
stdout?(message: string): void;
stderr?(message: string): void;
};
// This adapter only support single-threaded debugging,
// which means that there is only one thread at a time.
const threadId = 1;
// @ts-ignore
export class DebugAdapter implements IDebugAdapter, InspectorListener {
#sendToAdapter: DebugAdapterOptions["sendToAdapter"];
#sendToAdapter: DebugAdapterOptions["send"];
#stdout?: DebugAdapterOptions["stdout"];
#stderr?: DebugAdapterOptions["stderr"];
#inspector: WebSocketInspector;
#sourceId: number;
#pendingSources: Map<string, ((source: Source) => void)[]>;
@@ -105,9 +109,12 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
#terminated?: boolean;
#url?: URL;
constructor({ sendToAdapter }: DebugAdapterOptions) {
this.#inspector = new WebSocketInspector({ listener: this });
this.#sendToAdapter = sendToAdapter;
constructor({ send, stdout, stderr, ...options }: DebugAdapterOptions) {
// @ts-ignore
this.#inspector = new WebSocketInspector({ ...options, listener: this });
this.#stdout = stdout;
this.#stderr = stderr;
this.#sendToAdapter = send;
this.#sourceId = 1;
this.#pendingSources = new Map();
this.#sources = new Map();
@@ -247,16 +254,27 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
throw new Error("Program must be a JavaScript or TypeScript file.");
}
const argz = ["--inspect-wait=0", ...args];
const finalArgs = ["--inspect-wait=0", ...args];
if (watch) {
argz.push(watch === "hot" ? "--hot" : "--watch");
finalArgs.push(watch === "hot" ? "--hot" : "--watch");
}
console.log(argz);
const subprocess = spawn(runtime, [...argz, program], {
const finalEnv = inheritEnv
? {
...process.env,
...env,
}
: {
...env,
};
// https://github.com/microsoft/vscode/issues/571
finalEnv["NO_COLOR"] = "1";
const subprocess = spawn(runtime, [...finalArgs, program], {
stdio: ["ignore", "pipe", "pipe", "pipe"],
cwd,
env: inheritEnv ? { ...process.env, ...env } : env,
env: finalEnv,
});
subprocess.on("spawn", () => {
@@ -278,8 +296,10 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
const stdout: string[] = [];
subprocess.stdout!.on("data", data => {
const text = data.toString();
this.#stdout?.(text);
if (!this.#url) {
const text = data.toString();
stdout.push(text);
const url = (this.#url = parseUrlMaybe(text));
this.#inspector.start(url);
@@ -290,8 +310,11 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
const stderr: string[] = [];
subprocess.stderr!.on("data", data => {
const text = data.toString();
this.#stderr?.(text);
if (!this.#url) {
stderr.push(data.toString());
stderr.push(text);
} else if (stderr.length) {
stderr.length = 0;
}
@@ -468,20 +491,20 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
if (!numberIsValid(line)) {
return 0;
}
if (this.#initialized?.linesStartAt1) {
return line - 1;
if (!this.#initialized?.linesStartAt1) {
return line;
}
return line;
return line - 1;
}
#columnTo0BasedColumn(column?: number): number {
if (!numberIsValid(column)) {
return 0;
}
if (this.#initialized?.columnsStartAt1) {
return column - 1;
if (!this.#initialized?.columnsStartAt1) {
return column;
}
return column;
return column - 1;
}
#originalLocation(
@@ -505,17 +528,17 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
}
#lineFrom0BasedLine(line?: number): number {
if (this.#initialized?.linesStartAt1) {
return numberIsValid(line) ? line + 1 : 1;
if (!this.#initialized?.linesStartAt1) {
return numberIsValid(line) ? line : 0;
}
return numberIsValid(line) ? line : 0;
return numberIsValid(line) ? line + 1 : 1;
}
#columnFrom0BasedColumn(column?: number): number {
if (this.#initialized?.columnsStartAt1) {
return numberIsValid(column) ? column + 1 : 1;
if (!this.#initialized?.columnsStartAt1) {
return numberIsValid(column) ? column : 0;
}
return numberIsValid(column) ? column : 0;
return numberIsValid(column) ? column + 1 : 1;
}
async setBreakpoints(request: DAP.SetBreakpointsRequest): Promise<DAP.SetBreakpointsResponse> {
@@ -942,8 +965,7 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
const variables = parameters.map((parameter, i) => {
const variable = this.#addVariable(parameter, { name: `${i}` });
const { value } = variable;
output += value + " ";
output += remoteObjectToString(parameter, true) + " ";
return variable;
});
@@ -1681,8 +1703,6 @@ function consoleLevelToAnsiColor(level: JSC.Console.ConsoleMessage["level"]): st
return "\u001b[33m";
case "error":
return "\u001b[31m";
case "debug":
return "\u001b[36m";
}
return undefined;
}

View File

@@ -1,4 +1,4 @@
import type { DAP } from "..";
import type { DAP } from "../protocol";
const capabilities: DAP.Capabilities = {
/**

View File

@@ -52,6 +52,7 @@ class ActualSourceMap implements SourceMap {
generatedLocation(request: LocationRequest): Location {
const { line, column, url } = request;
let lineRange: LineRange;
try {
const source = this.#getSource(url);
@@ -68,6 +69,7 @@ class ActualSourceMap implements SourceMap {
message: unknownToError(error),
};
}
if (!locationIsValid(lineRange)) {
return {
line: lineToLine(line),
@@ -75,6 +77,7 @@ class ActualSourceMap implements SourceMap {
verified: false,
};
}
const { line: gline, column: gcolumn } = lineRange;
return {
line: lineToLine(gline),
@@ -85,6 +88,7 @@ class ActualSourceMap implements SourceMap {
originalLocation(request: LocationRequest): Location {
const { line, column } = request;
let mappedPosition: MappedPosition;
try {
mappedPosition = this.#sourceMap.originalPositionFor({
@@ -99,6 +103,7 @@ class ActualSourceMap implements SourceMap {
message: unknownToError(error),
};
}
if (!locationIsValid(mappedPosition)) {
return {
line: lineToLine(line),
@@ -106,6 +111,7 @@ class ActualSourceMap implements SourceMap {
verified: false,
};
}
const { line: oline, column: ocolumn } = mappedPosition;
return {
line: lineTo0BasedLine(oline),

File diff suppressed because it is too large Load Diff

View File

@@ -15,8 +15,7 @@
"forceConsistentCasingInFileNames": true,
"inlineSourceMap": true,
"allowJs": true,
"types": ["bun-types"],
"outDir": "dist",
},
"include": [".", "../bun-types/index.d.ts", "../bun-inspector-protocol/index"]
"include": ["src", "scripts", "../bun-types/index.d.ts", "../bun-inspector-protocol/src"]
}

View File

@@ -1,3 +1,4 @@
export type * from "./protocol";
export type * from "./inspector";
export * from "./inspector/websocket";
export type * from "./src/protocol";
export type * from "./src/inspector";
export * from "./src/util/preview";
export * from "./src/inspector/websocket";

View File

@@ -1,7 +0,0 @@
export default {
fetch(request) {
console.log(request);
debugger;
return new Response();
},
};

View File

@@ -1,83 +0,0 @@
import { afterAll, beforeAll, mock, test, expect } from "bun:test";
import type { JSC } from "..";
import type { InspectorListener } from ".";
import { WebSocketInspector } from "./websocket";
import { sleep, spawn } from "bun";
let inspectee: any;
let url: string;
beforeAll(async () => {
const { pathname } = new URL("fixtures/inspectee.js", import.meta.url);
inspectee = spawn({
cmd: [process.argv0, "--inspect", pathname],
stdout: "pipe",
stderr: "pipe",
});
url = await new Promise(async resolve => {
for await (const chunk of inspectee.stdout) {
const text = new TextDecoder().decode(chunk);
const match = /(wss?:\/\/.*:[0-9]+\/.*)/.exec(text);
if (!match) {
continue;
}
const [_, url] = match;
resolve(url);
}
});
});
afterAll(() => {
inspectee?.kill();
});
test(
"WebSocketInspector",
async () => {
const listener: InspectorListener = {
["Inspector.connected"]: mock((...args) => {
expect(args).toBeEmpty();
}),
["Inspector.disconnected"]: mock((error?: Error) => {
expect(error).toBeUndefined();
}),
["Debugger.scriptParsed"]: mock((event: JSC.Debugger.ScriptParsedEvent) => {
expect(event).toMatchObject({
endColumn: expect.any(Number),
endLine: expect.any(Number),
isContentScript: expect.any(Boolean),
module: expect.any(Boolean),
scriptId: expect.any(String),
startColumn: expect.any(Number),
startLine: expect.any(Number),
url: expect.any(String),
});
}),
};
const inspector = new WebSocketInspector({
url,
listener,
});
inspector.start();
inspector.send("Runtime.enable");
inspector.send("Debugger.enable");
//expect(inspector.send("Runtime.enable")).resolves.toBeEmpty();
//expect(inspector.send("Debugger.enable")).resolves.toBeEmpty();
expect(inspector.send("Runtime.evaluate", { expression: "1 + 1" })).resolves.toMatchObject({
result: {
type: "number",
value: 2,
description: "2",
},
wasThrown: false,
});
expect(listener["Inspector.connected"]).toHaveBeenCalled();
expect(listener["Debugger.scriptParsed"]).toHaveBeenCalled();
inspector.close();
expect(inspector.closed).toBeTrue();
expect(listener["Inspector.disconnected"]).toHaveBeenCalled();
},
{
timeout: 100000,
},
);

View File

@@ -1,4 +1,4 @@
import type { Protocol, Domain, Property } from "../protocol/schema";
import type { Protocol, Domain, Property } from "../src/protocol/schema";
import { readFileSync, writeFileSync } from "node:fs";
import { spawnSync } from "node:child_process";

View File

@@ -1,10 +1,11 @@
import type { Inspector, InspectorListener } from ".";
import { JSC } from "..";
import type { JSC } from "../protocol";
import { WebSocket } from "ws";
export type WebSocketInspectorOptions = {
url?: string | URL;
listener?: InspectorListener;
logger?: (...messages: unknown[]) => void;
};
/**
@@ -17,13 +18,15 @@ export class WebSocketInspector implements Inspector {
#pendingRequests: Map<number, (result: unknown) => void>;
#pendingMessages: string[];
#listener: InspectorListener;
#log: (...messages: unknown[]) => void;
constructor({ url, listener }: WebSocketInspectorOptions) {
constructor({ url, listener, logger }: WebSocketInspectorOptions) {
this.#url = url ? new URL(url) : undefined;
this.#listener = listener ?? {};
this.#requestId = 1;
this.#pendingRequests = new Map();
this.#pendingMessages = [];
this.#listener = listener ?? {};
this.#log = logger ?? (() => {});
}
start(url?: string | URL): void {
@@ -40,9 +43,10 @@ export class WebSocketInspector implements Inspector {
return;
}
this.#webSocket?.close();
let webSocket: WebSocket;
try {
console.log("[jsc] connecting", this.#url.href);
this.#log("connecting:", this.#url.href);
webSocket = new WebSocket(this.#url, {
headers: {
"Ref-Event-Loop": "0",
@@ -52,35 +56,44 @@ export class WebSocketInspector implements Inspector {
this.#close(unknownToError(error));
return;
}
webSocket.addEventListener("open", () => {
console.log("[jsc] connected");
this.#log("connected");
for (const message of this.#pendingMessages) {
this.#send(message);
}
this.#pendingMessages.length = 0;
this.#listener["Inspector.connected"]?.();
});
webSocket.addEventListener("message", ({ data }) => {
if (typeof data === "string") {
this.accept(data);
}
});
webSocket.addEventListener("error", event => {
console.log("[jsc] error", event);
this.#log("error:", event);
this.#close(unknownToError(event));
});
webSocket.addEventListener("unexpected-response", () => {
console.log("[jsc] unexpected-response");
this.#log("unexpected-response");
this.#close(new Error("WebSocket upgrade failed"));
});
webSocket.addEventListener("close", ({ code, reason }) => {
console.log("[jsc] closed", code, reason);
this.#log("closed:", code, reason);
if (code === 1001) {
this.#close();
} else {
this.#close(new Error(`WebSocket closed: ${code} ${reason}`.trimEnd()));
}
});
this.#webSocket = webSocket;
}
@@ -90,7 +103,9 @@ export class WebSocketInspector implements Inspector {
): Promise<JSC.ResponseMap[M]> {
const id = this.#requestId++;
const request = { id, method, params };
console.log("[jsc] -->", request);
this.#log("-->", request);
return new Promise((resolve, reject) => {
const done = (result: any) => {
this.#pendingRequests.delete(id);
@@ -100,6 +115,7 @@ export class WebSocketInspector implements Inspector {
resolve(result);
}
};
this.#pendingRequests.set(id, done);
this.#send(JSON.stringify(request));
});
@@ -113,6 +129,7 @@ export class WebSocketInspector implements Inspector {
}
return;
}
if (!this.#pendingMessages.includes(message)) {
this.#pendingMessages.push(message);
}
@@ -123,35 +140,37 @@ export class WebSocketInspector implements Inspector {
try {
event = JSON.parse(message);
} catch (error) {
console.error("Failed to parse message:", message);
this.#log("Failed to parse message:", message);
return;
}
console.log("[jsc] <--", event);
if ("id" in event) {
const { id } = event;
const resolve = this.#pendingRequests.get(id);
if (!resolve) {
console.error(`Failed to accept response for unknown ID ${id}:`, event);
return;
}
this.#pendingRequests.delete(id);
if ("error" in event) {
const { error } = event;
const { message } = error;
resolve(new Error(message));
} else {
const { result } = event;
resolve(result);
}
} else {
this.#log("<--", event);
if (!("id" in event)) {
const { method, params } = event;
try {
// @ts-ignore
this.#listener[method]?.(params);
this.#listener[method]?.(params as any);
} catch (error) {
console.error(`Failed to accept ${method} event:`, error);
return;
this.#log(`Failed to accept ${method} event:`, error);
}
return;
}
const { id } = event;
const resolve = this.#pendingRequests.get(id);
if (!resolve) {
this.#log("Failed to accept response with unknown ID:", id);
return;
}
this.#pendingRequests.delete(id);
if ("error" in event) {
const { error } = event;
const { message } = error;
resolve(new Error(message));
} else {
const { result } = event;
resolve(result);
}
}
@@ -159,12 +178,14 @@ export class WebSocketInspector implements Inspector {
if (!this.#webSocket) {
return true;
}
const { readyState } = this.#webSocket;
switch (readyState) {
case WebSocket.CLOSED:
case WebSocket.CLOSING:
return true;
}
return false;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
import type { JSC } from "../../bun-inspector-protocol";
import type { JSC } from "../protocol";
export function remoteObjectToString(remoteObject: JSC.Runtime.RemoteObject): string {
export function remoteObjectToString(remoteObject: JSC.Runtime.RemoteObject, topLevel?: boolean): string {
const { type, subtype, value, description, className, preview } = remoteObject;
switch (type) {
case "undefined":
@@ -9,6 +9,9 @@ export function remoteObjectToString(remoteObject: JSC.Runtime.RemoteObject): st
case "number":
return description ?? JSON.stringify(value);
case "string":
if (topLevel) {
return String(value ?? description);
}
return JSON.stringify(value ?? description);
case "symbol":
case "bigint":

View File

@@ -1,9 +1,8 @@
import { beforeAll, afterAll, test, expect } from "bun:test";
import type { JSC } from "../../bun-inspector-protocol";
import { WebSocketInspector } from "../../bun-inspector-protocol";
import type { PipedSubprocess } from "bun";
import { spawn } from "bun";
import { remoteObjectToString } from "./preview";
import type { JSC } from "../..";
import { WebSocketInspector, remoteObjectToString } from "../..";
let subprocess: PipedSubprocess | undefined;
let objects: JSC.Runtime.RemoteObject[] = [];
@@ -11,7 +10,7 @@ let objects: JSC.Runtime.RemoteObject[] = [];
beforeAll(async () => {
subprocess = spawn({
cwd: import.meta.dir,
cmd: [process.argv0, "--inspect-wait=0", "fixtures/preview.js"],
cmd: [process.argv0, "--inspect-wait=0", "preview.js"],
stdout: "pipe",
stderr: "pipe",
stdin: "pipe",

View File

@@ -1,9 +1,9 @@
{
"compilerOptions": {
"lib": ["ESNext"],
"module": "esnext",
"target": "esnext",
"moduleResolution": "nodenext",
"module": "ESNext",
"target": "ESNext",
"moduleResolution": "NodeNext",
"moduleDetection": "force",
"strict": true,
"downlevelIteration": true,
@@ -13,7 +13,6 @@
"inlineSourceMap": true,
"allowJs": true,
"outDir": "dist",
"types": ["node"]
},
"include": [".", "../bun-types/index.d.ts"]
}

View File

@@ -6,7 +6,7 @@
"request": "launch",
"name": "Debug Bun",
"program": "${file}",
"watch": "hot"
"watch": true
},
{
"type": "bun",

View File

@@ -4,7 +4,7 @@ export default {
const coolThing: CoolThing = new SuperCoolThing();
coolThing.doCoolThing();
debugger;
return new Response("HELLO WORLD");
return new Response("HELLO WHAT!");
},
};

View File

@@ -115,13 +115,27 @@ function isJavaScript(languageId: string): boolean {
export class VSCodeAdapter extends DebugSession {
#adapter: DebugAdapter;
#console: vscode.OutputChannel;
#dap: vscode.OutputChannel;
#jsc: vscode.OutputChannel;
constructor(session: vscode.DebugSession) {
super();
this.#dap = vscode.window.createOutputChannel("Debug Adapter Protocol");
const output = (this.#console = vscode.window.createOutputChannel("Console (Bun)"));
this.#dap = vscode.window.createOutputChannel("Debug Adapter Protocol (Bun)");
const jsc = (this.#jsc = vscode.window.createOutputChannel("JavaScript Inspector (Bun)"));
this.#adapter = new DebugAdapter({
sendToAdapter: this.sendMessage.bind(this),
send: this.sendMessage.bind(this),
logger(...messages) {
console.log("[jsc]", ...messages);
jsc.appendLine(messages.map(v => (typeof v === "object" ? JSON.stringify(v) : v)).join(" "));
},
stdout(message) {
output.append(message);
},
stderr(message) {
output.append(message);
},
});
}
@@ -148,6 +162,8 @@ export class VSCodeAdapter extends DebugSession {
dispose() {
this.#adapter.close();
this.#console.dispose();
this.#dap.dispose();
this.#jsc.dispose();
}
}