Skip to content

Commit

Permalink
debugger using websockets
Browse files Browse the repository at this point in the history
  • Loading branch information
snoglobe committed Sep 18, 2024
1 parent 6d98bcc commit 49090cf
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 186 deletions.
147 changes: 51 additions & 96 deletions packages/bun-debug-adapter-protocol/src/debugger/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,19 @@ import type { InspectorEventMap } from "../../../bun-inspector-protocol/src/insp
import type { JSC } from "../../../bun-inspector-protocol/src/protocol";
import type { DAP } from "../protocol";
// @ts-ignore
import type { ChildProcess } from "node:child_process";
import { spawn } from "node:child_process";
import { spawn, ChildProcess } from "node:child_process";
import { EventEmitter } from "node:events";
import { WebSocketInspector, remoteObjectToString } from "../../../bun-inspector-protocol/index";
import { UnixSignal, randomUnixPath } from "./signal";
import { WebSocketSignal } from "./signal";
import { Location, SourceMap } from "./sourcemap";
import { createServer, AddressInfo } from "node:net";

export function getAvailablePort(): number {
const server = createServer().listen(0);
const port = (server.address() as AddressInfo).port;
server.close();
return port;
}

const capabilities: DAP.Capabilities = {
supportsConfigurationDoneRequest: true,
Expand Down Expand Up @@ -215,6 +222,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
#variables: Map<number, Variable>;
#initialized?: InitializeRequest;
#options?: DebuggerOptions;
#signal?: WebSocketSignal;

constructor(url?: string | URL) {
super();
Expand Down Expand Up @@ -466,10 +474,6 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
throw new Error("No program specified. Did you set the 'program' property in your launch.json?");
}

if (!isJavaScript(program)) {
throw new Error("Program must be a JavaScript or TypeScript file.");
}

const processArgs = [...runtimeArgs, program, ...args];

if (isTestJavaScript(program) && !runtimeArgs.includes("test")) {
Expand All @@ -480,108 +484,59 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
processArgs.unshift(watchMode === "hot" ? "--hot" : "--watch");
}

const processEnv = strictEnv
? {
...env,
}
: {
...process.env,
...env,
};

const url = `ws+unix://${randomUnixPath()}`;
const signal = new UnixSignal();
const processEnv: Record<string, string> = strictEnv ? { ...env } : { ...process.env, ...env };

signal.on("Signal.received", () => {
this.#attach({ url });
});
// Create WebSocketSignal
const signalUrl = `ws://localhost:${getAvailablePort()}`;
this.#signal = new WebSocketSignal(signalUrl);

this.once("Adapter.terminated", () => {
signal.close();
});
// Set BUN_INSPECT_NOTIFY to signal's URL
processEnv["BUN_INSPECT_NOTIFY"] = this.#signal.url;

const query = stopOnEntry ? "break=1" : "wait=1";
processEnv["BUN_INSPECT"] = `${url}?${query}`;
processEnv["BUN_INSPECT_NOTIFY"] = signal.url;
// Set BUN_INSPECT to the debugger's inspector URL with ?wait=1
processEnv["BUN_INSPECT"] = `${this.#inspector.url}?wait=1`;

// This is probably not correct, but it's the best we can do for now.
processEnv["FORCE_COLOR"] = "1";
processEnv["BUN_QUIET_DEBUG_LOGS"] = "1";
processEnv["BUN_DEBUG_QUIET_LOGS"] = "1";

const started = await this.#spawn({
command: runtime,
args: processArgs,
env: processEnv,
// Spawn the process
const child = spawn(runtime, processArgs, {
cwd,
isDebugee: true,
env: processEnv,
stdio: ["ignore", "pipe", "pipe"],
});

if (!started) {
throw new Error("Program could not be started.");
}
}

async #spawn(options: {
command: string;
args?: string[];
cwd?: string;
env?: Record<string, string | undefined>;
isDebugee?: boolean;
}): Promise<boolean> {
const { command, args = [], cwd, env, isDebugee } = options;
const request = { command, args, cwd, env };
this.emit("Process.requested", request);
this.#process = child;

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.#process = subprocess;
this.#emit("process", {
name: `${command} ${args.join(" ")}`,
systemProcessId: subprocess.pid,
isLocalProcess: true,
startMethod: "launch",
});
}
// Attach event listeners for the process
child.stderr?.setEncoding("utf-8");
child.stderr?.on("data", data => this.emit("Process.stderr", data));
child.on("exit", (code, signal) => {
this.emit("Process.exited", code ?? signal ?? null, signal ?? null);
});

subprocess.on("exit", (code, signal) => {
this.emit("Process.exited", code, signal);

if (isDebugee) {
this.#process = undefined;
this.#emit("exited", {
exitCode: code ?? -1,
});
this.#emit("terminated");
}
});
this.emit("Process.spawned", child);

subprocess.stdout?.on("data", data => {
this.emit("Process.stdout", data.toString());
// Wait for the signal from the debuggee
await this.#signal.ready;
await new Promise<void>((resolve, reject) => {
this.#signal!.once("Signal.received", () => {
resolve();
});
this.#signal!.once("Signal.error", error => {
reject(error);
});
});

subprocess.stderr?.on("data", data => {
this.emit("Process.stderr", data.toString());
});
// Connect the debugger to the debuggee
const connected = await this.#inspector.start();
if (!connected) {
this.terminate();
}

return new Promise(resolve => {
subprocess.on("spawn", () => resolve(true));
subprocess.on("exit", () => resolve(false));
subprocess.on("error", () => resolve(false));
// Additional setup if needed
this.emit("Adapter.process", {
name: `${runtime} ${processArgs.join(" ")}`,
systemProcessId: child.pid,
isLocalProcess: true,
startMethod: "launch",
});
}

Expand Down Expand Up @@ -2107,7 +2062,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements

close(): void {
this.#process?.kill();
// this.#signal?.close();
this.#signal?.close();
this.#inspector.close();
this.#reset();
}
Expand Down
50 changes: 20 additions & 30 deletions packages/bun-debug-adapter-protocol/src/debugger/signal.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,55 @@
import { EventEmitter } from "node:events";
import type { Server } from "node:net";
import { createServer } from "node:net";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { WebSocketServer } from "ws";

const isDebug = process.env.NODE_ENV === "development";

export type UnixSignalEventMap = {
export type WebSocketSignalEventMap = {
"Signal.listening": [string];
"Signal.error": [Error];
"Signal.received": [string];
"Signal.closed": [];
};

/**
* Starts a server that listens for signals on a UNIX domain socket.
* Starts a server that listens for signals over a WebSocket.
*/
export class UnixSignal extends EventEmitter<UnixSignalEventMap> {
#path: string;
#server: Server;
export class WebSocketSignal extends EventEmitter<WebSocketSignalEventMap> {
#url: string;
#server: WebSocketServer;
#ready: Promise<void>;

constructor(path?: string | URL) {
constructor(url: string) {
super();
this.#path = path ? parseUnixPath(path) : randomUnixPath();
this.#server = createServer();
this.#server.on("listening", () => this.emit("Signal.listening", this.#path));
this.#url = url;
const port = getPortFromUrl(url);
this.#server = new WebSocketServer({ port });

this.#server.on("listening", () => this.emit("Signal.listening", this.#url));
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 => {
socket.on("message", 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 {
emit<E extends keyof WebSocketSignalEventMap>(event: E, ...args: WebSocketSignalEventMap[E]): boolean {
if (isDebug) {
console.log(event, ...args);
}

return super.emit(event, ...args);
}

/**
* The path to the UNIX domain socket.
* The WebSocket URL.
*/
get url(): string {
return `unix://${this.#path}`;
return this.#url;
}

/**
Expand All @@ -70,18 +67,11 @@ export class UnixSignal extends EventEmitter<UnixSignalEventMap> {
}
}

export function randomUnixPath(): string {
return join(tmpdir(), `${Math.random().toString(36).slice(2)}.sock`);
}

function parseUnixPath(path: string | URL): string {
if (typeof path === "string" && path.startsWith("/")) {
return path;
}
function getPortFromUrl(url: string): number {
try {
const { pathname } = new URL(path);
return pathname;
const parsedUrl = new URL(url);
return parseInt(parsedUrl.port, 10) || 0;
} catch {
throw new Error(`Invalid UNIX path: ${path}`);
throw new Error(`Invalid WebSocket URL: ${url}`);
}
}
2 changes: 1 addition & 1 deletion packages/bun-vscode/scripts/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ buildSync({
entryPoints: ["src/extension.ts", "src/web-extension.ts"],
outdir: "dist",
bundle: true,
external: ["vscode"],
external: ["vscode", "ws"],
platform: "node",
format: "cjs",
// The following settings are required to allow for extension debugging
Expand Down
10 changes: 5 additions & 5 deletions packages/bun-vscode/src/features/debug.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { DebugSession } from "@vscode/debugadapter";
import { tmpdir } from "node:os";
import * as vscode from "vscode";
import type { DAP } from "../../../bun-debug-adapter-protocol";
import { DebugAdapter, UnixSignal } from "../../../bun-debug-adapter-protocol";
import { DebugAdapter, getAvailablePort, WebSocketSignal } from "../../../bun-debug-adapter-protocol";

export const DEBUG_CONFIGURATION: vscode.DebugConfiguration = {
type: "bun",
Expand Down Expand Up @@ -176,7 +175,7 @@ class FileDebugSession extends DebugSession {
constructor(sessionId?: string) {
super();
const uniqueId = sessionId ?? Math.random().toString(36).slice(2);
const url = `ws+unix://${tmpdir()}/${uniqueId}.sock`;
const url = `ws://localhost:${getAvailablePort()}`;

this.adapter = new DebugAdapter(url);
this.adapter.on("Adapter.response", response => this.sendResponse(response));
Expand Down Expand Up @@ -204,11 +203,12 @@ class FileDebugSession extends DebugSession {
}

class TerminalDebugSession extends FileDebugSession {
readonly signal: UnixSignal;
readonly signal: WebSocketSignal;

constructor() {
super();
this.signal = new UnixSignal();
const signalUrl = `ws://localhost:${getAvailablePort()}`;
this.signal = new WebSocketSignal(signalUrl);
this.signal.on("Signal.received", () => {
vscode.debug.startDebugging(undefined, {
...ATTACH_CONFIGURATION,
Expand Down
Loading

0 comments on commit 49090cf

Please sign in to comment.