diff options
Diffstat (limited to 'packages/bun-inspector-protocol/src')
-rw-r--r-- | packages/bun-inspector-protocol/src/inspector/index.d.ts | 41 | ||||
-rw-r--r-- | packages/bun-inspector-protocol/src/inspector/websocket.ts | 205 |
2 files changed, 97 insertions, 149 deletions
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`; -} |