diff --git a/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts b/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts index fca2d9677fdf0f..93817257b53e06 100644 --- a/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts +++ b/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts @@ -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, @@ -215,6 +222,7 @@ export class DebugAdapter extends EventEmitter implements #variables: Map; #initialized?: InitializeRequest; #options?: DebuggerOptions; + #signal?: WebSocketSignal; constructor(url?: string | URL) { super(); @@ -466,10 +474,6 @@ export class DebugAdapter extends EventEmitter 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")) { @@ -480,108 +484,59 @@ export class DebugAdapter extends EventEmitter 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 = 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; - isDebugee?: boolean; - }): Promise { - 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((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", }); } @@ -2107,7 +2062,7 @@ export class DebugAdapter extends EventEmitter implements close(): void { this.#process?.kill(); - // this.#signal?.close(); + this.#signal?.close(); this.#inspector.close(); this.#reset(); } diff --git a/packages/bun-debug-adapter-protocol/src/debugger/signal.ts b/packages/bun-debug-adapter-protocol/src/debugger/signal.ts index 6cd6e5ca3bf64c..e378c7db8ab65e 100644 --- a/packages/bun-debug-adapter-protocol/src/debugger/signal.ts +++ b/packages/bun-debug-adapter-protocol/src/debugger/signal.ts @@ -1,12 +1,9 @@ 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]; @@ -14,22 +11,24 @@ export type UnixSignalEventMap = { }; /** - * 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 { - #path: string; - #server: Server; +export class WebSocketSignal extends EventEmitter { + #url: string; + #server: WebSocketServer; #ready: Promise; - 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()); }); }); @@ -37,22 +36,20 @@ export class UnixSignal extends EventEmitter { this.#server.on("listening", resolve); this.#server.on("error", reject); }); - this.#server.listen(this.#path); } - emit(event: E, ...args: UnixSignalEventMap[E]): boolean { + emit(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; } /** @@ -70,18 +67,11 @@ export class UnixSignal extends EventEmitter { } } -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}`); } } diff --git a/packages/bun-vscode/scripts/build.mjs b/packages/bun-vscode/scripts/build.mjs index c2281c467a0486..19aaa6b7092608 100644 --- a/packages/bun-vscode/scripts/build.mjs +++ b/packages/bun-vscode/scripts/build.mjs @@ -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 diff --git a/packages/bun-vscode/src/features/debug.ts b/packages/bun-vscode/src/features/debug.ts index 88434622a1f8be..dd2fba4fb9ae1f 100644 --- a/packages/bun-vscode/src/features/debug.ts +++ b/packages/bun-vscode/src/features/debug.ts @@ -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", @@ -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)); @@ -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, diff --git a/src/js/internal/debugger.ts b/src/js/internal/debugger.ts index 6c4b8376dcb191..4f37be6fbd1d6c 100644 --- a/src/js/internal/debugger.ts +++ b/src/js/internal/debugger.ts @@ -28,11 +28,11 @@ export default function ( Bun.write(Bun.stderr, dim("--------------------- Bun Inspector ---------------------") + reset() + "\n"); } - const unix = process.env["BUN_INSPECT_NOTIFY"]; - if (unix) { - const { protocol, pathname } = parseUrl(unix); - if (protocol === "unix:") { - notify(pathname); + const notifyUrl = process.env["BUN_INSPECT_NOTIFY"]; + if (notifyUrl) { + const { protocol } = new URL(notifyUrl); + if (protocol === "ws:" || protocol === "wss:") { + notifyWebSocket(notifyUrl); } } } @@ -73,7 +73,7 @@ class Debugger { #listen(): void { const { protocol, hostname, port, pathname } = this.#url; - if (protocol === "ws:" || protocol === "ws+tcp:") { + if (protocol === "ws:" || protocol === "wss:") { const server = Bun.serve({ hostname, port, @@ -85,16 +85,7 @@ class Debugger { return; } - if (protocol === "ws+unix:") { - Bun.serve({ - unix: pathname, - fetch: this.#fetch.bind(this), - websocket: this.#websocket, - }); - return; - } - - throw new TypeError(`Unsupported protocol: '${protocol}' (expected 'ws:', 'ws+unix:', or 'unix:')`); + throw new TypeError(`Unsupported protocol: '${protocol}' (expected 'ws:' or 'wss:')`); } get #websocket(): WebSocketHandler { @@ -264,34 +255,11 @@ const defaultHostname = "localhost"; const defaultPort = 6499; function parseUrl(input: string): URL { - if (input.startsWith("ws://") || input.startsWith("ws+unix://") || input.startsWith("unix://")) { + try { return new URL(input); + } catch { + throw new Error(`Invalid URL: ${input}`); } - const url = new URL(`ws://${defaultHostname}:${defaultPort}/${randomId()}`); - for (const part of input.split(/(\[[a-z0-9:]+\])|:/).filter(Boolean)) { - if (/^\d+$/.test(part)) { - url.port = part; - continue; - } - if (part.startsWith("[")) { - url.hostname = part; - continue; - } - if (part.startsWith("/")) { - url.pathname = part; - continue; - } - const [hostname, ...pathnames] = part.split("/"); - if (/^\d+$/.test(hostname)) { - url.port = hostname; - } else { - url.hostname = hostname; - } - if (pathnames.length) { - url.pathname = `/${pathnames.join("/")}`; - } - } - return url; } function randomId() { @@ -321,18 +289,16 @@ function reset(): string { return ""; } -function notify(unix: string): void { - Bun.connect({ - unix, - socket: { - open: socket => { - socket.end("1"); - }, - data: () => {}, // required or it errors - }, - }).finally(() => { - // Best-effort - }); +function notifyWebSocket(url: string): void { + const ws = new WebSocket(url); + ws.onopen = () => { + ws.send("1"); + ws.close(); + }; + ws.onerror = error => { + // Handle error if needed + console.error("WebSocket error:", error); + }; } function exit(...args: unknown[]): never {