From 0486cea35a80be97ba43f41a29ce55f0d3a8eb01 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Wed, 16 Aug 2023 19:40:20 -0700 Subject: `bun --inspect` (#4158) * Let the debugger to pause/resume the event loop * Add initial support for Debug Adapter Protocol * Add progress * Update Worker.cpp * Fix require("console") #3820 (#4073) * Fix #3820 * Add Module (#4074) * Set exports to {} in user-constructed CommonJSModuleRecords (#4076) * feat(bun/test): Implement "toSatisfy" & "toIncludeRepeated" (fwup) (#3651) * Fix merge issues * oop * make codegen * Fix build issues --------- Co-authored-by: dave caruso * worker tests (#4058) Co-authored-by: Jarred Sumner * feat(bun:test) add support for test.each() and describe.each() (#4047) * rename callback to func * update testscope to handle function arguments * works * big cleanup * works in debug, not release * fix memory issue & update tests * catch & str test * write types for each() & switch tests to ts * rm & typo * move some code around & support describe * review changes * Fix one of the astro segfaults, also fix bun init version (#4079) * 4->16 * add assertions * fix version stuff * Remove unintentional logs from #4043 * Run prettier * Update main-worker-file.js * Update SIMDUTF (#4078) * Fix constructing buffer from a UTF16 string with the Latin1 encoding. (#4086) Close: #3914 * Add support for `bun --revision` (#4027) Co-authored-by: Yash Sharma * Updates * Update __global.zig * remove non-node node-fallbacks (#4082) * remove non-node node-fallbacks. * organize the imports * Fix test * Sync bun-polyfills branch (#4081) * bun-polyfills: initial impl. & baseline refactor * move @types/ws dep from root to /test/ * bun-types: remove ReadableStream.forEach method (this does not exist, probably added by mistake) * bun-polyfills: remove extraneous stream utils * bun-polyfills: add types syncing file * bun-polyfills: re-arrange global polyfills * bun-polyfills: fix FileBlob streams types again * bun-polyfills: sync all of @types/node * bun-polyfills: typeguard all current polyfills * bun-polyfills: fix import paths * bun-polyfills: switch to wasm impl. of farmhash * bun-polyfills: support default import of bun obj * bun-polyfills: transpiler placeholder file * bun-polyfills: loaderless import.meta polyfill * bun-polyfills: refactor import.meta polyfill * bun-polyfills: repl entrypoint & todo list index * bun-types: Add null to return type of Bun.which * bun-types: match Bun.sha with Bun.hash.SHA512_256 * bun-polyfills: new "repl" package.json script * bun-polyfills: full refactor of toplevel hashes * bun-polyfills: these are fixed * bun-types: NODE_ENV is optional * bun-polyfills: fix Bun.env types * bun-types+polyfills: fix HeapSnapshot.version type * bun-polyfills: fix some web streams type conflicts * bun-polyfills: update internal FileBlob.slice * bun-polyfills: fix subproc stdin conversions * bun-polyfills: better internal fileblob types * bun-polyfills: try to sync global performance type * bun-polyfills: working zig wasm polyfills setup * bun-polyfills: update scripts * bun-polyfills: fix wasm file location resolution * bun-polyfills: goodbye farmhash (replaced by zig) * bun-polyfills: move all Bun.hash polyfills to zig * bun-polyfills: reimpl. seeding of seeded hashes * bun-polyfills: impl. undocumented murmur32v2 * bun-polyfills: switch zighash from jsdoc to .d.ts * bun-types: partial fix of Hash types * bun-polyfills: documented Hash.murmur32v2 * bun-polyfills: misc updates * bun-polyfills: enable sourcemaps * bun-polyfills: handle empty inputs to hash funcs * bun-types: narrow down hash func types * bun-polyfills: remove unnecessary bigint casts * bun-polyfills: impl. Bun.isMainThread * bun-polyfills: impl. Bun.sleep and fix sleepSync * bun-polyfills: impl. indexOfLine * bun-polyfills: impl. Bun.peek.status * bun-types: fix hashing test --------- Co-authored-by: Jarred Sumner * Add remix guide * Fix title * add util.formatWithOptions (#4090) * Add formatWithOptions * tests and tweaks * adjust --------- Co-authored-by: Jarred Sumner * bun test: format description of test.each (#4092) * bun test: format description * add tests for tests * only --------- Co-authored-by: Jarred Sumner * Fixes #4062 (#4106) * Fixes #4062 * Update encoding.zig * Use faster C++ impl * Update wtf-bindings.cpp * undo * Fixup --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> * zig fmt * Update remix guide * fs.zig: create temp files with 0o700, not 0o007 (#4107) * Handle thundering herd of setInterval (#4109) Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> * Fix memory leak in base64url (#4111) Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> * run files without extensions (#4113) * run script without extension * process stdio write fix * don't check for trailing slash, var stream * More lazily initialize these static strings * Remove assertion * wip * Add --inspect flag * Deprecate loading `node_modules.bun` * realpath * regenerate schema * More * more * Update cli.zig * Debugger JS loads * have fun * Most of the code * Its starting to work. * more progress * win some, lose some * Update dap.ts * Async Context Tracking * untested websocket * Emit Console messages * Error handling * errors * Make profiling work better * [clap] CLI arguments with optional values ignore positional params In `bun --inspect foo.js`, `foo.js` should not be the value of `--inspect`. It is a positional parameter. `--inspect=foo` * Support multiple simultaneous clients, automatically unpause on disconnect * regenerate * Update Makefile * Update WebKit * Update cli.zig * Update InternalModuleRegistry+createInternalModuleById.h * BaseURL * Update WebKit * Add web-inspector-bun * Update build.ts * formatting, mostly * Update debugger.ts * Update InternalModuleRegistryConstants.h * wrap * Update test * Update test --------- Co-authored-by: Ashcon Partovi Co-authored-by: Dylan Conway Co-authored-by: dave caruso Co-authored-by: Tiramify (A.K. Daniel) <94789999+TiranexDev@users.noreply.github.com> Co-authored-by: Jacques <25390037+jecquas@users.noreply.github.com> Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> Co-authored-by: Ai Hoshino Co-authored-by: Yash Sharma Co-authored-by: Yash Sharma Co-authored-by: Colin McDonnell Co-authored-by: jhmaster <32803471+jhmaster2000@users.noreply.github.com> Co-authored-by: Adhityaa Chandrasekar Co-authored-by: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> --- src/js/internal/debugger.ts | 280 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 src/js/internal/debugger.ts (limited to 'src/js/internal/debugger.ts') diff --git a/src/js/internal/debugger.ts b/src/js/internal/debugger.ts new file mode 100644 index 000000000..2fd875d10 --- /dev/null +++ b/src/js/internal/debugger.ts @@ -0,0 +1,280 @@ +import type * as BunType from "bun"; + +// We want to avoid dealing with creating a prototype for the inspector class +let sendFn_, disconnectFn_; + +var debuggerCounter = 1; +class DebuggerWithMessageQueue { + debugger?: Debugger = undefined; + messageQueue: string[] = []; + count: number = debuggerCounter++; + + send(msg: string) { + sendFn_.call(this.debugger, msg); + } + + disconnect() { + disconnectFn_.call(this.debugger); + this.messageQueue.length = 0; + } +} + +let defaultPort = 6499; + +let generatedPath: string = ""; +function generatePath() { + if (!generatedPath) { + generatedPath = "/" + Math.random().toString(36).slice(2); + } + + return generatedPath; +} + +function terminalLink(url) { + if (Bun.enableANSIColors) { + // bold + hyperlink + reset + return "\x1b[1m\x1b]8;;" + url + "\x1b\\" + url + "\x1b]8;;\x1b\\" + "\x1b[22m"; + } + + return url; +} + +function dim(text) { + if (Bun.enableANSIColors) { + return "\x1b[2m" + text + "\x1b[22m"; + } + + return text; +} + +class WebSocketListener { + server: BunType.Server; + url: string = ""; + createInspectorConnection; + scriptExecutionContextId: number = 0; + activeConnections: Set> = new Set(); + + constructor(scriptExecutionContextId: number = 0, url: string, createInspectorConnection) { + this.scriptExecutionContextId = scriptExecutionContextId; + this.createInspectorConnection = createInspectorConnection; + this.server = this.start(url); + } + + start(url: string): BunType.Server { + let defaultHostname = "localhost"; + let usingDefaultPort = false; + 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); + } + } + + 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); + } + + const serveOptions: BunType.WebSocketServeOptions = { + hostname, + development: false, + + // ts-ignore + reusePort: false, + + websocket: { + idleTimeout: 0, + open: socket => { + var connection = new DebuggerWithMessageQueue(); + socket.data = connection; + this.activeConnections.add(socket); + connection.debugger = this.createInspectorConnection(this.scriptExecutionContextId, (...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; + } + } + }); + + 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(); + console.log( + "[Inspector]", + "Connection #" + socket.data.count + " closed", + "(" + + new Intl.DateTimeFormat(undefined, { + "timeStyle": "long", + "dateStyle": "short", + }).format(new Date()) + + ")", + ); + this.activeConnections.delete(socket); + }, + }, + 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 (pathname === this.url) { + if (server.upgrade(req)) { + return new Response(); + } + + return new Response("WebSocket expected", { + status: 400, + }); + } + + return new Response("Not found", { + status: 404, + }); + }, + }; + + if (port === "") { + port = defaultPort + ""; + } + + let portNumber = Number(port); + var server, lastError; + + if (usingDefaultPort) { + for (let tries = 0; tries < 10 && !server; tries++) { + try { + lastError = undefined; + server = Bun.serve({ + ...serveOptions, + port: portNumber++, + }); + } catch (e) { + lastError = e; + } + } + } else { + try { + server = Bun.serve({ + ...serveOptions, + port: portNumber, + }); + } catch (e) { + lastError = e; + } + } + + if (!server) { + console.error("[Inspector]", "Failed to start server"); + if (lastError) console.error(lastError); + process.exit(1); + } + + let textToWrite = ""; + function writeToConsole(text) { + textToWrite += text; + } + function flushToConsole() { + console.write(textToWrite); + } + + // yellow foreground + writeToConsole(dim(`------------------ Bun Inspector ------------------` + "\n")); + // reset background + writeToConsole("\x1b[49m"); + + 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; + } +} + +interface Debugger { + send(msg: string): void; + disconnect(): void; +} + +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}`; +} -- cgit v1.2.3