diff --git a/.vscode/launch.json b/.vscode/launch.json index 191c0a815e..00f72d4ddf 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -224,8 +224,11 @@ "cwd": "${fileDirname}", "env": { "FORCE_COLOR": "1", + // "BUN_DEBUG_DEBUGGER": "1", + // "BUN_DEBUG_INTERNAL_DEBUGGER": "1", "BUN_DEBUG_QUIET_LOGS": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + // "BUN_INSPECT": "ws+unix:///var/folders/jk/8fzl9l5119598vsqrmphsw7m0000gn/T/tl15npi7qtf.sock?report=1", }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. diff --git a/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts b/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts index 50af2bfa40..87bdedea0c 100644 --- a/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts +++ b/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts @@ -294,7 +294,7 @@ export abstract class BaseDebugAdapter /** * Gets the inspector url. This is deprecated and exists for compat. - * @deprecated You should get the inspector directly, and if it's a WebSocketInspector you can access `.url` direclty. + * @deprecated You should get the inspector directly (with .getInspector()), and if it's a WebSocketInspector you can access `.url` direclty. */ get url(): string { // This code has been migrated from a time when the inspector was always a WebSocketInspector. @@ -305,6 +305,10 @@ export abstract class BaseDebugAdapter throw new Error("Inspector does not offer a URL"); } + public getInspector() { + return this.inspector; + } + abstract start(...args: unknown[]): Promise; /** @@ -2064,7 +2068,7 @@ export class NodeSocketDebugAdapter extends BaseDebugAdapter { +export class WebSocketDebugAdapter extends BaseDebugAdapter { #process?: ChildProcess; public constructor(url?: string | URL, untitledDocPath?: string, bunEvalPath?: string) { @@ -2331,6 +2335,8 @@ export class DebugAdapter extends BaseDebugAdapter { } } +export const DebugAdapter = WebSocketDebugAdapter; + function stoppedReason(reason: JSC.Debugger.PausedEvent["reason"]): DAP.StoppedEvent["reason"] { switch (reason) { case "Breakpoint": diff --git a/packages/bun-inspector-protocol/src/inspector/node-socket.ts b/packages/bun-inspector-protocol/src/inspector/node-socket.ts index 4cd108db82..06bac6ac3c 100644 --- a/packages/bun-inspector-protocol/src/inspector/node-socket.ts +++ b/packages/bun-inspector-protocol/src/inspector/node-socket.ts @@ -35,7 +35,6 @@ export class NodeSocketInspector extends EventEmitter impleme this.#pendingResponses = new Map(); this.#framer = new SocketFramer(socket, message => { - // console.log(message); if (Array.isArray(message)) { for (const m of message) { this.#accept(m); diff --git a/packages/bun-inspector-protocol/src/inspector/websocket.ts b/packages/bun-inspector-protocol/src/inspector/websocket.ts index fbe26418f1..08be605378 100644 --- a/packages/bun-inspector-protocol/src/inspector/websocket.ts +++ b/packages/bun-inspector-protocol/src/inspector/websocket.ts @@ -1,7 +1,7 @@ import { EventEmitter } from "node:events"; import { WebSocket } from "ws"; -import type { Inspector, InspectorEventMap } from "./index"; import type { JSC } from "../protocol"; +import type { Inspector, InspectorEventMap } from "./index"; /** * An inspector that communicates with a debugger over a WebSocket. @@ -170,6 +170,7 @@ export class WebSocketInspector extends EventEmitter implemen #accept(message: string): void { let data: JSC.Event | JSC.Response; + try { data = JSON.parse(message); } catch (cause) { diff --git a/packages/bun-vscode/src/features/debug.ts b/packages/bun-vscode/src/features/debug.ts index 32c54f6a39..ee7c2ca91f 100644 --- a/packages/bun-vscode/src/features/debug.ts +++ b/packages/bun-vscode/src/features/debug.ts @@ -4,11 +4,11 @@ import { join } from "node:path"; import * as vscode from "vscode"; import { type DAP, - DebugAdapter, getAvailablePort, getRandomId, TCPSocketSignal, UnixSignal, + WebSocketDebugAdapter, } from "../../../bun-debug-adapter-protocol"; export const DEBUG_CONFIGURATION: vscode.DebugConfiguration = { @@ -239,7 +239,7 @@ class FileDebugSession extends DebugSession { // If these classes are moved/published, we should make sure // we remove these non-null assertions so consumers of // this lib are not running into these hard - adapter!: DebugAdapter; + adapter!: WebSocketDebugAdapter; sessionId?: string; untitledDocPath?: string; bunEvalPath?: string; @@ -263,7 +263,7 @@ class FileDebugSession extends DebugSession { : `ws+unix://${tmpdir()}/${uniqueId}.sock`; const { untitledDocPath, bunEvalPath } = this; - this.adapter = new DebugAdapter(url, untitledDocPath, bunEvalPath); + this.adapter = new WebSocketDebugAdapter(url, untitledDocPath, bunEvalPath); if (untitledDocPath) { this.adapter.on("Adapter.response", (response: DebugProtocolResponse) => { diff --git a/packages/bun-vscode/src/features/diagnostics/diagnostics.ts b/packages/bun-vscode/src/features/diagnostics/diagnostics.ts index 03cc2b5247..931cf8d72b 100644 --- a/packages/bun-vscode/src/features/diagnostics/diagnostics.ts +++ b/packages/bun-vscode/src/features/diagnostics/diagnostics.ts @@ -1,15 +1,16 @@ import * as fs from "node:fs/promises"; -import { Socket } from "node:net"; import * as os from "node:os"; +import { inspect } from "node:util"; import * as vscode from "vscode"; import { getAvailablePort, - NodeSocketDebugAdapter, + getRandomId, TCPSocketSignal, UnixSignal, + WebSocketDebugAdapter, } from "../../../../bun-debug-adapter-protocol"; import type { JSC } from "../../../../bun-inspector-protocol"; -import { typedGlobalState } from "../../global-state"; +import { createGlobalStateGenerationFn, typedGlobalState } from "../../global-state"; const output = vscode.window.createOutputChannel("Bun - Diagnostics"); @@ -69,8 +70,9 @@ class BunDiagnosticsManager { private readonly editorState: EditorStateManager; private readonly signal: UnixSignal | TCPSocketSignal; private readonly context: vscode.ExtensionContext; + public readonly inspectUrl: string; - public get signalUrl() { + public get notifyUrl() { return this.signal.url; } @@ -122,19 +124,30 @@ class BunDiagnosticsManager { } } + private static getOrCreateOldVersionInspectURL = createGlobalStateGenerationFn( + "DIAGNOSTICS_BUN_INSPECT", + async () => { + const url = + process.platform === "win32" + ? `ws://127.0.0.1:${await getAvailablePort()}/${getRandomId()}` + : `ws+unix://${os.tmpdir()}/${getRandomId()}.sock`; + + return url; + }, + ); + public static async initialize(context: vscode.ExtensionContext) { const signal = await BunDiagnosticsManager.getOrRecreateSignal(context); + const oldVersionInspectURL = await BunDiagnosticsManager.getOrCreateOldVersionInspectURL(context.globalState); - await signal.ready; - - return new BunDiagnosticsManager(context, signal); + return new BunDiagnosticsManager(context, signal, oldVersionInspectURL); } /** * Called when Bun pings BUN_INSPECT_NOTIFY (indicating a program has started). */ - private async handleSocketConnection(socket: Socket) { - const debugAdapter = new NodeSocketDebugAdapter(socket); + private async handleSocketConnection() { + const debugAdapter = new WebSocketDebugAdapter(this.inspectUrl); this.editorState.clearAll("A new socket connected"); @@ -146,6 +159,10 @@ class BunDiagnosticsManager { output.appendLine(`Received inspector event: ${e.method}`); }); + debugAdapter.on("Inspector.error", e => { + output.appendLine(inspect(e, true, null)); + }); + debugAdapter.on("LifecycleReporter.error", event => this.handleLifecycleError(event)); const ok = await debugAdapter.start(); @@ -203,8 +220,6 @@ class BunDiagnosticsManager { const [line = null, col = null] = event.lineColumns.slice(i * 2, i * 2 + 2); - output.appendLine(`Adding related information for ${url} at ${line}:${col}`); - if (line === null || col === null) { return []; } @@ -231,10 +246,15 @@ class BunDiagnosticsManager { }); } - private constructor(context: vscode.ExtensionContext, signal: UnixSignal | TCPSocketSignal) { + private constructor( + context: vscode.ExtensionContext, + signal: UnixSignal | TCPSocketSignal, + oldVersionInspectURL: string, + ) { this.editorState = new EditorStateManager(); this.signal = signal; this.context = context; + this.inspectUrl = oldVersionInspectURL; this.context.subscriptions.push( // on did type @@ -243,7 +263,9 @@ class BunDiagnosticsManager { }), ); - this.signal.on("Signal.Socket.connect", this.handleSocketConnection.bind(this)); + this.signal.on("Signal.received", () => { + this.handleSocketConnection(); + }); } } @@ -255,7 +277,9 @@ export async function registerDiagnosticsSocket(context: vscode.ExtensionContext context.environmentVariableCollection.description = description; const manager = await BunDiagnosticsManager.initialize(context); - context.environmentVariableCollection.replace("BUN_INSPECT_NOTIFY", manager.signalUrl); + + context.environmentVariableCollection.replace("BUN_INSPECT_NOTIFY", manager.notifyUrl); + context.environmentVariableCollection.replace("BUN_INSPECT", `${manager.inspectUrl}?report=1?wait=1`); // Intentionally invalid query params context.subscriptions.push(manager); } diff --git a/packages/bun-vscode/src/global-state.ts b/packages/bun-vscode/src/global-state.ts index f0b7756ba1..87bda9659b 100644 --- a/packages/bun-vscode/src/global-state.ts +++ b/packages/bun-vscode/src/global-state.ts @@ -1,5 +1,7 @@ import { ExtensionContext } from "vscode"; +export const GLOBAL_STATE_VERSION = 1; + export type GlobalStateTypes = { BUN_INSPECT_NOTIFY: | { @@ -10,8 +12,16 @@ export type GlobalStateTypes = { type: "unix"; url: string; }; + + DIAGNOSTICS_BUN_INSPECT: string; }; +export async function clearGlobalState(gs: ExtensionContext["globalState"]) { + const tgs = typedGlobalState(gs); + + await Promise.all(tgs.keys().map(key => tgs.update(key, undefined as never))); +} + export function typedGlobalState(state: ExtensionContext["globalState"]) { return state as { get(key: K): GlobalStateTypes[K] | undefined; @@ -37,4 +47,19 @@ export function typedGlobalState(state: ExtensionContext["globalState"]) { }; } +export function createGlobalStateGenerationFn( + key: T, + resolve: () => Promise, +) { + return async (gs: ExtensionContext["globalState"]) => { + const value = (gs as TypedGlobalState).get(key); + if (value) return value; + + const next = await resolve(); + await (gs as TypedGlobalState).update(key, next); + + return next; + }; +} + export type TypedGlobalState = ReturnType; diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 9b78740a1f..bd2197511c 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -1560,7 +1560,8 @@ pub const VirtualMachine = struct { script_execution_context_id: u32 = 0, next_debugger_id: u64 = 1, poll_ref: Async.KeepAlive = .{}, - wait_for_connection: bool = false, + wait_for_connection: Wait = .off, + // wait_for_connection: bool = false, set_breakpoint_on_first_line: bool = false, mode: enum { /// Bun acts as the server. https://debug.bun.sh/ uses this @@ -1573,6 +1574,8 @@ pub const VirtualMachine = struct { lifecycle_reporter_agent: LifecycleAgent = .{}, must_block_until_connected: bool = false, + pub const Wait = enum { off, shortly, forever }; + pub const log = Output.scoped(.debugger, false); extern "C" fn Bun__createJSDebugger(*JSC.JSGlobalObject) u32; @@ -1597,11 +1600,24 @@ pub const VirtualMachine = struct { .duration_ns = @truncate(@as(u128, @intCast(std.time.nanoTimestamp() - bun.CLI.start_time))), }}); - Bun__ensureDebugger(debugger.script_execution_context_id, debugger.wait_for_connection); - while (debugger.wait_for_connection) { + Bun__ensureDebugger(debugger.script_execution_context_id, debugger.wait_for_connection != .off); + var deadline: bun.timespec = if (debugger.wait_for_connection == .shortly) bun.timespec.now().addMs(30) else undefined; + + while (debugger.wait_for_connection != .off) { this.eventLoop().tick(); - if (debugger.wait_for_connection) - this.eventLoop().autoTickActive(); + switch (debugger.wait_for_connection) { + .forever => { + this.eventLoop().autoTickActive(); + }, + .shortly => { + this.uwsLoop().tickWithTimeout(&deadline); + if (bun.timespec.now().order(&deadline) != .lt) { + log("Timed out waiting for the debugger", .{}); + break; + } + }, + .off => {}, + } } } @@ -1624,7 +1640,7 @@ pub const VirtualMachine = struct { } this.eventLoop().ensureWaker(); - if (debugger.wait_for_connection) { + if (debugger.wait_for_connection != .off) { debugger.poll_ref.ref(this); debugger.must_block_until_connected = true; } @@ -1654,8 +1670,8 @@ pub const VirtualMachine = struct { pub export fn Debugger__didConnect() void { var this = VirtualMachine.get(); - if (this.debugger.?.wait_for_connection) { - this.debugger.?.wait_for_connection = false; + if (this.debugger.?.wait_for_connection != .off) { + this.debugger.?.wait_for_connection = .off; this.debugger.?.poll_ref.unref(this); } } @@ -1999,8 +2015,21 @@ pub const VirtualMachine = struct { } const notify = bun.getenvZ("BUN_INSPECT_NOTIFY") orelse ""; const unix = bun.getenvZ("BUN_INSPECT") orelse ""; - const set_breakpoint_on_first_line = unix.len > 0 and strings.endsWith(unix, "?break=1"); - const wait_for_connection = set_breakpoint_on_first_line or (unix.len > 0 and strings.endsWith(unix, "?wait=1")); + + const set_breakpoint_on_first_line = unix.len > 0 and strings.endsWith(unix, "?break=1"); // If we should set a breakpoint on the first line + const wait_for_debugger = unix.len > 0 and strings.endsWith(unix, "?wait=1"); // If we should wait (either 30ms if report is passed, forever otherwise) for the debugger to connect + const report = unix.len > 0 and strings.includes(unix, "?report=1"); // If either `break=1` or `wait=1` are specified, passing this will make the wait be 30ms and act like it's reporting to clients like the VSCode extension + + // NOTE: + // It's possible (and likely!) that the unix url will end like `?report=1?wait=1`. + // This is done because we needed to support the BUN_INSPECT url in versions of bun before we introduced `report=1` mode. + // Report mode is used for the VSCode extension (and other clients), it just tells bun to timeout connecting quickly rather + // than waiting forever. + + const wait_for_connection: Debugger.Wait = switch (set_breakpoint_on_first_line or wait_for_debugger) { + true => if (report) .shortly else .forever, + false => .off, + }; switch (cli_flag) { .unspecified => { @@ -2015,7 +2044,7 @@ pub const VirtualMachine = struct { this.debugger = Debugger{ .path_or_port = null, .from_environment_variable = notify, - .wait_for_connection = true, + .wait_for_connection = wait_for_connection, .set_breakpoint_on_first_line = set_breakpoint_on_first_line, .mode = .connect, }; @@ -2025,7 +2054,7 @@ pub const VirtualMachine = struct { this.debugger = Debugger{ .path_or_port = cli_flag.enable.path_or_port, .from_environment_variable = unix, - .wait_for_connection = wait_for_connection or cli_flag.enable.wait_for_connection, + .wait_for_connection = if (cli_flag.enable.wait_for_connection) .forever else wait_for_connection, .set_breakpoint_on_first_line = set_breakpoint_on_first_line or cli_flag.enable.set_breakpoint_on_first_line, }; },