aboutsummaryrefslogtreecommitdiff
path: root/src/js/internal/debugger.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/js/internal/debugger.ts')
-rw-r--r--src/js/internal/debugger.ts580
1 files changed, 297 insertions, 283 deletions
diff --git a/src/js/internal/debugger.ts b/src/js/internal/debugger.ts
index 2e76b2c7c..cd6f8f516 100644
--- a/src/js/internal/debugger.ts
+++ b/src/js/internal/debugger.ts
@@ -1,309 +1,322 @@
-import type * as BunType from "bun";
-
-// We want to avoid dealing with creating a prototype for the inspector class
-let sendFn_, disconnectFn_;
-const colors = Bun.enableANSIColors && process.env.NO_COLOR !== "1";
-
-var debuggerCounter = 1;
-class DebuggerWithMessageQueue {
- debugger?: Debugger = undefined;
- messageQueue: string[] = [];
- count: number = debuggerCounter++;
+import type { Server as WebSocketServer, WebSocketHandler, ServerWebSocket, SocketHandler, Socket } from "bun";
+
+export default function (
+ executionContextId: string,
+ url: string,
+ createBackend: (
+ executionContextId: string,
+ refEventLoop: boolean,
+ receive: (...messages: string[]) => void,
+ ) => unknown,
+ send: (message: string) => void,
+ close: () => void,
+): void {
+ let debug: Debugger | undefined;
+ try {
+ debug = new Debugger(executionContextId, url, createBackend, send, close);
+ } catch (error) {
+ exit("Failed to start inspector:\n", error);
+ }
- send(msg: string) {
- sendFn_.call(this.debugger, msg);
+ const { protocol, href, host, pathname } = debug.url;
+ if (!protocol.includes("unix")) {
+ console.log(dim("--------------------- Bun Inspector ---------------------"), reset());
+ console.log(`Listening:\n ${dim(href)}`);
+ if (protocol.includes("ws")) {
+ console.log(`Inspect in browser:\n ${link(`https://debug.bun.sh/#${host}${pathname}`)}`);
+ }
+ console.log(dim("--------------------- Bun Inspector ---------------------"), reset());
}
- disconnect() {
- disconnectFn_.call(this.debugger);
- this.messageQueue.length = 0;
+ const unix = process.env["BUN_INSPECT_NOTIFY"];
+ if (unix) {
+ const { protocol, pathname } = parseUrl(unix);
+ if (protocol === "unix:") {
+ notify(pathname);
+ }
}
}
-let defaultPort = 6499;
+class Debugger {
+ #url: URL;
+ #createBackend: (refEventLoop: boolean, receive: (...messages: string[]) => void) => Writer;
+
+ constructor(
+ executionContextId: string,
+ url: string,
+ createBackend: (
+ executionContextId: string,
+ refEventLoop: boolean,
+ receive: (...messages: string[]) => void,
+ ) => unknown,
+ send: (message: string) => void,
+ close: () => void,
+ ) {
+ this.#url = parseUrl(url);
+ this.#createBackend = (refEventLoop, receive) => {
+ const backend = createBackend(executionContextId, refEventLoop, receive);
+ return {
+ write: message => {
+ send.call(backend, message);
+ return true;
+ },
+ close: () => close.call(backend),
+ };
+ };
+ this.#listen();
+ }
-let generatedPath: string = "";
-function generatePath() {
- if (!generatedPath) {
- generatedPath = "/" + Math.random().toString(36).slice(2);
+ get url(): URL {
+ return this.#url;
}
- return generatedPath;
-}
+ #listen(): void {
+ const { protocol, hostname, port, pathname } = this.#url;
+
+ if (protocol === "ws:" || protocol === "ws+tcp:") {
+ const server = Bun.serve({
+ hostname,
+ port,
+ fetch: this.#fetch.bind(this),
+ websocket: this.#websocket,
+ });
+ this.#url.hostname = server.hostname;
+ this.#url.port = `${server.port}`;
+ return;
+ }
-function terminalLink(url) {
- if (colors) {
- // bold + hyperlink + reset
- return "\x1b[1m\x1b]8;;" + url + "\x1b\\" + url + "\x1b]8;;\x1b\\" + "\x1b[22m";
- }
+ if (protocol === "ws+unix:") {
+ Bun.serve({
+ unix: pathname,
+ fetch: this.#fetch.bind(this),
+ websocket: this.#websocket,
+ });
+ return;
+ }
- return url;
-}
+ throw new TypeError(`Unsupported protocol: '${protocol}' (expected 'ws:', 'ws+unix:', or 'unix:')`);
+ }
-function dim(text) {
- if (colors) {
- return "\x1b[2m" + text + "\x1b[22m";
+ get #websocket(): WebSocketHandler<Connection> {
+ return {
+ idleTimeout: 0,
+ closeOnBackpressureLimit: false,
+ open: ws => this.#open(ws, webSocketWriter(ws)),
+ message: (ws, message) => {
+ if (typeof message === "string") {
+ this.#message(ws, message);
+ } else {
+ this.#error(ws, new Error(`Unexpected binary message: ${message.toString()}`));
+ }
+ },
+ drain: ws => this.#drain(ws),
+ close: ws => this.#close(ws),
+ };
}
- return text;
-}
+ #fetch(request: Request, server: WebSocketServer): Response | undefined {
+ const { method, url, headers } = request;
+ const { pathname } = new URL(url);
-class WebSocketListener {
- server: BunType.Server;
- url: string = "";
- createInspectorConnection;
- scriptExecutionContextId: number = 0;
- activeConnections: Set<BunType.ServerWebSocket<DebuggerWithMessageQueue>> = new Set();
-
- constructor(scriptExecutionContextId: number = 0, url: string, createInspectorConnection) {
- this.scriptExecutionContextId = scriptExecutionContextId;
- this.createInspectorConnection = createInspectorConnection;
- this.server = this.start(url);
- }
+ if (method !== "GET") {
+ return new Response(null, {
+ status: 405, // Method Not Allowed
+ });
+ }
- start(url: string): BunType.Server {
- let defaultHostname = "localhost";
- let usingDefaultPort = false;
- let isUnix = false;
-
- if (url.startsWith("ws+unix://")) {
- isUnix = true;
- url = url.slice(10);
- } else if (/^[0-9]*$/.test(url)) {
- url = "ws://" + defaultHostname + ":" + url + generatePath();
- } else if (!url || url.startsWith("/")) {
- url = "ws://" + defaultHostname + ":" + defaultPort + generatePath();
- usingDefaultPort = true;
- } else if (url.includes(":") && !url.includes("://")) {
- try {
- const insertSlash = !url.includes("/");
- url = new URL("ws://" + url).href;
- if (insertSlash) {
- url += generatePath().slice(1);
- }
- } catch (e) {
- console.error("[Inspector]", "Failed to parse url", '"' + url + '"');
- process.exit(1);
- }
+ switch (pathname) {
+ case "/json/version":
+ return Response.json(versionInfo());
+ case "/json":
+ case "/json/list":
+ // TODO?
}
- if (!isUnix) {
- try {
- var { hostname, port, pathname } = new URL(url);
- this.url = pathname.toLowerCase();
- } catch (e) {
- console.error("[Inspector]", "Failed to parse url", '"' + url + '"');
- process.exit(1);
- }
+ if (!this.#url.protocol.includes("unix") && this.#url.pathname !== pathname) {
+ return new Response(null, {
+ status: 404, // Not Found
+ });
}
- const serveOptions: BunType.WebSocketServeOptions<DebuggerWithMessageQueue> = {
- ...(isUnix ? { unix: url } : { hostname }),
- development: false,
-
- // @ts-ignore
- reusePort: false,
-
- websocket: {
- idleTimeout: 0,
- open: socket => {
- var connection = new DebuggerWithMessageQueue();
- // @ts-expect-error
- const shouldRefEventLoop = !!socket.data?.shouldRefEventLoop;
-
- socket.data = connection;
- this.activeConnections.add(socket);
- connection.debugger = this.createInspectorConnection(
- this.scriptExecutionContextId,
- shouldRefEventLoop,
- (...msgs: string[]) => {
- if (socket.readyState > 1) {
- connection.disconnect();
- return;
- }
-
- if (connection.messageQueue.length > 0) {
- connection.messageQueue.push(...msgs);
- return;
- }
-
- for (let i = 0; i < msgs.length; i++) {
- if (!socket.sendText(msgs[i])) {
- if (socket.readyState < 2) {
- connection.messageQueue.push(...msgs.slice(i));
- }
- return;
- }
- }
- },
- );
-
- if (!isUnix) {
- console.log(
- "[Inspector]",
- "Connection #" + connection.count + " opened",
- "(" +
- new Intl.DateTimeFormat(undefined, {
- "timeStyle": "long",
- "dateStyle": "short",
- }).format(new Date()) +
- ")",
- );
- }
- },
- drain: socket => {
- const queue = socket.data.messageQueue;
- for (let i = 0; i < queue.length; i++) {
- if (!socket.sendText(queue[i])) {
- socket.data.messageQueue = queue.slice(i);
- return;
- }
- }
- queue.length = 0;
- },
- message: (socket, message) => {
- if (typeof message !== "string") {
- console.warn("[Inspector]", "Received non-string message");
- return;
- }
- socket.data.send(message as string);
- },
- close: socket => {
- socket.data.disconnect();
- if (!isUnix) {
- console.log(
- "[Inspector]",
- "Connection #" + socket.data.count + " closed",
- "(" +
- new Intl.DateTimeFormat(undefined, {
- "timeStyle": "long",
- "dateStyle": "short",
- }).format(new Date()) +
- ")",
- );
- }
- this.activeConnections.delete(socket);
+ const data: Connection = {
+ refEventLoop: headers.get("Ref-Event-Loop") === "0",
+ };
+
+ if (!server.upgrade(request, { data })) {
+ return new Response(null, {
+ status: 426, // Upgrade Required
+ headers: {
+ "Upgrade": "websocket",
},
- },
- fetch: (req, server) => {
- let { pathname } = new URL(req.url);
- pathname = pathname.toLowerCase();
-
- if (pathname === "/json/version") {
- return Response.json({
- "Browser": navigator.userAgent,
- "WebKit-Version": process.versions.webkit,
- "Bun-Version": Bun.version,
- "Bun-Revision": Bun.revision,
- });
- }
+ });
+ }
+ }
- if (!this.url || pathname === this.url) {
- const refHeader = req.headers.get("Ref-Event-Loop");
- if (
- server.upgrade(req, {
- data: {
- shouldRefEventLoop: !!refHeader && refHeader !== "0",
- },
- })
- ) {
- return new Response();
- }
+ get #socket(): SocketHandler<Connection> {
+ return {
+ open: socket => this.#open(socket, socketWriter(socket)),
+ data: (socket, message) => this.#message(socket, message.toString()),
+ drain: socket => this.#drain(socket),
+ close: socket => this.#close(socket),
+ error: (socket, error) => this.#error(socket, error),
+ connectError: (_, error) => exit("Failed to start inspector:\n", error),
+ };
+ }
- return new Response("WebSocket expected", {
- status: 400,
- });
- }
+ #open(connection: ConnectionOwner, writer: Writer): void {
+ const { data } = connection;
+ const { refEventLoop } = data;
- return new Response("Not found", {
- status: 404,
- });
- },
- };
+ const client = bufferedWriter(writer);
+ const backend = this.#createBackend(refEventLoop, (...messages: string[]) => {
+ for (const message of messages) {
+ client.write(message);
+ }
+ });
- if (port === "") {
- port = defaultPort + "";
- }
+ data.client = client;
+ data.backend = backend;
+ }
- let portNumber = Number(port);
- var server, lastError;
-
- if (usingDefaultPort) {
- for (let tries = 0; tries < 10 && !server; tries++) {
- try {
- lastError = undefined;
- server = Bun.serve<DebuggerWithMessageQueue>({
- ...serveOptions,
- port: portNumber++,
- });
- if (isUnix) {
- notify();
- }
- } catch (e) {
- lastError = e;
- }
+ #message(connection: ConnectionOwner, message: string): void {
+ const { data } = connection;
+ const { backend } = data;
+ backend?.write(message);
+ }
+
+ #drain(connection: ConnectionOwner): void {
+ const { data } = connection;
+ const { client } = data;
+ client?.drain?.();
+ }
+
+ #close(connection: ConnectionOwner): void {
+ const { data } = connection;
+ const { backend } = data;
+ backend?.close();
+ }
+
+ #error(connection: ConnectionOwner, error: Error): void {
+ const { data } = connection;
+ const { backend } = data;
+ console.error(error);
+ backend?.close();
+ }
+}
+
+function versionInfo(): unknown {
+ return {
+ "Protocol-Version": "1.3",
+ "Browser": "Bun",
+ // @ts-ignore: Missing types for `navigator`
+ "User-Agent": navigator.userAgent,
+ "WebKit-Version": process.versions.webkit,
+ "Bun-Version": Bun.version,
+ "Bun-Revision": Bun.revision,
+ };
+}
+
+function webSocketWriter(ws: ServerWebSocket<unknown>): Writer {
+ return {
+ write: message => !!ws.sendText(message),
+ close: () => ws.close(),
+ };
+}
+
+function socketWriter(socket: Socket<unknown>): Writer {
+ return {
+ write: message => !!socket.write(message),
+ close: () => socket.end(),
+ };
+}
+
+function bufferedWriter(writer: Writer): Writer {
+ let draining = false;
+ let pendingMessages: string[] = [];
+
+ return {
+ write: message => {
+ if (draining || !writer.write(message)) {
+ pendingMessages.push(message);
}
- } else {
+ return true;
+ },
+ drain: () => {
+ draining = true;
try {
- server = Bun.serve<DebuggerWithMessageQueue>({
- ...serveOptions,
- port: portNumber,
- });
- if (isUnix) {
- notify();
+ for (let i = 0; i < pendingMessages.length; i++) {
+ if (!writer.write(pendingMessages[i])) {
+ pendingMessages = pendingMessages.slice(i);
+ return;
+ }
}
- } catch (e) {
- lastError = e;
+ } finally {
+ draining = false;
}
- }
+ },
+ close: () => {
+ writer.close();
+ pendingMessages.length = 0;
+ },
+ };
+}
- if (!server) {
- console.error("[Inspector]", "Failed to start server");
- if (lastError) console.error(lastError);
- process.exit(1);
- }
+const defaultHostname = "localhost";
+const defaultPort = 6499;
- let textToWrite = "";
- function writeToConsole(text) {
- textToWrite += text;
- }
- function flushToConsole() {
- console.write(textToWrite);
+function parseUrl(url: string): URL {
+ try {
+ if (!url) {
+ return new URL(randomId(), `ws://${defaultHostname}:${defaultPort}/`);
+ } else if (url.startsWith("/")) {
+ return new URL(url, `ws://${defaultHostname}:${defaultPort}/`);
+ } else if (/^[a-z+]+:\/\//i.test(url)) {
+ return new URL(url);
+ } else if (/^\d+$/.test(url)) {
+ return new URL(randomId(), `ws://${defaultHostname}:${url}/`);
+ } else if (!url.includes("/") && url.includes(":")) {
+ return new URL(randomId(), `ws://${url}/`);
+ } else if (!url.includes(":")) {
+ const [hostname, pathname] = url.split("/", 2);
+ return new URL(`ws://${hostname}:${defaultPort}/${pathname}`);
+ } else {
+ return new URL(randomId(), `ws://${url}`);
}
+ } catch {
+ throw new TypeError(`Invalid hostname or URL: '${url}'`);
+ }
+}
- if (!this.url) {
- return server;
- }
+function randomId() {
+ return Math.random().toString(36).slice(2);
+}
- // yellow foreground
- writeToConsole(dim(`------------------ Bun Inspector ------------------` + "\n"));
- if (colors) {
- // reset background
- writeToConsole("\x1b[49m");
- }
+const { enableANSIColors } = Bun;
- writeToConsole(
- "Listening at:\n " +
- `ws://${hostname}:${server.port}${this.url}` +
- "\n\n" +
- "Inspect in browser:\n " +
- terminalLink(new URL(`https://debug.bun.sh#${server.hostname}:${server.port}${this.url}`).href) +
- "\n",
- );
- writeToConsole(dim(`------------------ Bun Inspector ------------------` + "\n"));
- flushToConsole();
-
- return server;
+function dim(string: string): string {
+ if (enableANSIColors) {
+ return `\x1b[2m${string}\x1b[22m`;
}
+ return string;
}
-function notify(): void {
- const unix = process.env["BUN_INSPECT_NOTIFY"];
- if (!unix || !unix.startsWith("unix://")) {
- return;
+function link(url: string): string {
+ if (enableANSIColors) {
+ return `\x1b[1m\x1b]8;;${url}\x1b\\${url}\x1b]8;;\x1b\\\x1b[22m`;
+ }
+ return url;
+}
+
+function reset(): string {
+ if (enableANSIColors) {
+ return "\x1b[49m";
}
+ return "";
+}
+
+function notify(unix: string): void {
Bun.connect({
- unix: unix.slice(7),
+ unix,
socket: {
open: socket => {
socket.end("1");
@@ -311,26 +324,27 @@ function notify(): void {
data: () => {}, // required or it errors
},
}).finally(() => {
- // Do nothing
+ // Best-effort
});
}
-interface Debugger {
- send(msg: string): void;
- disconnect(): void;
+function exit(...args: unknown[]): never {
+ console.error(...args);
+ process.exit(1);
}
-var listener: WebSocketListener;
-
-export default function start(debuggerId, hostOrPort, createInspectorConnection, sendFn, disconnectFn) {
- try {
- sendFn_ = sendFn;
- disconnectFn_ = disconnectFn;
- globalThis.listener = listener ||= new WebSocketListener(debuggerId, hostOrPort, createInspectorConnection);
- } catch (e) {
- console.error("Bun Inspector threw an exception\n", e);
- process.exit(1);
- }
-
- return `http://${listener.server.hostname}:${listener.server.port}${listener.url}`;
-}
+type ConnectionOwner = {
+ data: Connection;
+};
+
+type Connection = {
+ refEventLoop: boolean;
+ client?: Writer;
+ backend?: Writer;
+};
+
+type Writer = {
+ write: (message: string) => boolean;
+ drain?: () => void;
+ close: () => void;
+};