aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Ashcon Partovi <ashcon@partovi.net> 2023-08-26 18:26:50 -0700
committerGravatar Ashcon Partovi <ashcon@partovi.net> 2023-08-26 18:26:50 -0700
commiteeeef5aaf05c832ccc4e6de6781f1924a5893808 (patch)
tree8b1283bd79d0e17a850717c9a88d668fb0c2c21d
parentf9b966c13f5ed3d870c54b69cb59a5adc12060b7 (diff)
downloadbun-dap3.tar.gz
bun-dap3.tar.zst
bun-dap3.zip
Terminal works, launch is being reworkeddap3
-rw-r--r--packages/bun-debug-adapter-protocol/index.ts1
-rw-r--r--packages/bun-debug-adapter-protocol/src/debugger/adapter.ts499
-rw-r--r--packages/bun-debug-adapter-protocol/src/debugger/signal.ts87
-rw-r--r--packages/bun-inspector-protocol/src/inspector/index.d.ts41
-rw-r--r--packages/bun-inspector-protocol/src/inspector/websocket.ts205
-rw-r--r--packages/bun-inspector-protocol/test/inspector/websocket.test.ts190
-rw-r--r--packages/bun-inspector-protocol/test/util/__snapshots__/preview.test.ts.snap143
-rw-r--r--packages/bun-inspector-protocol/test/util/preview.js99
-rw-r--r--packages/bun-inspector-protocol/test/util/preview.test.ts61
-rw-r--r--packages/bun-vscode/src/features/debug.ts58
10 files changed, 646 insertions, 738 deletions
diff --git a/packages/bun-debug-adapter-protocol/index.ts b/packages/bun-debug-adapter-protocol/index.ts
index e1b6e900d..170a8d1c1 100644
--- a/packages/bun-debug-adapter-protocol/index.ts
+++ b/packages/bun-debug-adapter-protocol/index.ts
@@ -1,2 +1,3 @@
export type * from "./src/protocol";
export * from "./src/debugger/adapter";
+export * from "./src/debugger/signal";
diff --git a/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts b/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts
index 33555dbb0..fc55a57da 100644
--- a/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts
+++ b/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts
@@ -1,12 +1,14 @@
import type { DAP } from "../protocol";
+import type { JSC } from "../../../bun-inspector-protocol/src/protocol";
+import type { InspectorEventMap } from "../../../bun-inspector-protocol/src/inspector";
// @ts-ignore
-import type { JSC, InspectorListener, WebSocketInspectorOptions } from "../../../bun-inspector-protocol";
-import { UnixWebSocketInspector, remoteObjectToString } from "../../../bun-inspector-protocol/index";
+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 { compare, parse } from "semver";
+import { EventEmitter } from "node:events";
type InitializeRequest = DAP.InitializeRequest & {
supportsConfigurationDoneRequest?: boolean;
@@ -46,6 +48,7 @@ type Source = DAP.Source & {
type Breakpoint = DAP.Breakpoint & {
id: number;
breakpointId: string;
+ generatedLocation: JSC.Debugger.Location;
source: Source;
};
@@ -78,24 +81,28 @@ type IDebugAdapter = {
) => void | DAP.ResponseMap[R] | Promise<DAP.ResponseMap[R]> | Promise<void>;
};
-export type DebugAdapterOptions = WebSocketInspectorOptions & {
- url: string | URL;
- send(message: DAP.Request | DAP.Response | DAP.Event): Promise<void>;
- stdout?(message: string): void;
- stderr?(message: string): void;
+export type DebugAdapterEventMap = InspectorEventMap & {
+ [E in keyof DAP.EventMap as E extends string ? `Adapter.${E}` : never]: [DAP.EventMap[E]];
+} & {
+ "Adapter.request": [DAP.Request];
+ "Adapter.response": [DAP.Response];
+ "Adapter.event": [DAP.Event];
+ "Adapter.error": [Error];
+} & {
+ "Process.requested": [unknown];
+ "Process.spawned": [ChildProcess];
+ "Process.exited": [number | Error | null, string | null];
+ "Process.stdout": [string];
+ "Process.stderr": [string];
};
// This adapter only support single-threaded debugging,
// which means that there is only one thread at a time.
const threadId = 1;
+const isDebug = process.env.NODE_ENV === "development";
-// @ts-ignore
-export class DebugAdapter implements IDebugAdapter, InspectorListener {
- #url: URL;
- #sendToAdapter: DebugAdapterOptions["send"];
- #stdout?: DebugAdapterOptions["stdout"];
- #stderr?: DebugAdapterOptions["stderr"];
- #inspector: UnixWebSocketInspector;
+export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements IDebugAdapter {
+ #inspector: WebSocketInspector;
#sourceId: number;
#pendingSources: Map<string, ((source: Source) => void)[]>;
#sources: Map<string | number, Source>;
@@ -110,13 +117,16 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
#launched?: LaunchRequest;
#connected?: boolean;
- constructor({ url, send, stdout, stderr, ...options }: DebugAdapterOptions) {
- this.#url = new URL(url);
- // @ts-ignore
- this.#inspector = new UnixWebSocketInspector({ ...options, url, listener: this });
- this.#stdout = stdout;
- this.#stderr = stderr;
- this.#sendToAdapter = send;
+ constructor(url?: string | URL) {
+ super();
+ this.#inspector = new WebSocketInspector(url);
+ const emit = this.#inspector.emit.bind(this.#inspector);
+ this.#inspector.emit = (event, ...args) => {
+ let sent = false;
+ sent ||= emit(event, ...args);
+ sent ||= this.emit(event, ...(args as any));
+ return sent;
+ };
this.#sourceId = 1;
this.#pendingSources = new Map();
this.#sources = new Map();
@@ -128,84 +138,200 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
this.#variables = [{ name: "", value: "", type: undefined, variablesReference: 0 }];
}
- get inspector(): UnixWebSocketInspector {
- return this.#inspector;
+ get url(): string {
+ return this.#inspector.url;
+ }
+
+ start(url?: string): Promise<boolean> {
+ return this.#inspector.start(url);
}
- async accept(message: DAP.Request | DAP.Response | DAP.Event): Promise<void> {
- const { type } = message;
+ /**
+ * Sends a request to the JavaScript inspector.
+ * @param method the method name
+ * @param params the method parameters
+ * @returns the response
+ * @example
+ * const { result, wasThrown } = await adapter.send("Runtime.evaluate", {
+ * expression: "1 + 1",
+ * });
+ * console.log(result.value); // 2
+ */
+ async send<M extends keyof JSC.ResponseMap>(method: M, params?: JSC.RequestMap[M]): Promise<JSC.ResponseMap[M]> {
+ return this.#inspector.send(method, params);
+ }
- switch (type) {
- case "request":
- return this.#acceptRequest(message);
+ /**
+ * Emits an event. For the adapter to work, you must:
+ * - emit `Adapter.request` when the client sends a request to the adapter.
+ * - listen to `Adapter.response` to receive responses from the adapter.
+ * - listen to `Adapter.event` to receive events from the adapter.
+ * @param event the event name
+ * @param args the event arguments
+ * @returns if the event was sent to a listener
+ */
+ emit<E extends keyof DebugAdapterEventMap>(event: E, ...args: DebugAdapterEventMap[E] | []): boolean {
+ if (isDebug && event !== "Adapter.event" && event !== "Inspector.event") {
+ console.log(event, ...args);
}
- throw new Error(`Not supported: ${type}`);
+ let sent = super.emit(event, ...(args as any));
+
+ if (!(event in this)) {
+ return sent;
+ }
+
+ let result: unknown;
+ try {
+ // @ts-ignore
+ result = this[event as keyof this](...(args as any));
+ } catch (cause) {
+ sent ||= this.emit("Adapter.error", unknownToError(cause));
+ return sent;
+ }
+
+ if (result instanceof Promise) {
+ result.catch(cause => {
+ this.emit("Adapter.error", unknownToError(cause));
+ });
+ }
+
+ return sent;
+ }
+
+ #emit<E extends keyof DAP.EventMap>(event: E, body?: DAP.EventMap[E]): void {
+ this.emit("Adapter.event", {
+ type: "event",
+ seq: 0,
+ event,
+ body,
+ });
}
- async #acceptRequest(request: DAP.Request): Promise<void> {
- const { seq, command, arguments: args } = request;
+ async ["Adapter.request"](request: DAP.Request): Promise<void> {
+ const { command, arguments: args } = request;
+
+ if (!(command in this)) {
+ return;
+ }
- let response;
+ let result: unknown;
try {
- if (!(command! in this)) {
- throw new Error(`Not supported: ${command}`);
- }
- response = await this[command as keyof this](args);
- } catch (error) {
- const { message } = unknownToError(error);
- return this.#sendToAdapter({
+ // @ts-ignore
+ result = await this[command as keyof this](args);
+ } catch (cause) {
+ const error = unknownToError(cause);
+ this.emit("Adapter.error", error);
+
+ const { message } = error;
+ this.emit("Adapter.response", {
type: "response",
+ command,
success: false,
message,
- request_seq: seq,
+ request_seq: request.seq,
seq: 0,
- command,
});
+ return;
}
- return this.#sendToAdapter({
+ this.emit("Adapter.response", {
type: "response",
+ command,
success: true,
- request_seq: seq,
+ request_seq: request.seq,
seq: 0,
- command,
- body: response,
+ body: result,
});
}
- async #send<M extends keyof JSC.RequestMap & keyof JSC.ResponseMap>(
- method: M,
- params?: JSC.RequestMap[M],
- ): Promise<JSC.ResponseMap[M]> {
- return this.#inspector.send(method, params);
+ ["Adapter.event"](event: DAP.Event): void {
+ const { event: name, body } = event;
+ this.emit(`Adapter.${name}` as keyof DebugAdapterEventMap, body);
}
- async #emit<E extends keyof DAP.EventMap>(name: E, body?: DAP.EventMap[E]): Promise<void> {
- await this.#sendToAdapter({
- type: "event",
- seq: 0,
- event: name,
- body,
+ async #spawn(options: {
+ command: string;
+ args?: string[];
+ cwd?: string;
+ env?: Record<string, string>;
+ strictEnv?: boolean;
+ isDebugee?: boolean;
+ }): Promise<boolean> {
+ const { command, args = [], cwd, env = {}, strictEnv, isDebugee } = options;
+ const request = {
+ command,
+ args,
+ cwd,
+ env: strictEnv ? env : { ...process.env, ...env },
+ };
+ this.emit("Process.requested", request);
+
+ let subprocess: ChildProcess;
+ try {
+ subprocess = spawn(command, args, {
+ ...request,
+ stdio: ["ignore", "pipe", "pipe"],
+ });
+ } catch (cause) {
+ this.emit("Process.exited", new Error("Failed to spawn process", { cause }), null);
+ return false;
+ }
+
+ subprocess.on("spawn", () => {
+ this.emit("Process.spawned", subprocess);
+
+ if (isDebugee) {
+ this.#emit("process", {
+ name: `${command} ${args.join(" ")}`,
+ systemProcessId: subprocess.pid,
+ isLocalProcess: true,
+ startMethod: "launch",
+ });
+ }
+ });
+
+ subprocess.on("exit", (code, signal) => {
+ this.emit("Process.exited", code, signal);
+
+ if (isDebugee) {
+ this.#emit("exited", {
+ exitCode: code ?? -1,
+ });
+ }
+ });
+
+ subprocess.stdout?.on("data", data => {
+ this.emit("Process.stdout", data.toString());
+ });
+
+ subprocess.stderr?.on("data", data => {
+ this.emit("Process.stderr", data.toString());
+ });
+
+ return new Promise(resolve => {
+ subprocess.on("spawn", () => resolve(true));
+ subprocess.on("exit", () => resolve(false));
+ subprocess.on("error", () => resolve(false));
});
}
initialize(request: InitializeRequest): DAP.InitializeResponse {
const { clientID, supportsConfigurationDoneRequest } = (this.#initialized = request);
- this.#send("Inspector.enable");
- this.#send("Runtime.enable");
- this.#send("Console.enable");
- this.#send("Debugger.enable");
- this.#send("Debugger.setAsyncStackTraceDepth", { depth: 200 });
- this.#send("Debugger.setPauseOnDebuggerStatements", { enabled: true });
- this.#send("Debugger.setBlackboxBreakpointEvaluations", { blackboxBreakpointEvaluations: true });
- this.#send("Debugger.setBreakpointsActive", { active: true });
+ this.send("Inspector.enable");
+ this.send("Runtime.enable");
+ this.send("Console.enable");
+ this.send("Debugger.enable");
+ this.send("Debugger.setAsyncStackTraceDepth", { depth: 200 });
+ this.send("Debugger.setPauseOnDebuggerStatements", { enabled: true });
+ this.send("Debugger.setBlackboxBreakpointEvaluations", { blackboxBreakpointEvaluations: true });
+ this.send("Debugger.setBreakpointsActive", { active: true });
// If the client will not send a `configurationDone` request, then we need to
// tell the debugger that everything is ready.
if (!supportsConfigurationDoneRequest && clientID !== "vscode") {
- this.#send("Inspector.initialized");
+ this.send("Inspector.initialized");
}
// Tell the client what capabilities this adapter supports.
@@ -216,16 +342,16 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
// If the client requested that `noDebug` mode be enabled,
// then we need to disable all breakpoints and pause on statements.
if (this.#launched?.noDebug) {
- this.#send("Debugger.setBreakpointsActive", { active: false });
- this.#send("Debugger.setPauseOnExceptions", { state: "none" });
- this.#send("Debugger.setPauseOnDebuggerStatements", { enabled: false });
- this.#send("Debugger.setPauseOnMicrotasks", { enabled: false });
- this.#send("Debugger.setPauseForInternalScripts", { shouldPause: false });
- this.#send("Debugger.setPauseOnAssertions", { enabled: false });
+ this.send("Debugger.setBreakpointsActive", { active: false });
+ this.send("Debugger.setPauseOnExceptions", { state: "none" });
+ this.send("Debugger.setPauseOnDebuggerStatements", { enabled: false });
+ this.send("Debugger.setPauseOnMicrotasks", { enabled: false });
+ this.send("Debugger.setPauseForInternalScripts", { shouldPause: false });
+ this.send("Debugger.setPauseOnAssertions", { enabled: false });
}
// Tell the debugger that everything is ready.
- this.#send("Inspector.initialized");
+ this.send("Inspector.initialized");
}
async launch(request: DAP.LaunchRequest): Promise<void> {
@@ -241,11 +367,12 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
category: "stderr",
output: `Failed to start debugger.\n${message}`,
});
- this.#emit("terminated");
+ this.terminate();
}
}
async #launch(request: LaunchRequest): Promise<void> {
+ /*
if (this.#process?.exitCode === null) {
throw new Error("Another program is already running. Did you terminate the last session?");
}
@@ -281,76 +408,14 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
finalEnv["BUN_INSPECT"] = `1${this.#url}`;
finalEnv["BUN_INSPECT_NOTIFY"] = `unix://${this.#inspector.unix}`;
- if (isTest) {
+ if (true) {
finalEnv["FORCE_COLOR"] = "1";
} else {
// https://github.com/microsoft/vscode/issues/571
finalEnv["NO_COLOR"] = "1";
}
- const subprocess = spawn(runtime, [...finalArgs, program], {
- stdio: ["ignore", "pipe", "pipe"],
- cwd,
- env: finalEnv,
- });
-
- subprocess.on("spawn", () => {
- this.#process = subprocess;
- this.#emit("process", {
- name: program,
- systemProcessId: subprocess.pid,
- isLocalProcess: true,
- startMethod: "launch",
- });
- });
-
- subprocess.on("exit", (code, signal) => {
- this.#emit("exited", {
- exitCode: code ?? -1,
- });
- this.#process = undefined;
- });
-
- subprocess.stdout!.on("data", data => {
- const text = data.toString();
- this.#stdout?.(text);
-
- if (isTest) {
- this.#emit("output", {
- category: "stdout",
- output: text,
- source: {
- path: program,
- },
- });
- }
- });
-
- subprocess.stderr!.on("data", data => {
- const text = data.toString();
- this.#stderr?.(text);
-
- if (isTest) {
- this.#emit("output", {
- category: "stdout", // Not stderr, since VSCode will highlight it as red.
- output: text,
- source: {
- path: program,
- },
- });
- }
- });
-
- const start = new Promise<undefined>(resolve => {
- subprocess.on("spawn", () => resolve(undefined));
- });
-
- const exitOrError = new Promise<number | string | Error>(resolve => {
- subprocess.on("exit", (code, signal) => resolve(code ?? signal ?? -1));
- subprocess.on("error", resolve);
- });
-
- const reason = await Promise.race([start, exitOrError]);
+ let reason = undefined;
if (reason instanceof Error) {
const { message } = reason;
@@ -379,14 +444,10 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
throw new Error(`This extension requires Bun v${minVersion} or later. Please upgrade by running: bun upgrade`);
}
- throw new Error("Program started, but the debugger could not be attached.");
+ throw new Error("Program started, but the debugger could not be attached.");*/
}
async #start(url?: string | URL): Promise<boolean> {
- if (url) {
- this.#url = new URL(url);
- }
-
for (let i = 0; i < 5; i++) {
const ok = await this.#inspector.start(url);
if (ok) {
@@ -410,23 +471,13 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
category: "stderr",
output: `Failed to start debugger.\n${message}`,
});
- this.#emit("terminated");
+ this.terminate();
}
}
async #attach(request: AttachRequest): Promise<void> {
const { url } = request;
- if (this.#url.href === url) {
- this.#emit("output", {
- category: "debug console",
- output: "Debugger attached.\n",
- });
-
- this.configurationDone();
- return;
- }
-
if (await this.#start(url)) {
this.configurationDone();
return;
@@ -437,6 +488,7 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
terminate(): void {
this.#process?.kill();
+ this.#emit("terminated");
}
disconnect(request: DAP.DisconnectRequest): void {
@@ -453,7 +505,7 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
const { source } = request;
const { scriptId } = await this.#getSource(sourceToId(source));
- const { scriptSource } = await this.#send("Debugger.getScriptSource", { scriptId });
+ const { scriptSource } = await this.send("Debugger.getScriptSource", { scriptId });
return {
content: scriptSource,
@@ -472,27 +524,27 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
}
async pause(): Promise<void> {
- await this.#send("Debugger.pause");
+ await this.send("Debugger.pause");
this.#stopped = "pause";
}
async continue(): Promise<void> {
- await this.#send("Debugger.resume");
+ await this.send("Debugger.resume");
this.#stopped = undefined;
}
async next(): Promise<void> {
- await this.#send("Debugger.stepNext");
+ await this.send("Debugger.stepNext");
this.#stopped = "step";
}
async stepIn(): Promise<void> {
- await this.#send("Debugger.stepInto");
+ await this.send("Debugger.stepInto");
this.#stopped = "step";
}
async stepOut(): Promise<void> {
- await this.#send("Debugger.stepOut");
+ await this.send("Debugger.stepOut");
this.#stopped = "step";
}
@@ -505,7 +557,7 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
this.#generatedLocation(source, endLine ?? line + 1, endColumn),
]);
- const { locations } = await this.#send("Debugger.getBreakpointLocations", {
+ const { locations } = await this.send("Debugger.getBreakpointLocations", {
start,
end,
});
@@ -590,17 +642,27 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
const source = await this.#getSource(sourceId);
const oldBreakpoints = this.#getBreakpoints(sourceId);
+ console.log("OLD BREAKPOINTS", oldBreakpoints);
const breakpoints = await Promise.all(
requests!.map(async ({ line, column, ...options }) => {
- const breakpoint = this.#getBreakpoint(sourceId, line, column);
- if (breakpoint) {
- return breakpoint;
+ const location = this.#generatedLocation(source, line, column);
+ console.log("NEW BREAKPOINT", location);
+
+ for (const breakpoint of oldBreakpoints) {
+ const { generatedLocation } = breakpoint;
+ if (
+ location.lineNumber === generatedLocation.lineNumber &&
+ location.columnNumber === generatedLocation.columnNumber
+ ) {
+ console.log("SAME BREAKPOINT");
+ return breakpoint;
+ }
}
- const location = this.#generatedLocation(source, line, column);
+ console.log("CREATE BREAKPOINT");
try {
- const { breakpointId, actualLocation } = await this.#send("Debugger.setBreakpoint", {
+ const { breakpointId, actualLocation } = await this.send("Debugger.setBreakpoint", {
location,
options: breakpointOptions(options),
});
@@ -611,6 +673,7 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
breakpointId,
source,
verified: true,
+ generatedLocation: location,
...originalLocation,
});
} catch (error) {
@@ -626,6 +689,7 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
source,
verified: false,
message,
+ generatedLocation: location,
});
}
}),
@@ -635,7 +699,7 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
oldBreakpoints.map(async ({ breakpointId }) => {
const isRemoved = !breakpoints.filter(({ breakpointId: id }) => breakpointId === id).length;
if (isRemoved) {
- await this.#send("Debugger.removeBreakpoint", {
+ await this.send("Debugger.removeBreakpoint", {
breakpointId,
});
this.#removeBreakpoint(breakpointId);
@@ -661,18 +725,13 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
return breakpoints;
}
- #getBreakpoint(sourceId: string | number, line?: number, column?: number): Breakpoint | undefined {
- for (const breakpoint of this.#getBreakpoints(sourceId)) {
- if (isSameLocation(breakpoint, { line, column })) {
- return breakpoint;
- }
- }
- return undefined;
- }
-
#addBreakpoint(breakpoint: Breakpoint): Breakpoint {
this.#breakpoints.push(breakpoint);
+ // For now, remove the column from breakpoints because
+ // it can be inaccurate and causes weird rendering issues in VSCode.
+ breakpoint.column = this.#lineFrom0BasedLine(0);
+
this.#emit("breakpoint", {
reason: "changed",
breakpoint,
@@ -709,7 +768,7 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
}
try {
- await this.#send("Debugger.addSymbolicBreakpoint", {
+ await this.send("Debugger.addSymbolicBreakpoint", {
symbol: name,
caseSensitive: true,
isRegex: false,
@@ -737,7 +796,7 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
oldBreakpoints.map(async ({ name }) => {
const isRemoved = !breakpoints.filter(({ name: n }) => name === n).length;
if (isRemoved) {
- await this.#send("Debugger.removeSymbolicBreakpoint", {
+ await this.send("Debugger.removeSymbolicBreakpoint", {
symbol: name,
caseSensitive: true,
isRegex: false,
@@ -789,7 +848,7 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
filterIds.push(...filterOptions.map(({ filterId }) => filterId));
}
- await this.#send("Debugger.setPauseOnExceptions", {
+ await this.send("Debugger.setPauseOnExceptions", {
state: exceptionFiltersToPauseOnExceptionsState(filterIds),
});
}
@@ -818,7 +877,7 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
async #evaluate(expression: string, callFrameId?: string): Promise<JSC.Runtime.EvaluateResponse> {
const method = callFrameId ? "Debugger.evaluateOnCallFrame" : "Runtime.evaluate";
- return this.#send(method, {
+ return this.send(method, {
callFrameId,
expression: sanitizeExpression(expression),
generatePreview: true,
@@ -839,13 +898,6 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
}
["Inspector.connected"](): void {
- if (this.#connected) {
- this.restart();
- return;
- }
-
- this.#connected = true;
-
this.#emit("output", {
category: "debug console",
output: "Debugger attached.\n",
@@ -855,19 +907,19 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
}
async ["Inspector.disconnected"](error?: Error): Promise<void> {
- if (this.#connected && this.#process?.exitCode === null && (await this.#start())) {
- return;
- }
-
- if (!this.#connected) {
- return;
- }
-
this.#emit("output", {
category: "debug console",
output: "Debugger detached.\n",
});
+ if (error) {
+ const { message } = error;
+ this.#emit("output", {
+ category: "stderr",
+ output: `${message}\n`,
+ });
+ }
+
this.#emit("terminated");
this.#reset();
}
@@ -915,6 +967,11 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
["Debugger.scriptFailedToParse"](event: JSC.Debugger.ScriptFailedToParseEvent): void {
const { url, errorMessage, errorLine } = event;
+ // If no url is present, the script is from a `evaluate` request.
+ if (!url) {
+ return;
+ }
+
this.#emit("output", {
category: "stderr",
output: errorMessage,
@@ -931,7 +988,7 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
if (reason === "PauseOnNextStatement") {
for (const { functionName } of callFrames) {
if (functionName === "module code") {
- this.#send("Debugger.resume");
+ this.send("Debugger.resume");
return;
}
}
@@ -946,10 +1003,11 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
this.#addAsyncStackTrace(asyncStackTrace);
}
- let hitBreakpointIds: number[] | undefined;
// Depending on the reason, the `data` property is set to the reason
// why the execution was paused. For example, if the reason is "breakpoint",
// the `data` property is set to the breakpoint ID.
+ let hitBreakpointIds: number[] | undefined;
+
if (data) {
if (reason === "exception") {
const remoteObject = data as JSC.Runtime.RemoteObject;
@@ -1002,9 +1060,7 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
const variables = parameters.map((parameter, i) => {
const variable = this.#addVariable(parameter, { name: `${i}` });
-
output += remoteObjectToString(parameter, true) + " ";
-
return variable;
});
@@ -1361,7 +1417,7 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
return [];
}
- const { properties, internalProperties } = await this.#send("Runtime.getDisplayableProperties", {
+ const { properties, internalProperties } = await this.send("Runtime.getDisplayableProperties", {
objectId,
generatePreview: true,
});
@@ -1379,7 +1435,7 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
const hasEntries = type !== "array" && (indexedVariables || namedVariables);
if (hasEntries) {
- const { entries } = await this.#send("Runtime.getCollectionEntries", {
+ const { entries } = await this.send("Runtime.getCollectionEntries", {
objectId,
fetchStart: offset,
fetchCount: count,
@@ -1555,14 +1611,6 @@ function consoleMessageGroup(type: JSC.Console.ConsoleMessage["type"]): DAP.Outp
return undefined;
}
-function sourceToPath(source?: DAP.Source): string {
- const { path } = source ?? {};
- if (!path) {
- throw new Error("No source found.");
- }
- return path;
-}
-
function sourceToId(source?: DAP.Source): string | number {
const { path, sourceReference } = source ?? {};
if (path) {
@@ -1659,41 +1707,6 @@ function isTestJavaScript(path: string): boolean {
return /\.(test|spec)\.(c|m)?(j|t)sx?$/.test(path);
}
-function parseUrl(hostname?: string, port?: number): URL {
- hostname ||= "localhost";
- port ||= 6499;
- let url: URL;
- try {
- if (hostname.includes("://")) {
- url = new URL(hostname);
- } else if (hostname.includes(":") && !hostname.startsWith("[")) {
- url = new URL(`ws://[${hostname}]:${port}/`);
- } else {
- url = new URL(`ws://${hostname}:${port}/`);
- }
- } catch {
- throw new Error(`Invalid URL or hostname/port: ${hostname}`);
- }
- // HACK: Bun sometimes has issues connecting through "127.0.0.1"
- if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
- url.hostname = "[::1]";
- }
- return url;
-}
-
-function parseUrlMaybe(string: string): URL | undefined {
- const match = /(wss?:\/\/.*)/im.exec(string);
- if (!match) {
- return undefined;
- }
- const [_, href] = match;
- try {
- return parseUrl(href);
- } catch {
- return undefined;
- }
-}
-
function variablesSortBy(a: DAP.Variable, b: DAP.Variable): number {
const visibility = (variable: DAP.Variable): number => {
const { presentationHint } = variable;
diff --git a/packages/bun-debug-adapter-protocol/src/debugger/signal.ts b/packages/bun-debug-adapter-protocol/src/debugger/signal.ts
new file mode 100644
index 000000000..2a1b05938
--- /dev/null
+++ b/packages/bun-debug-adapter-protocol/src/debugger/signal.ts
@@ -0,0 +1,87 @@
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import type { Server } from "node:net";
+import { createServer } from "node:net";
+import { EventEmitter } from "node:events";
+
+const isDebug = process.env.NODE_ENV === "development";
+
+export type UnixSignalEventMap = {
+ "Signal.listening": [string];
+ "Signal.error": [Error];
+ "Signal.received": [string];
+ "Signal.closed": [];
+};
+
+/**
+ * Starts a server that listens for signals on a UNIX domain socket.
+ */
+export class UnixSignal extends EventEmitter<UnixSignalEventMap> {
+ #path: string;
+ #server: Server;
+ #ready: Promise<void>;
+
+ constructor(path?: string) {
+ super();
+ this.#path = path ? parseUnixPath(path) : randomUnixPath();
+ this.#server = createServer();
+ this.#server.on("listening", () => this.emit("Signal.listening", this.#path));
+ this.#server.on("error", error => this.emit("Signal.error", error));
+ this.#server.on("close", () => this.emit("Signal.closed"));
+ this.#server.on("connection", socket => {
+ socket.on("data", data => {
+ this.emit("Signal.received", data.toString());
+ });
+ });
+ this.#ready = new Promise((resolve, reject) => {
+ this.#server.on("listening", resolve);
+ this.#server.on("error", reject);
+ });
+ this.#server.listen(this.#path);
+ }
+
+ emit<E extends keyof UnixSignalEventMap>(event: E, ...args: UnixSignalEventMap[E]): boolean {
+ if (isDebug) {
+ console.log(event, ...args);
+ }
+
+ return super.emit(event, ...args);
+ }
+
+ /**
+ * The path to the UNIX domain socket.
+ */
+ get url(): string {
+ return `unix://${this.#path}`;
+ }
+
+ /**
+ * Resolves when the server is listening or rejects if an error occurs.
+ */
+ get ready(): Promise<void> {
+ return this.#ready;
+ }
+
+ /**
+ * Closes the server.
+ */
+ close(): void {
+ this.#server.close();
+ }
+}
+
+function randomUnixPath(): string {
+ return join(tmpdir(), `${Math.random().toString(36).slice(2)}.sock`);
+}
+
+function parseUnixPath(path: string): string {
+ if (path.startsWith("/")) {
+ return path;
+ }
+ try {
+ const { pathname } = new URL(path);
+ return pathname;
+ } catch {
+ throw new Error(`Invalid UNIX path: ${path}`);
+ }
+}
diff --git a/packages/bun-inspector-protocol/src/inspector/index.d.ts b/packages/bun-inspector-protocol/src/inspector/index.d.ts
index 7080f1dba..00c000189 100644
--- a/packages/bun-inspector-protocol/src/inspector/index.d.ts
+++ b/packages/bun-inspector-protocol/src/inspector/index.d.ts
@@ -1,10 +1,23 @@
-import type { JSC } from "..";
+import type { EventEmitter } from "node:events";
+import type { JSC } from "../protocol";
+
+export type InspectorEventMap = {
+ [E in keyof JSC.EventMap]: [JSC.EventMap[E]];
+} & {
+ "Inspector.connecting": [string];
+ "Inspector.connected": [];
+ "Inspector.disconnected": [Error | undefined];
+ "Inspector.error": [Error];
+ "Inspector.pendingRequest": [JSC.Request];
+ "Inspector.request": [JSC.Request];
+ "Inspector.response": [JSC.Response];
+ "Inspector.event": [JSC.Event];
+};
/**
* A client that can send and receive messages to/from a debugger.
*/
-export abstract class Inspector {
- constructor(listener?: InspectorListener);
+export interface Inspector extends EventEmitter<InspectorEventMap> {
/**
* Starts the inspector.
*/
@@ -17,11 +30,6 @@ export abstract class Inspector {
params?: JSC.RequestMap[M],
): Promise<JSC.ResponseMap[M]>;
/**
- * Accepts a message from the debugger.
- * @param message the unparsed message from the debugger
- */
- accept(message: string): void;
- /**
* If the inspector is closed.
*/
get closed(): boolean;
@@ -30,20 +38,3 @@ export abstract class Inspector {
*/
close(...args: unknown[]): void;
}
-
-export type InspectorListener = {
- /**
- * Defines a handler when a debugger event is received.
- */
- [M in keyof JSC.EventMap]?: (event: JSC.EventMap[M]) => void;
-} & {
- /**
- * Defines a handler when the debugger is connected or reconnected.
- */
- ["Inspector.connected"]?: () => void;
- /**
- * Defines a handler when the debugger is disconnected.
- * @param error the error that caused the disconnect, if any
- */
- ["Inspector.disconnected"]?: (error?: Error) => void;
-};
diff --git a/packages/bun-inspector-protocol/src/inspector/websocket.ts b/packages/bun-inspector-protocol/src/inspector/websocket.ts
index 238d3b2b7..0c203a11b 100644
--- a/packages/bun-inspector-protocol/src/inspector/websocket.ts
+++ b/packages/bun-inspector-protocol/src/inspector/websocket.ts
@@ -1,46 +1,42 @@
-import type { Inspector, InspectorListener } from ".";
+import type { Inspector, InspectorEventMap } from ".";
import type { JSC } from "../protocol";
+import { EventEmitter } from "node:events";
import { WebSocket } from "ws";
-import { createServer, type Server } from "node:net";
-import { tmpdir } from "node:os";
-
-export type WebSocketInspectorOptions = {
- url?: string | URL;
- listener?: InspectorListener;
- logger?: (...messages: unknown[]) => void;
-};
/**
* An inspector that communicates with a debugger over a WebSocket.
*/
-export class WebSocketInspector implements Inspector {
- #url?: URL;
+export class WebSocketInspector extends EventEmitter<InspectorEventMap> implements Inspector {
+ #url?: string;
#webSocket?: WebSocket;
#ready: Promise<boolean> | undefined;
#requestId: number;
- #pendingRequests: Map<number, (result: unknown) => void>;
- #pendingMessages: string[];
- #listener: InspectorListener;
- #log: (...messages: unknown[]) => void;
+ #pendingRequests: JSC.Request[];
+ #pendingResponses: Map<number, (result: unknown) => void>;
- constructor({ url, listener, logger }: WebSocketInspectorOptions) {
- this.#url = url ? new URL(url) : undefined;
+ constructor(url?: string | URL) {
+ super();
+ this.#url = url ? String(url) : undefined;
this.#requestId = 1;
- this.#pendingRequests = new Map();
- this.#pendingMessages = [];
- this.#listener = listener ?? {};
- this.#log = logger ?? (() => {});
+ this.#pendingRequests = [];
+ this.#pendingResponses = new Map();
+ }
+
+ get url(): string {
+ return this.#url!;
}
async start(url?: string | URL): Promise<boolean> {
if (url) {
- this.#url = new URL(url);
+ this.#url = String(url);
}
- if (this.#url) {
- const { href } = this.#url;
- return this.#connect(href);
+
+ if (!this.#url) {
+ this.emit("Inspector.error", new Error("Inspector needs a URL, but none was provided"));
+ return false;
}
- return false;
+
+ return this.#connect(this.#url);
}
async #connect(url: string): Promise<boolean> {
@@ -48,10 +44,12 @@ export class WebSocketInspector implements Inspector {
return this.#ready;
}
+ this.close(1001, "Restarting...");
+ this.emit("Inspector.connecting", url);
+
let webSocket: WebSocket;
try {
- this.#log("connecting:", url);
- // @ts-expect-error: Node.js
+ // @ts-expect-error: Support both Bun and Node.js version of `headers`.
webSocket = new WebSocket(url, {
headers: {
"Ref-Event-Loop": "1",
@@ -61,43 +59,43 @@ export class WebSocketInspector implements Inspector {
request.end();
},
});
- } catch (error) {
- this.#close(unknownToError(error));
+ } catch (cause) {
+ this.#close(unknownToError(cause));
return false;
}
webSocket.addEventListener("open", () => {
- this.#log("connected");
- for (const message of this.#pendingMessages) {
- this.#send(message);
+ this.emit("Inspector.connected");
+
+ for (const request of this.#pendingRequests) {
+ if (this.#send(request)) {
+ this.emit("Inspector.request", request);
+ }
}
- this.#pendingMessages.length = 0;
- this.#listener["Inspector.connected"]?.();
+
+ this.#pendingRequests.length = 0;
});
webSocket.addEventListener("message", ({ data }) => {
if (typeof data === "string") {
- this.accept(data);
+ this.#accept(data);
}
});
webSocket.addEventListener("error", event => {
- this.#log("error:", event);
this.#close(unknownToError(event));
});
webSocket.addEventListener("unexpected-response", () => {
- this.#log("unexpected-response");
this.#close(new Error("WebSocket upgrade failed"));
});
webSocket.addEventListener("close", ({ code, reason }) => {
- this.#log("closed:", code, reason);
- if (code === 1001) {
+ if (code === 1001 || code === 1006) {
this.#close();
- } else {
- this.#close(new Error(`WebSocket closed: ${code} ${reason}`.trimEnd()));
+ return;
}
+ this.#close(new Error(`WebSocket closed: ${code} ${reason}`.trimEnd()));
});
this.#webSocket = webSocket;
@@ -115,19 +113,20 @@ export class WebSocketInspector implements Inspector {
return ready;
}
- // @ts-ignore
send<M extends keyof JSC.RequestMap & keyof JSC.ResponseMap>(
method: M,
params?: JSC.RequestMap[M] | undefined,
): Promise<JSC.ResponseMap[M]> {
const id = this.#requestId++;
- const request = { id, method, params };
-
- this.#log("-->", request);
+ const request = {
+ id,
+ method,
+ params: params ?? {},
+ };
return new Promise((resolve, reject) => {
const done = (result: any) => {
- this.#pendingRequests.delete(id);
+ this.#pendingResponses.delete(id);
if (result instanceof Error) {
reject(result);
} else {
@@ -135,60 +134,62 @@ export class WebSocketInspector implements Inspector {
}
};
- this.#pendingRequests.set(id, done);
- this.#send(JSON.stringify(request));
+ this.#pendingResponses.set(id, done);
+ if (this.#send(request)) {
+ this.emit("Inspector.request", request);
+ } else {
+ this.emit("Inspector.pendingRequest", request);
+ }
});
}
- #send(message: string): void {
+ #send(request: JSC.Request): boolean {
if (this.#webSocket) {
const { readyState } = this.#webSocket!;
if (readyState === WebSocket.OPEN) {
- this.#webSocket.send(message);
+ this.#webSocket.send(JSON.stringify(request));
+ return true;
}
- return;
}
- if (!this.#pendingMessages.includes(message)) {
- this.#pendingMessages.push(message);
+ if (!this.#pendingRequests.includes(request)) {
+ this.#pendingRequests.push(request);
}
+ return false;
}
- accept(message: string): void {
- let event: JSC.Event | JSC.Response;
+ #accept(message: string): void {
+ let data: JSC.Event | JSC.Response;
try {
- event = JSON.parse(message);
- } catch (error) {
- this.#log("Failed to parse message:", message);
+ data = JSON.parse(message);
+ } catch (cause) {
+ this.emit("Inspector.error", new Error(`Failed to parse message: ${message}`, { cause }));
return;
}
- this.#log("<--", event);
-
- if (!("id" in event)) {
- const { method, params } = event;
- try {
- this.#listener[method]?.(params as any);
- } catch (error) {
- this.#log(`Failed to accept ${method} event:`, error);
- }
+ if (!("id" in data)) {
+ this.emit("Inspector.event", data);
+ const { method, params } = data;
+ this.emit(method, params);
return;
}
- const { id } = event;
- const resolve = this.#pendingRequests.get(id);
+ this.emit("Inspector.response", data);
+
+ const { id } = data;
+ const resolve = this.#pendingResponses.get(id);
if (!resolve) {
- this.#log("Failed to accept response with unknown ID:", id);
+ this.emit("Inspector.error", new Error(`Failed to find matching request for ID: ${id}`));
return;
}
- this.#pendingRequests.delete(id);
- if ("error" in event) {
- const { error } = event;
+ this.#pendingResponses.delete(id);
+ if ("error" in data) {
+ const { error } = data;
const { message } = error;
resolve(new Error(message));
} else {
- const { result } = event;
+ const { result } = data;
resolve(result);
}
}
@@ -213,54 +214,14 @@ export class WebSocketInspector implements Inspector {
}
#close(error?: Error): void {
- for (const resolve of this.#pendingRequests.values()) {
+ for (const resolve of this.#pendingResponses.values()) {
resolve(error ?? new Error("WebSocket closed"));
}
- this.#pendingRequests.clear();
- this.#listener["Inspector.disconnected"]?.(error);
- }
-}
-
-export class UnixWebSocketInspector extends WebSocketInspector {
- #unix: string;
- #server: Server;
- #ready: Promise<unknown>;
- startDebugging?: () => void;
-
- constructor(options: WebSocketInspectorOptions) {
- super(options);
- this.#unix = unixSocket();
- this.#server = createServer();
- this.#server.listen(this.#unix);
- this.#ready = this.#wait().then(() => {
- setTimeout(() => {
- this.start().then(() => this.startDebugging?.());
- }, 1);
- });
- }
-
- get unix(): string {
- return this.#unix;
- }
-
- #wait(): Promise<void> {
- return new Promise(resolve => {
- console.log("waiting");
- this.#server.once("connection", socket => {
- console.log("received");
- socket.once("data", resolve);
- });
- });
- }
-
- async start(url?: string | URL): Promise<boolean> {
- await this.#ready;
- try {
- console.log("starting");
- return await super.start(url);
- } finally {
- this.#ready = this.#wait();
+ this.#pendingResponses.clear();
+ if (error) {
+ this.emit("Inspector.error", error);
}
+ this.emit("Inspector.disconnected", error);
}
}
@@ -276,7 +237,3 @@ function unknownToError(input: unknown): Error {
return new Error(`${input}`);
}
-
-function unixSocket(): string {
- return `${tmpdir()}/bun-inspect-${Math.random().toString(36).slice(2)}.sock`;
-}
diff --git a/packages/bun-inspector-protocol/test/inspector/websocket.test.ts b/packages/bun-inspector-protocol/test/inspector/websocket.test.ts
new file mode 100644
index 000000000..4a6c60c28
--- /dev/null
+++ b/packages/bun-inspector-protocol/test/inspector/websocket.test.ts
@@ -0,0 +1,190 @@
+import { describe, test, expect, mock, beforeAll, afterAll } from "bun:test";
+import { WebSocketInspector } from "../../src/inspector/websocket";
+import type { Server } from "bun";
+import { serve } from "bun";
+
+let server: Server;
+let url: URL;
+
+describe("WebSocketInspector", () => {
+ test("fails without a URL", () => {
+ const ws = new WebSocketInspector();
+ const fn = mock(error => {
+ expect(error).toBeInstanceOf(Error);
+ });
+ ws.on("Inspector.error", fn);
+ expect(ws.start()).resolves.toBeFalse();
+ expect(fn).toHaveBeenCalled();
+ });
+
+ test("fails with invalid URL", () => {
+ const ws = new WebSocketInspector("notaurl");
+ const fn = mock(error => {
+ expect(error).toBeInstanceOf(Error);
+ });
+ ws.on("Inspector.error", fn);
+ expect(ws.start()).resolves.toBeFalse();
+ expect(fn).toHaveBeenCalled();
+ });
+
+ test("fails with valid URL but no server", () => {
+ const ws = new WebSocketInspector("ws://localhost:0/doesnotexist/");
+ const fn = mock(error => {
+ expect(error).toBeInstanceOf(Error);
+ });
+ ws.on("Inspector.error", fn);
+ expect(ws.start()).resolves.toBeFalse();
+ expect(fn).toHaveBeenCalled();
+ });
+
+ test("fails with invalid upgrade response", () => {
+ const ws = new WebSocketInspector(new URL("/", url));
+ const fn = mock(error => {
+ expect(error).toBeInstanceOf(Error);
+ });
+ ws.on("Inspector.error", fn);
+ expect(ws.start()).resolves.toBeFalse();
+ expect(fn).toHaveBeenCalled();
+ });
+
+ test("can connect to a server", () => {
+ const ws = new WebSocketInspector(url);
+ const fn = mock(() => {
+ expect(ws.closed).toBe(false);
+ });
+ ws.on("Inspector.connected", fn);
+ expect(ws.start()).resolves.toBeTrue();
+ expect(fn).toHaveBeenCalled();
+ ws.close();
+ });
+
+ test("can disconnect from a server", () => {
+ const ws = new WebSocketInspector(url);
+ const fn = mock(() => {
+ expect(ws.closed).toBeTrue();
+ });
+ ws.on("Inspector.disconnected", fn);
+ expect(ws.start()).resolves.toBeTrue();
+ ws.close();
+ expect(fn).toHaveBeenCalled();
+ });
+
+ test("can connect to a server multiple times", () => {
+ const ws = new WebSocketInspector(url);
+ const fn0 = mock(() => {
+ expect(ws.closed).toBeFalse();
+ });
+ ws.on("Inspector.connected", fn0);
+ const fn1 = mock(() => {
+ expect(ws.closed).toBeTrue();
+ });
+ ws.on("Inspector.disconnected", fn1);
+ for (let i = 0; i < 3; i++) {
+ expect(ws.start()).resolves.toBeTrue();
+ ws.close();
+ }
+ expect(fn0).toHaveBeenCalledTimes(3);
+ expect(fn1).toHaveBeenCalledTimes(3);
+ });
+
+ test("can send a request", () => {
+ const ws = new WebSocketInspector(url);
+ const fn0 = mock(request => {
+ expect(request).toStrictEqual({
+ id: 1,
+ method: "Debugger.setPauseOnAssertions",
+ params: {
+ enabled: true,
+ },
+ });
+ });
+ ws.on("Inspector.request", fn0);
+ const fn1 = mock(response => {
+ expect(response).toStrictEqual({
+ id: 1,
+ result: {
+ ok: true,
+ },
+ });
+ });
+ ws.on("Inspector.response", fn1);
+ expect(ws.start()).resolves.toBeTrue();
+ expect(ws.send("Debugger.setPauseOnAssertions", { enabled: true })).resolves.toMatchObject({ ok: true });
+ expect(fn0).toHaveBeenCalled();
+ expect(fn1).toHaveBeenCalled();
+ ws.close();
+ });
+
+ test("can send a request before connecting", () => {
+ const ws = new WebSocketInspector(url);
+ const fn0 = mock(request => {
+ expect(request).toStrictEqual({
+ id: 1,
+ method: "Runtime.enable",
+ params: {},
+ });
+ });
+ ws.on("Inspector.pendingRequest", fn0);
+ ws.on("Inspector.request", fn0);
+ const fn1 = mock(response => {
+ expect(response).toStrictEqual({
+ id: 1,
+ result: {
+ ok: true,
+ },
+ });
+ });
+ ws.on("Inspector.response", fn1);
+ const request = ws.send("Runtime.enable");
+ expect(ws.start()).resolves.toBe(true);
+ expect(request).resolves.toMatchObject({ ok: true });
+ expect(fn0).toHaveBeenCalledTimes(2);
+ expect(fn1).toHaveBeenCalled();
+ ws.close();
+ });
+
+ test("can receive an event", () => {
+ const ws = new WebSocketInspector(url);
+ const fn = mock(event => {
+ expect(event).toStrictEqual({
+ method: "Debugger.scriptParsed",
+ params: {
+ scriptId: "1",
+ },
+ });
+ });
+ ws.on("Inspector.event", fn);
+ expect(ws.start()).resolves.toBeTrue();
+ expect(ws.send("Debugger.enable")).resolves.toMatchObject({ ok: true });
+ expect(fn).toHaveBeenCalled();
+ ws.close();
+ });
+});
+
+beforeAll(() => {
+ server = serve({
+ port: 0,
+ fetch(request, server) {
+ if (request.url.endsWith("/ws") && server.upgrade(request)) {
+ return;
+ }
+ return new Response();
+ },
+ websocket: {
+ message(ws, message) {
+ const { id, method } = JSON.parse(String(message));
+ ws.send(JSON.stringify({ id, result: { ok: true } }));
+
+ if (method === "Debugger.enable") {
+ ws.send(JSON.stringify({ method: "Debugger.scriptParsed", params: { scriptId: "1" } }));
+ }
+ },
+ },
+ });
+ const { hostname, port } = server;
+ url = new URL(`ws://${hostname}:${port}/ws`);
+});
+
+afterAll(() => {
+ server?.stop(true);
+});
diff --git a/packages/bun-inspector-protocol/test/util/__snapshots__/preview.test.ts.snap b/packages/bun-inspector-protocol/test/util/__snapshots__/preview.test.ts.snap
deleted file mode 100644
index 0acc17575..000000000
--- a/packages/bun-inspector-protocol/test/util/__snapshots__/preview.test.ts.snap
+++ /dev/null
@@ -1,143 +0,0 @@
-// Bun Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`remoteObjectToString 1`] = `"undefined"`;
-
-exports[`remoteObjectToString 2`] = `"null"`;
-
-exports[`remoteObjectToString 3`] = `"true"`;
-
-exports[`remoteObjectToString 4`] = `"false"`;
-
-exports[`remoteObjectToString 5`] = `"0"`;
-
-exports[`remoteObjectToString 6`] = `"1"`;
-
-exports[`remoteObjectToString 7`] = `"3.141592653589793"`;
-
-exports[`remoteObjectToString 8`] = `"-2.718281828459045"`;
-
-exports[`remoteObjectToString 9`] = `"NaN"`;
-
-exports[`remoteObjectToString 10`] = `"Infinity"`;
-
-exports[`remoteObjectToString 11`] = `"-Infinity"`;
-
-exports[`remoteObjectToString 12`] = `"0n"`;
-
-exports[`remoteObjectToString 13`] = `"1n"`;
-
-exports[`remoteObjectToString 14`] = `"10000000000000n"`;
-
-exports[`remoteObjectToString 15`] = `"-10000000000000n"`;
-
-exports[`remoteObjectToString 16`] = `""""`;
-
-exports[`remoteObjectToString 17`] = `"" ""`;
-
-exports[`remoteObjectToString 18`] = `""Hello""`;
-
-exports[`remoteObjectToString 19`] = `""Hello World""`;
-
-exports[`remoteObjectToString 20`] = `"Array(0)"`;
-
-exports[`remoteObjectToString 21`] = `"Array(3) [1, 2, 3]"`;
-
-exports[`remoteObjectToString 22`] = `"Array(4) ["a", 1, null, undefined]"`;
-
-exports[`remoteObjectToString 23`] = `"Array(2) [1, Array]"`;
-
-exports[`remoteObjectToString 24`] = `"Array(1) [Array]"`;
-
-exports[`remoteObjectToString 25`] = `"{}"`;
-
-exports[`remoteObjectToString 26`] = `"{a: 1}"`;
-
-exports[`remoteObjectToString 27`] = `"{a: 1, b: 2, c: 3}"`;
-
-exports[`remoteObjectToString 28`] = `"{a: Object}"`;
-
-exports[`remoteObjectToString 29`] = `
-"ƒ() {
-}"
-`;
-
-exports[`remoteObjectToString 30`] = `
-"ƒ namedFunction() {
-}"
-`;
-
-exports[`remoteObjectToString 31`] = `
-"class {
-}"
-`;
-
-exports[`remoteObjectToString 32`] = `
-"class namedClass {
-}"
-`;
-
-exports[`remoteObjectToString 33`] = `
-"class namedClass {
- a() {
- }
- b = 1;
- c = [
- null,
- undefined,
- "a",
- {
- a: 1,
- b: 2,
- c: 3
- }
- ];
-}"
-`;
-
-exports[`remoteObjectToString 34`] = `"Wed Dec 31 1969 16:00:00 GMT-0800 (Pacific Standard Time)"`;
-
-exports[`remoteObjectToString 35`] = `"Invalid Date"`;
-
-exports[`remoteObjectToString 36`] = `"/(?:)/"`;
-
-exports[`remoteObjectToString 37`] = `"/abc/"`;
-
-exports[`remoteObjectToString 38`] = `"/abc/g"`;
-
-exports[`remoteObjectToString 39`] = `"/abc/"`;
-
-exports[`remoteObjectToString 40`] = `"Set(0)"`;
-
-exports[`remoteObjectToString 41`] = `"Set(3) [1, 2, 3]"`;
-
-exports[`remoteObjectToString 42`] = `"WeakSet(0)"`;
-
-exports[`remoteObjectToString 43`] = `"WeakSet(3) [{a: 1}, {b: 2}, {c: 3}]"`;
-
-exports[`remoteObjectToString 44`] = `"Map(0)"`;
-
-exports[`remoteObjectToString 45`] = `"Map(3) {"a" => 1, "b" => 2, "c" => 3}"`;
-
-exports[`remoteObjectToString 46`] = `"WeakMap(0)"`;
-
-exports[`remoteObjectToString 47`] = `"WeakMap(3) {{a: 1} => 1, {b: 2} => 2, {c: 3} => 3}"`;
-
-exports[`remoteObjectToString 48`] = `"Symbol()"`;
-
-exports[`remoteObjectToString 49`] = `"Symbol(namedSymbol)"`;
-
-exports[`remoteObjectToString 50`] = `"Error"`;
-
-exports[`remoteObjectToString 51`] = `"TypeError: This is a TypeError"`;
-
-exports[`remoteObjectToString 52`] = `"Headers {append: ƒ, delete: ƒ, get: ƒ, getAll: ƒ, has: ƒ, …}"`;
-
-exports[`remoteObjectToString 53`] = `"Headers {a: "1", append: ƒ, b: "2", delete: ƒ, get: ƒ, …}"`;
-
-exports[`remoteObjectToString 54`] = `"Request {arrayBuffer: ƒ, blob: ƒ, body: null, bodyUsed: false, cache: "default", …}"`;
-
-exports[`remoteObjectToString 55`] = `"Request {arrayBuffer: ƒ, blob: ƒ, body: ReadableStream, bodyUsed: false, cache: "default", …}"`;
-
-exports[`remoteObjectToString 56`] = `"Response {arrayBuffer: ƒ, blob: ƒ, body: null, bodyUsed: false, clone: ƒ, …}"`;
-
-exports[`remoteObjectToString 57`] = `"Response {arrayBuffer: ƒ, blob: ƒ, body: ReadableStream, bodyUsed: false, clone: ƒ, …}"`;
diff --git a/packages/bun-inspector-protocol/test/util/preview.js b/packages/bun-inspector-protocol/test/util/preview.js
deleted file mode 100644
index 15062240b..000000000
--- a/packages/bun-inspector-protocol/test/util/preview.js
+++ /dev/null
@@ -1,99 +0,0 @@
-console.log(
- undefined,
- null,
- true,
- false,
- 0,
- 1,
- Math.PI,
- -Math.E,
- NaN,
- Infinity,
- -Infinity,
- BigInt(0),
- BigInt(1),
- BigInt("10000000000000"),
- BigInt("-10000000000000"),
- "",
- " ",
- "Hello",
- "Hello World",
- [],
- [1, 2, 3],
- ["a", 1, null, undefined],
- [1, [2, [3, [4, [5, [6, [7, [8, [9, [10]]]]]]]]]],
- [[[[[]]]]],
- {},
- { a: 1 },
- { a: 1, b: 2, c: 3 },
- { a: { b: { c: { d: { e: { f: { g: { h: { i: { j: 10 } } } } } } } } } },
- function () {},
- function namedFunction() {},
- class {},
- class namedClass {},
- class namedClass {
- a() {}
- b = 1;
- c = [
- null,
- undefined,
- "a",
- {
- a: 1,
- b: 2,
- c: 3,
- },
- ];
- },
- new Date(0),
- new Date(NaN),
- new RegExp(),
- new RegExp("abc"),
- new RegExp("abc", "g"),
- /abc/,
- new Set(),
- new Set([1, 2, 3]),
- new WeakSet(),
- new WeakSet([{ a: 1 }, { b: 2 }, { c: 3 }]),
- new Map(),
- new Map([
- ["a", 1],
- ["b", 2],
- ["c", 3],
- ]),
- new WeakMap(),
- new WeakMap([
- [{ a: 1 }, 1],
- [{ b: 2 }, 2],
- [{ c: 3 }, 3],
- ]),
- Symbol(),
- Symbol("namedSymbol"),
- new Error(),
- new TypeError("This is a TypeError"),
- //"a".repeat(10000),
- //["a"].fill("a", 0, 10000),
- new Headers(),
- new Headers({
- a: "1",
- b: "2",
- }),
- new Request("https://example.com/"),
- new Request("https://example.com/", {
- method: "POST",
- headers: {
- a: "1",
- b: "2",
- },
- body: '{"example":true}',
- }),
- new Response(),
- new Response('{"example":true}', {
- status: 200,
- statusText: "OK",
- headers: {
- a: "1",
- b: "2",
- },
- }),
-);
diff --git a/packages/bun-inspector-protocol/test/util/preview.test.ts b/packages/bun-inspector-protocol/test/util/preview.test.ts
deleted file mode 100644
index 99214ef0e..000000000
--- a/packages/bun-inspector-protocol/test/util/preview.test.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import { beforeAll, afterAll, test, expect } from "bun:test";
-import type { PipedSubprocess } from "bun";
-import { spawn } from "bun";
-import type { JSC } from "../..";
-import { WebSocketInspector, remoteObjectToString } from "../..";
-
-let subprocess: PipedSubprocess | undefined;
-let objects: JSC.Runtime.RemoteObject[] = [];
-
-beforeAll(async () => {
- subprocess = spawn({
- cwd: import.meta.dir,
- cmd: [process.argv0, "--inspect-wait=0", "preview.js"],
- stdout: "pipe",
- stderr: "pipe",
- stdin: "pipe",
- });
- const decoder = new TextDecoder();
- let url: URL;
- for await (const chunk of subprocess!.stdout) {
- const text = decoder.decode(chunk);
- if (text.includes("ws://")) {
- url = new URL(/(ws:\/\/.*)/.exec(text)![0]);
- break;
- }
- }
- objects = await new Promise((resolve, reject) => {
- const inspector = new WebSocketInspector({
- url,
- listener: {
- ["Inspector.connected"]: () => {
- inspector.send("Inspector.enable");
- inspector.send("Runtime.enable");
- inspector.send("Console.enable");
- inspector.send("Debugger.enable");
- inspector.send("Debugger.resume");
- inspector.send("Inspector.initialized");
- },
- ["Inspector.disconnected"]: error => {
- reject(error);
- },
- ["Console.messageAdded"]: ({ message }) => {
- const { parameters } = message;
- resolve(parameters!);
- inspector.close();
- },
- },
- });
- inspector.start();
- });
-});
-
-afterAll(() => {
- subprocess?.kill();
-});
-
-test("remoteObjectToString", () => {
- for (const object of objects) {
- expect(remoteObjectToString(object)).toMatchSnapshot();
- }
-});
diff --git a/packages/bun-vscode/src/features/debug.ts b/packages/bun-vscode/src/features/debug.ts
index eae2b1c33..91e175413 100644
--- a/packages/bun-vscode/src/features/debug.ts
+++ b/packages/bun-vscode/src/features/debug.ts
@@ -1,9 +1,8 @@
import * as vscode from "vscode";
import type { CancellationToken, DebugConfiguration, ProviderResult, WorkspaceFolder } from "vscode";
import type { DAP } from "../../../bun-debug-adapter-protocol";
-import { DebugAdapter } from "../../../bun-debug-adapter-protocol";
+import { DebugAdapter, UnixSignal } from "../../../bun-debug-adapter-protocol";
import { DebugSession } from "@vscode/debugadapter";
-import { inspect } from "node:util";
import { tmpdir } from "node:os";
const debugConfiguration: vscode.DebugConfiguration = {
@@ -110,7 +109,7 @@ class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory
const { configuration } = session;
const { request, url } = configuration;
- if (request === "attach" && url === terminal?.url) {
+ if (request === "attach" && url === terminal?.adapter.url) {
return new vscode.DebugAdapterInlineImplementation(terminal);
}
@@ -120,47 +119,27 @@ class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory
}
class FileDebugSession extends DebugSession {
- readonly url: string;
readonly adapter: DebugAdapter;
+ readonly signal: UnixSignal;
constructor(sessionId?: string) {
super();
const uniqueId = sessionId ?? Math.random().toString(36).slice(2);
- this.url = `ws+unix://${tmpdir()}/bun-vscode-${uniqueId}.sock`;
- this.adapter = new DebugAdapter({
- url: this.url,
- send: this.sendMessage.bind(this),
- logger(...messages) {
- log("jsc", ...messages);
- },
- stdout(message) {
- log("console", message);
- },
- stderr(message) {
- log("console", message);
- },
- });
+ this.adapter = new DebugAdapter(`ws+unix://${tmpdir()}/${uniqueId}.sock`);
+ this.adapter.on("Adapter.response", response => this.sendResponse(response));
+ this.adapter.on("Adapter.event", event => this.sendEvent(event));
+ this.signal = new UnixSignal();
}
- sendMessage(message: DAP.Request | DAP.Response | DAP.Event): void {
- log("dap", "-->", message);
-
+ handleMessage(message: DAP.Event | DAP.Request | DAP.Response): void {
const { type } = message;
- if (type === "response") {
- this.sendResponse(message);
- } else if (type === "event") {
- this.sendEvent(message);
+ if (type === "request") {
+ this.adapter.emit("Adapter.request", message);
} else {
throw new Error(`Not supported: ${type}`);
}
}
- handleMessage(message: DAP.Event | DAP.Request | DAP.Response): void {
- log("dap", "<--", message);
-
- this.adapter.accept(message);
- }
-
dispose() {
this.adapter.close();
}
@@ -174,26 +153,19 @@ class TerminalDebugSession extends FileDebugSession {
this.terminal = vscode.window.createTerminal({
name: "Bun Terminal",
env: {
- "BUN_INSPECT": `1${this.url}`,
- "BUN_INSPECT_NOTIFY": `unix://${this.adapter.inspector.unix}`,
+ "BUN_INSPECT": `1${this.adapter.url}`,
+ "BUN_INSPECT_NOTIFY": `${this.signal.url}`,
},
isTransient: true,
iconPath: new vscode.ThemeIcon("debug-console"),
});
this.terminal.show();
- this.adapter.inspector.startDebugging = () => {
+ this.signal.on("Signal.received", () => {
vscode.debug.startDebugging(undefined, {
...attachConfiguration,
- url: this.url,
+ url: this.adapter.url,
});
- };
- }
-}
-
-function log(channel: string, ...message: unknown[]): void {
- if (process.env.NODE_ENV === "development") {
- console.log(`[${channel}]`, ...message);
- channels[channel]?.appendLine(message.map(v => inspect(v)).join(" "));
+ });
}
}