diff options
author | 2023-08-26 18:26:50 -0700 | |
---|---|---|
committer | 2023-08-26 18:26:50 -0700 | |
commit | eeeef5aaf05c832ccc4e6de6781f1924a5893808 (patch) | |
tree | 8b1283bd79d0e17a850717c9a88d668fb0c2c21d | |
parent | f9b966c13f5ed3d870c54b69cb59a5adc12060b7 (diff) | |
download | bun-eeeef5aaf05c832ccc4e6de6781f1924a5893808.tar.gz bun-eeeef5aaf05c832ccc4e6de6781f1924a5893808.tar.zst bun-eeeef5aaf05c832ccc4e6de6781f1924a5893808.zip |
Terminal works, launch is being reworkeddap3
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(" ")); + }); } } |