mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
More support for DAP (#4380)
* Fix reconnect with --watch * Support setVariable * Support setExpression * Support watch variables * Conditional and hit breakpoints * Support exceptionInfo * Support goto and gotoTargets * Support completions * Support both a URL and UNIX inspector at the same time * Fix url * WIP, add timeouts to figure out issue * Fix messages being dropped from debugger.ts * Progress * Fix breakpoints and ref-event-loop * More fixes * Fix exit * Make hovers better * Fix --hot
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,271 +0,0 @@
|
||||
import type { DAP } from "../protocol";
|
||||
|
||||
const capabilities: DAP.Capabilities = {
|
||||
/**
|
||||
* The debug adapter supports the `configurationDone` request.
|
||||
* @see configurationDone
|
||||
*/
|
||||
supportsConfigurationDoneRequest: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports function breakpoints using the `setFunctionBreakpoints` request.
|
||||
* @see setFunctionBreakpoints
|
||||
*/
|
||||
supportsFunctionBreakpoints: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports conditional breakpoints.
|
||||
* @see setBreakpoints
|
||||
* @see setInstructionBreakpoints
|
||||
* @see setFunctionBreakpoints
|
||||
* @see setExceptionBreakpoints
|
||||
* @see setDataBreakpoints
|
||||
*/
|
||||
supportsConditionalBreakpoints: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports breakpoints that break execution after a specified number of hits.
|
||||
* @see setBreakpoints
|
||||
* @see setInstructionBreakpoints
|
||||
* @see setFunctionBreakpoints
|
||||
* @see setExceptionBreakpoints
|
||||
* @see setDataBreakpoints
|
||||
*/
|
||||
supportsHitConditionalBreakpoints: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports a (side effect free) `evaluate` request for data hovers.
|
||||
* @see evaluate
|
||||
*/
|
||||
supportsEvaluateForHovers: true,
|
||||
|
||||
/**
|
||||
* Available exception filter options for the `setExceptionBreakpoints` request.
|
||||
* @see setExceptionBreakpoints
|
||||
*/
|
||||
exceptionBreakpointFilters: [
|
||||
{
|
||||
filter: "all",
|
||||
label: "Caught Exceptions",
|
||||
default: false,
|
||||
supportsCondition: true,
|
||||
description: "Breaks on all throw errors, even if they're caught later.",
|
||||
conditionDescription: `error.name == "CustomError"`,
|
||||
},
|
||||
{
|
||||
filter: "uncaught",
|
||||
label: "Uncaught Exceptions",
|
||||
default: false,
|
||||
supportsCondition: true,
|
||||
description: "Breaks only on errors or promise rejections that are not handled.",
|
||||
conditionDescription: `error.name == "CustomError"`,
|
||||
},
|
||||
],
|
||||
|
||||
/**
|
||||
* The debug adapter supports stepping back via the `stepBack` and `reverseContinue` requests.
|
||||
* @see stepBack
|
||||
* @see reverseContinue
|
||||
*/
|
||||
supportsStepBack: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports setting a variable to a value.
|
||||
* @see setVariable
|
||||
*/
|
||||
supportsSetVariable: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports restarting a frame.
|
||||
* @see restartFrame
|
||||
*/
|
||||
supportsRestartFrame: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `gotoTargets` request.
|
||||
* @see gotoTargets
|
||||
*/
|
||||
supportsGotoTargetsRequest: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `stepInTargets` request.
|
||||
* @see stepInTargets
|
||||
*/
|
||||
supportsStepInTargetsRequest: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `completions` request.
|
||||
* @see completions
|
||||
*/
|
||||
supportsCompletionsRequest: false,
|
||||
|
||||
/**
|
||||
* The set of characters that should trigger completion in a REPL.
|
||||
* If not specified, the UI should assume the `.` character.
|
||||
* @see completions
|
||||
*/
|
||||
completionTriggerCharacters: [".", "[", '"', "'"],
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `modules` request.
|
||||
* @see modules
|
||||
*/
|
||||
supportsModulesRequest: false,
|
||||
|
||||
/**
|
||||
* The set of additional module information exposed by the debug adapter.
|
||||
* @see modules
|
||||
*/
|
||||
additionalModuleColumns: [],
|
||||
|
||||
/**
|
||||
* Checksum algorithms supported by the debug adapter.
|
||||
*/
|
||||
supportedChecksumAlgorithms: [],
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `restart` request.
|
||||
* In this case a client should not implement `restart` by terminating
|
||||
* and relaunching the adapter but by calling the `restart` request.
|
||||
* @see restart
|
||||
*/
|
||||
supportsRestartRequest: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports `exceptionOptions` on the `setExceptionBreakpoints` request.
|
||||
* @see setExceptionBreakpoints
|
||||
*/
|
||||
supportsExceptionOptions: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports a `format` attribute on the `stackTrace`, `variables`, and `evaluate` requests.
|
||||
* @see stackTrace
|
||||
* @see variables
|
||||
* @see evaluate
|
||||
*/
|
||||
supportsValueFormattingOptions: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `exceptionInfo` request.
|
||||
* @see exceptionInfo
|
||||
*/
|
||||
supportsExceptionInfoRequest: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `terminateDebuggee` attribute on the `disconnect` request.
|
||||
* @see disconnect
|
||||
*/
|
||||
supportTerminateDebuggee: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `suspendDebuggee` attribute on the `disconnect` request.
|
||||
* @see disconnect
|
||||
*/
|
||||
supportSuspendDebuggee: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the delayed loading of parts of the stack,
|
||||
* which requires that both the `startFrame` and `levels` arguments and
|
||||
* the `totalFrames` result of the `stackTrace` request are supported.
|
||||
* @see stackTrace
|
||||
*/
|
||||
supportsDelayedStackTraceLoading: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `loadedSources` request.
|
||||
* @see loadedSources
|
||||
*/
|
||||
supportsLoadedSourcesRequest: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports log points by interpreting the `logMessage` attribute of the `SourceBreakpoint`.
|
||||
* @see setBreakpoints
|
||||
*/
|
||||
supportsLogPoints: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `terminateThreads` request.
|
||||
* @see terminateThreads
|
||||
*/
|
||||
supportsTerminateThreadsRequest: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `setExpression` request.
|
||||
* @see setExpression
|
||||
*/
|
||||
supportsSetExpression: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `terminate` request.
|
||||
* @see terminate
|
||||
*/
|
||||
supportsTerminateRequest: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports data breakpoints.
|
||||
* @see setDataBreakpoints
|
||||
*/
|
||||
supportsDataBreakpoints: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `readMemory` request.
|
||||
* @see readMemory
|
||||
*/
|
||||
supportsReadMemoryRequest: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `writeMemory` request.
|
||||
* @see writeMemory
|
||||
*/
|
||||
supportsWriteMemoryRequest: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `disassemble` request.
|
||||
* @see disassemble
|
||||
*/
|
||||
supportsDisassembleRequest: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `cancel` request.
|
||||
* @see cancel
|
||||
*/
|
||||
supportsCancelRequest: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `breakpointLocations` request.
|
||||
* @see breakpointLocations
|
||||
*/
|
||||
supportsBreakpointLocationsRequest: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `clipboard` context value in the `evaluate` request.
|
||||
* @see evaluate
|
||||
*/
|
||||
supportsClipboardContext: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports stepping granularities (argument `granularity`) for the stepping requests.
|
||||
* @see stepIn
|
||||
*/
|
||||
supportsSteppingGranularity: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports adding breakpoints based on instruction references.
|
||||
* @see setInstructionBreakpoints
|
||||
*/
|
||||
supportsInstructionBreakpoints: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports `filterOptions` as an argument on the `setExceptionBreakpoints` request.
|
||||
* @see setExceptionBreakpoints
|
||||
*/
|
||||
supportsExceptionFilterOptions: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `singleThread` property on the execution requests
|
||||
* (`continue`, `next`, `stepIn`, `stepOut`, `reverseContinue`, `stepBack`).
|
||||
*/
|
||||
supportsSingleThreadExecutionRequests: false,
|
||||
};
|
||||
|
||||
export default capabilities;
|
||||
@@ -21,7 +21,7 @@ export class UnixSignal extends EventEmitter<UnixSignalEventMap> {
|
||||
#server: Server;
|
||||
#ready: Promise<void>;
|
||||
|
||||
constructor(path?: string) {
|
||||
constructor(path?: string | URL) {
|
||||
super();
|
||||
this.#path = path ? parseUnixPath(path) : randomUnixPath();
|
||||
this.#server = createServer();
|
||||
@@ -74,8 +74,8 @@ export function randomUnixPath(): string {
|
||||
return join(tmpdir(), `${Math.random().toString(36).slice(2)}.sock`);
|
||||
}
|
||||
|
||||
function parseUnixPath(path: string): string {
|
||||
if (path.startsWith("/")) {
|
||||
function parseUnixPath(path: string | URL): string {
|
||||
if (typeof path === "string" && path.startsWith("/")) {
|
||||
return path;
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -80,7 +80,7 @@ class ActualSourceMap implements SourceMap {
|
||||
|
||||
const { line: gline, column: gcolumn } = lineRange;
|
||||
return {
|
||||
line: lineToLine(gline),
|
||||
line: lineTo0BasedLine(gline),
|
||||
column: columnToColumn(gcolumn),
|
||||
verified: true,
|
||||
};
|
||||
@@ -144,9 +144,17 @@ class NoopSourceMap implements SourceMap {
|
||||
const defaultSourceMap = new NoopSourceMap();
|
||||
|
||||
export function SourceMap(url?: string): SourceMap {
|
||||
if (!url || !url.startsWith("data:")) {
|
||||
if (!url) {
|
||||
return defaultSourceMap;
|
||||
}
|
||||
if (!url.startsWith("data:")) {
|
||||
const match = url.match(/\/\/[#@]\s*sourceMappingURL=(.*)$/m);
|
||||
if (!match) {
|
||||
return defaultSourceMap;
|
||||
}
|
||||
const [_, sourceMapUrl] = match;
|
||||
url = sourceMapUrl;
|
||||
}
|
||||
try {
|
||||
const [_, base64] = url.split(",", 2);
|
||||
const decoded = Buffer.from(base64, "base64url").toString("utf8");
|
||||
|
||||
@@ -52,10 +52,10 @@ export class WebSocketInspector extends EventEmitter<InspectorEventMap> implemen
|
||||
// @ts-expect-error: Support both Bun and Node.js version of `headers`.
|
||||
webSocket = new WebSocket(url, {
|
||||
headers: {
|
||||
"Ref-Event-Loop": "1",
|
||||
"Ref-Event-Loop": "0",
|
||||
},
|
||||
finishRequest: (request: import("http").ClientRequest) => {
|
||||
request.setHeader("Ref-Event-Loop", "1");
|
||||
request.setHeader("Ref-Event-Loop", "0");
|
||||
request.end();
|
||||
},
|
||||
});
|
||||
@@ -67,18 +67,23 @@ export class WebSocketInspector extends EventEmitter<InspectorEventMap> implemen
|
||||
webSocket.addEventListener("open", () => {
|
||||
this.emit("Inspector.connected");
|
||||
|
||||
for (const request of this.#pendingRequests) {
|
||||
for (let i = 0; i < this.#pendingRequests.length; i++) {
|
||||
const request = this.#pendingRequests[i];
|
||||
|
||||
if (this.#send(request)) {
|
||||
this.emit("Inspector.request", request);
|
||||
} else {
|
||||
this.#pendingRequests = this.#pendingRequests.slice(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.#pendingRequests.length = 0;
|
||||
});
|
||||
|
||||
webSocket.addEventListener("message", ({ data }) => {
|
||||
if (typeof data === "string") {
|
||||
this.#accept(data);
|
||||
} else {
|
||||
this.emit("Inspector.error", new Error(`WebSocket received unexpected binary message: ${data.toString()}`));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -125,8 +130,12 @@ export class WebSocketInspector extends EventEmitter<InspectorEventMap> implemen
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let timerId: number | undefined;
|
||||
const done = (result: any) => {
|
||||
this.#pendingResponses.delete(id);
|
||||
if (timerId) {
|
||||
clearTimeout(timerId);
|
||||
}
|
||||
if (result instanceof Error) {
|
||||
reject(result);
|
||||
} else {
|
||||
@@ -136,6 +145,7 @@ export class WebSocketInspector extends EventEmitter<InspectorEventMap> implemen
|
||||
|
||||
this.#pendingResponses.set(id, done);
|
||||
if (this.#send(request)) {
|
||||
timerId = +setTimeout(() => done(new Error(`Timed out: ${method}`)), 10_000);
|
||||
this.emit("Inspector.request", request);
|
||||
} else {
|
||||
this.emit("Inspector.pendingRequest", request);
|
||||
@@ -183,7 +193,6 @@ export class WebSocketInspector extends EventEmitter<InspectorEventMap> implemen
|
||||
return;
|
||||
}
|
||||
|
||||
this.#pendingResponses.delete(id);
|
||||
if ("error" in data) {
|
||||
const { error } = data;
|
||||
const { message } = error;
|
||||
@@ -218,6 +227,7 @@ export class WebSocketInspector extends EventEmitter<InspectorEventMap> implemen
|
||||
resolve(error ?? new Error("WebSocket closed"));
|
||||
}
|
||||
this.#pendingResponses.clear();
|
||||
|
||||
if (error) {
|
||||
this.emit("Inspector.error", error);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,24 @@ describe("example", () => {
|
||||
expect(1).toBe(1);
|
||||
expect(1).not.toBe(2);
|
||||
expect(() => {
|
||||
throw new Error("error");
|
||||
throw new TypeError("Oops! I did it again.");
|
||||
}).toThrow();
|
||||
expect(() => {
|
||||
throw new Error("Parent error.", {
|
||||
cause: new TypeError("Child error."),
|
||||
});
|
||||
}).toThrow();
|
||||
expect(() => {
|
||||
throw new AggregateError([new TypeError("Child error 1."), new TypeError("Child error 2.")], "Parent error.");
|
||||
}).toThrow();
|
||||
expect(() => {
|
||||
throw "This is a string error";
|
||||
}).toThrow();
|
||||
expect(() => {
|
||||
throw {
|
||||
message: "This is an object error",
|
||||
code: -1021,
|
||||
};
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
export default {
|
||||
async fetch(request: Request): Promise<Response> {
|
||||
a(request);
|
||||
const object = {
|
||||
a: "1",
|
||||
b: "2",
|
||||
c: new Map([[1, 2]]),
|
||||
};
|
||||
const coolThing: CoolThing = new SuperCoolThing();
|
||||
coolThing.doCoolThing();
|
||||
debugger;
|
||||
return new Response("BAI BAI");
|
||||
return new Response("Hello World");
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -131,35 +131,53 @@
|
||||
"description": "The path to Bun.",
|
||||
"default": "bun"
|
||||
},
|
||||
"runtimeArgs": {
|
||||
"type": "array",
|
||||
"description": "The command-line arguments passed to Bun.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": []
|
||||
},
|
||||
"program": {
|
||||
"type": "string",
|
||||
"description": "The file to debug.",
|
||||
"description": "The path to a JavaScript or TypeScript file.",
|
||||
"default": "${file}"
|
||||
},
|
||||
"args": {
|
||||
"type": "array",
|
||||
"description": "The command-line arguments passed to the program.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": []
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string",
|
||||
"description": "The working directory.",
|
||||
"default": "${workspaceFolder}"
|
||||
},
|
||||
"args": {
|
||||
"type": "array",
|
||||
"description": "The arguments passed to Bun.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": []
|
||||
},
|
||||
"env": {
|
||||
"type": "object",
|
||||
"description": "The environment variables passed to Bun.",
|
||||
"default": {}
|
||||
},
|
||||
"inheritEnv": {
|
||||
"strictEnv": {
|
||||
"type": "boolean",
|
||||
"description": "If environment variables should be inherited from the parent process.",
|
||||
"default": true
|
||||
"description": "If environment variables should not be inherited from the parent process.",
|
||||
"default": false
|
||||
},
|
||||
"watch": {
|
||||
"stopOnEntry": {
|
||||
"type": "boolean",
|
||||
"description": "If a breakpoint should be set at the first line.",
|
||||
"default": false
|
||||
},
|
||||
"noDebug": {
|
||||
"type": "boolean",
|
||||
"description": "If the debugger should be disabled.",
|
||||
"default": false
|
||||
},
|
||||
"watchMode": {
|
||||
"type": ["boolean", "string"],
|
||||
"description": "If the process should be restarted when files change.",
|
||||
"enum": [
|
||||
@@ -168,11 +186,6 @@
|
||||
"hot"
|
||||
],
|
||||
"default": false
|
||||
},
|
||||
"debug": {
|
||||
"type": "boolean",
|
||||
"description": "If the process should be started in debug mode.",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -181,6 +194,16 @@
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "The URL of the Bun process to attach to."
|
||||
},
|
||||
"noDebug": {
|
||||
"type": "boolean",
|
||||
"description": "If the debugger should be disabled.",
|
||||
"default": false
|
||||
},
|
||||
"stopOnEntry": {
|
||||
"type": "boolean",
|
||||
"description": "If a breakpoint should when the program is attached.",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ const debugConfiguration: vscode.DebugConfiguration = {
|
||||
request: "launch",
|
||||
name: "Debug Bun",
|
||||
program: "${file}",
|
||||
watch: false,
|
||||
stopOnEntry: false,
|
||||
watchMode: false,
|
||||
};
|
||||
|
||||
const runConfiguration: vscode.DebugConfiguration = {
|
||||
@@ -17,8 +18,8 @@ const runConfiguration: vscode.DebugConfiguration = {
|
||||
request: "launch",
|
||||
name: "Run Bun",
|
||||
program: "${file}",
|
||||
debug: false,
|
||||
watch: false,
|
||||
noDebug: true,
|
||||
watchMode: false,
|
||||
};
|
||||
|
||||
const attachConfiguration: vscode.DebugConfiguration = {
|
||||
@@ -48,15 +49,25 @@ export default function (context: vscode.ExtensionContext, factory?: vscode.Debu
|
||||
vscode.window.registerTerminalProfileProvider("bun", new TerminalProfileProvider()),
|
||||
);
|
||||
|
||||
const { terminalProfile } = new TerminalDebugSession();
|
||||
const { options } = terminalProfile;
|
||||
const terminal = vscode.window.createTerminal(options);
|
||||
terminal.show();
|
||||
context.subscriptions.push(terminal);
|
||||
const document = getActiveDocument();
|
||||
if (isJavaScript(document?.languageId)) {
|
||||
vscode.workspace.findFiles("bun.lockb", "node_modules", 1).then(files => {
|
||||
const { terminalProfile } = new TerminalDebugSession();
|
||||
const { options } = terminalProfile;
|
||||
const terminal = vscode.window.createTerminal(options);
|
||||
|
||||
const focus = files.length > 0;
|
||||
if (focus) {
|
||||
terminal.show();
|
||||
}
|
||||
|
||||
context.subscriptions.push(terminal);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function RunFileCommand(resource?: vscode.Uri): void {
|
||||
const path = getCurrentPath(resource);
|
||||
const path = getActivePath(resource);
|
||||
if (path) {
|
||||
vscode.debug.startDebugging(undefined, {
|
||||
...runConfiguration,
|
||||
@@ -67,7 +78,7 @@ function RunFileCommand(resource?: vscode.Uri): void {
|
||||
}
|
||||
|
||||
function DebugFileCommand(resource?: vscode.Uri): void {
|
||||
const path = getCurrentPath(resource);
|
||||
const path = getActivePath(resource);
|
||||
if (path) {
|
||||
vscode.debug.startDebugging(undefined, {
|
||||
...debugConfiguration,
|
||||
@@ -178,18 +189,36 @@ class TerminalDebugSession extends FileDebugSession {
|
||||
return new vscode.TerminalProfile({
|
||||
name: "Bun Terminal",
|
||||
env: {
|
||||
"BUN_INSPECT": `1${this.adapter.url}`,
|
||||
"BUN_INSPECT": `${this.adapter.url}?wait=1`,
|
||||
"BUN_INSPECT_NOTIFY": `${this.signal.url}`,
|
||||
},
|
||||
isTransient: true,
|
||||
iconPath: new vscode.ThemeIcon("debug-console"),
|
||||
});
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
this.signal.close();
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentPath(target?: vscode.Uri): string | undefined {
|
||||
if (!target && vscode.window.activeTextEditor) {
|
||||
target = vscode.window.activeTextEditor.document.uri;
|
||||
function getActiveDocument(): vscode.TextDocument | undefined {
|
||||
return vscode.window.activeTextEditor?.document;
|
||||
}
|
||||
|
||||
function getActivePath(target?: vscode.Uri): string | undefined {
|
||||
if (!target) {
|
||||
target = getActiveDocument()?.uri;
|
||||
}
|
||||
return target?.fsPath;
|
||||
}
|
||||
|
||||
function isJavaScript(languageId?: string): boolean {
|
||||
return (
|
||||
languageId === "javascript" ||
|
||||
languageId === "javascriptreact" ||
|
||||
languageId === "typescript" ||
|
||||
languageId === "typescriptreact"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ public:
|
||||
globalObject->setInspectable(true);
|
||||
auto& inspector = globalObject->inspectorDebuggable();
|
||||
inspector.setInspectable(true);
|
||||
globalObject->inspectorController().connectFrontend(*connection, true, waitingForConnection);
|
||||
globalObject->inspectorController().connectFrontend(*connection, true, false); // waitingForConnection
|
||||
|
||||
Inspector::JSGlobalObjectDebugger* debugger = reinterpret_cast<Inspector::JSGlobalObjectDebugger*>(globalObject->debugger());
|
||||
if (debugger) {
|
||||
@@ -482,7 +482,7 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionCreateConnection, (JSGlobalObject * globalObj
|
||||
return JSValue::encode(JSBunInspectorConnection::create(vm, JSBunInspectorConnection::createStructure(vm, globalObject, globalObject->objectPrototype()), connection));
|
||||
}
|
||||
|
||||
extern "C" BunString Bun__startJSDebuggerThread(Zig::GlobalObject* debuggerGlobalObject, ScriptExecutionContextIdentifier scriptId, BunString* portOrPathString)
|
||||
extern "C" void Bun__startJSDebuggerThread(Zig::GlobalObject* debuggerGlobalObject, ScriptExecutionContextIdentifier scriptId, BunString* portOrPathString)
|
||||
{
|
||||
if (!debuggerScriptExecutionContext)
|
||||
debuggerScriptExecutionContext = debuggerGlobalObject->scriptExecutionContext();
|
||||
@@ -498,12 +498,7 @@ extern "C" BunString Bun__startJSDebuggerThread(Zig::GlobalObject* debuggerGloba
|
||||
arguments.append(JSFunction::create(vm, debuggerGlobalObject, 1, String("send"_s), jsFunctionSend, ImplementationVisibility::Public));
|
||||
arguments.append(JSFunction::create(vm, debuggerGlobalObject, 0, String("disconnect"_s), jsFunctionDisconnect, ImplementationVisibility::Public));
|
||||
|
||||
JSValue serverURLValue = JSC::call(debuggerGlobalObject, debuggerDefaultFn, arguments, "Bun__initJSDebuggerThread - debuggerDefaultFn"_s);
|
||||
|
||||
if (serverURLValue.isUndefinedOrNull())
|
||||
return BunStringEmpty;
|
||||
|
||||
return Bun::toStringRef(debuggerGlobalObject, serverURLValue);
|
||||
JSC::call(debuggerGlobalObject, debuggerDefaultFn, arguments, "Bun__initJSDebuggerThread - debuggerDefaultFn"_s);
|
||||
}
|
||||
|
||||
enum class AsyncCallTypeUint8 : uint8_t {
|
||||
|
||||
@@ -502,6 +502,7 @@ pub const VirtualMachine = struct {
|
||||
worker: ?*JSC.WebWorker = null,
|
||||
|
||||
debugger: ?Debugger = null,
|
||||
has_started_debugger: bool = false,
|
||||
|
||||
pub const OnUnhandledRejection = fn (*VirtualMachine, globalObject: *JSC.JSGlobalObject, JSC.JSValue) void;
|
||||
|
||||
@@ -794,7 +795,8 @@ pub const VirtualMachine = struct {
|
||||
pub var has_created_debugger: bool = false;
|
||||
|
||||
pub const Debugger = struct {
|
||||
path_or_port: []const u8 = "",
|
||||
path_or_port: ?[]const u8 = null,
|
||||
unix: []const u8 = "",
|
||||
script_execution_context_id: u32 = 0,
|
||||
next_debugger_id: u64 = 1,
|
||||
poll_ref: JSC.PollRef = .{},
|
||||
@@ -805,8 +807,7 @@ pub const VirtualMachine = struct {
|
||||
|
||||
extern "C" fn Bun__createJSDebugger(*JSC.JSGlobalObject) u32;
|
||||
extern "C" fn Bun__ensureDebugger(u32, bool) void;
|
||||
extern "C" fn Bun__startJSDebuggerThread(*JSC.JSGlobalObject, u32, *bun.String) bun.String;
|
||||
var has_started_debugger_thread: bool = false;
|
||||
extern "C" fn Bun__startJSDebuggerThread(*JSC.JSGlobalObject, u32, *bun.String) void;
|
||||
var futex_atomic: std.atomic.Atomic(u32) = undefined;
|
||||
|
||||
pub fn create(this: *VirtualMachine, globalObject: *JSGlobalObject) !void {
|
||||
@@ -816,8 +817,8 @@ pub const VirtualMachine = struct {
|
||||
has_created_debugger = true;
|
||||
var debugger = &this.debugger.?;
|
||||
debugger.script_execution_context_id = Bun__createJSDebugger(globalObject);
|
||||
if (!has_started_debugger_thread) {
|
||||
has_started_debugger_thread = true;
|
||||
if (!this.has_started_debugger) {
|
||||
this.has_started_debugger = true;
|
||||
futex_atomic = std.atomic.Atomic(u32).init(0);
|
||||
var thread = try std.Thread.spawn(.{}, startJSDebuggerThread, .{this});
|
||||
thread.detach();
|
||||
@@ -865,8 +866,6 @@ pub const VirtualMachine = struct {
|
||||
vm.global.vm().holdAPILock(other_vm, @ptrCast(&start));
|
||||
}
|
||||
|
||||
pub export var Bun__debugger_server_url: bun.String = undefined;
|
||||
|
||||
pub export fn Debugger__didConnect() void {
|
||||
var this = VirtualMachine.get();
|
||||
std.debug.assert(this.debugger.?.wait_for_connection);
|
||||
@@ -878,9 +877,17 @@ pub const VirtualMachine = struct {
|
||||
JSC.markBinding(@src());
|
||||
|
||||
var this = VirtualMachine.get();
|
||||
var str = bun.String.create(other_vm.debugger.?.path_or_port);
|
||||
Bun__debugger_server_url = Bun__startJSDebuggerThread(this.global, other_vm.debugger.?.script_execution_context_id, &str);
|
||||
Bun__debugger_server_url.toThreadSafe();
|
||||
var debugger = other_vm.debugger.?;
|
||||
|
||||
if (debugger.unix.len > 0) {
|
||||
var url = bun.String.create(debugger.unix);
|
||||
Bun__startJSDebuggerThread(this.global, debugger.script_execution_context_id, &url);
|
||||
}
|
||||
|
||||
if (debugger.path_or_port) |path_or_port| {
|
||||
var url = bun.String.create(path_or_port);
|
||||
Bun__startJSDebuggerThread(this.global, debugger.script_execution_context_id, &url);
|
||||
}
|
||||
|
||||
this.global.handleRejectedPromises();
|
||||
|
||||
@@ -1189,13 +1196,27 @@ pub const VirtualMachine = struct {
|
||||
}
|
||||
|
||||
fn configureDebugger(this: *VirtualMachine, debugger: bun.CLI.Command.Debugger) void {
|
||||
var unix = bun.getenvZ("BUN_INSPECT") orelse "";
|
||||
var set_breakpoint_on_first_line = unix.len > 0 and strings.endsWith(unix, "?break=1");
|
||||
var wait_for_connection = set_breakpoint_on_first_line or (unix.len > 0 and strings.endsWith(unix, "?wait=1"));
|
||||
|
||||
switch (debugger) {
|
||||
.unspecified => {},
|
||||
.unspecified => {
|
||||
if (unix.len > 0) {
|
||||
this.debugger = Debugger{
|
||||
.path_or_port = null,
|
||||
.unix = unix,
|
||||
.wait_for_connection = wait_for_connection,
|
||||
.set_breakpoint_on_first_line = set_breakpoint_on_first_line,
|
||||
};
|
||||
}
|
||||
},
|
||||
.enable => {
|
||||
this.debugger = Debugger{
|
||||
.path_or_port = debugger.enable.path_or_port,
|
||||
.wait_for_connection = debugger.enable.wait_for_connection,
|
||||
.set_breakpoint_on_first_line = debugger.enable.set_breakpoint_on_first_line,
|
||||
.unix = unix,
|
||||
.wait_for_connection = wait_for_connection or debugger.enable.wait_for_connection,
|
||||
.set_breakpoint_on_first_line = set_breakpoint_on_first_line or debugger.enable.set_breakpoint_on_first_line,
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
@@ -543,15 +543,6 @@ pub const Arguments = struct {
|
||||
.wait_for_connection = true,
|
||||
.set_breakpoint_on_first_line = true,
|
||||
} };
|
||||
} else if (bun.getenvZ("BUN_INSPECT")) |inspect_value| {
|
||||
ctx.runtime_options.debugger = if (inspect_value.len == 0 or inspect_value[0] == '0')
|
||||
Command.Debugger{ .unspecified = {} }
|
||||
else
|
||||
Command.Debugger{ .enable = .{
|
||||
.path_or_port = inspect_value[1..],
|
||||
.wait_for_connection = inspect_value[0] == '1' or inspect_value[0] == '2',
|
||||
.set_breakpoint_on_first_line = inspect_value[0] == '2',
|
||||
} };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,309 +1,322 @@
|
||||
import type * as BunType from "bun";
|
||||
import type { Server as WebSocketServer, WebSocketHandler, ServerWebSocket, SocketHandler, Socket } 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++;
|
||||
|
||||
send(msg: string) {
|
||||
sendFn_.call(this.debugger, msg);
|
||||
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);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
disconnectFn_.call(this.debugger);
|
||||
this.messageQueue.length = 0;
|
||||
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());
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
let generatedPath: string = "";
|
||||
function generatePath() {
|
||||
if (!generatedPath) {
|
||||
generatedPath = "/" + Math.random().toString(36).slice(2);
|
||||
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();
|
||||
}
|
||||
|
||||
return generatedPath;
|
||||
get url(): URL {
|
||||
return this.#url;
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
if (protocol === "ws+unix:") {
|
||||
Bun.serve({
|
||||
unix: pathname,
|
||||
fetch: this.#fetch.bind(this),
|
||||
websocket: this.#websocket,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
throw new TypeError(`Unsupported protocol: '${protocol}' (expected 'ws:', 'ws+unix:', or 'unix:')`);
|
||||
}
|
||||
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
#fetch(request: Request, server: WebSocketServer): Response | undefined {
|
||||
const { method, url, headers } = request;
|
||||
const { pathname } = new URL(url);
|
||||
|
||||
if (method !== "GET") {
|
||||
return new Response(null, {
|
||||
status: 405, // Method Not Allowed
|
||||
});
|
||||
}
|
||||
|
||||
switch (pathname) {
|
||||
case "/json/version":
|
||||
return Response.json(versionInfo());
|
||||
case "/json":
|
||||
case "/json/list":
|
||||
// TODO?
|
||||
}
|
||||
|
||||
if (!this.#url.protocol.includes("unix") && this.#url.pathname !== pathname) {
|
||||
return new Response(null, {
|
||||
status: 404, // Not Found
|
||||
});
|
||||
}
|
||||
|
||||
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",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
#open(connection: ConnectionOwner, writer: Writer): void {
|
||||
const { data } = connection;
|
||||
const { refEventLoop } = data;
|
||||
|
||||
const client = bufferedWriter(writer);
|
||||
const backend = this.#createBackend(refEventLoop, (...messages: string[]) => {
|
||||
for (const message of messages) {
|
||||
client.write(message);
|
||||
}
|
||||
});
|
||||
|
||||
data.client = client;
|
||||
data.backend = backend;
|
||||
}
|
||||
|
||||
#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 terminalLink(url) {
|
||||
if (colors) {
|
||||
// bold + hyperlink + reset
|
||||
return "\x1b[1m\x1b]8;;" + url + "\x1b\\" + url + "\x1b]8;;\x1b\\" + "\x1b[22m";
|
||||
}
|
||||
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);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
drain: () => {
|
||||
draining = true;
|
||||
try {
|
||||
for (let i = 0; i < pendingMessages.length; i++) {
|
||||
if (!writer.write(pendingMessages[i])) {
|
||||
pendingMessages = pendingMessages.slice(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
draining = false;
|
||||
}
|
||||
},
|
||||
close: () => {
|
||||
writer.close();
|
||||
pendingMessages.length = 0;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const defaultHostname = "localhost";
|
||||
const defaultPort = 6499;
|
||||
|
||||
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}'`);
|
||||
}
|
||||
}
|
||||
|
||||
function randomId() {
|
||||
return Math.random().toString(36).slice(2);
|
||||
}
|
||||
|
||||
const { enableANSIColors } = Bun;
|
||||
|
||||
function dim(string: string): string {
|
||||
if (enableANSIColors) {
|
||||
return `\x1b[2m${string}\x1b[22m`;
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
function link(url: string): string {
|
||||
if (enableANSIColors) {
|
||||
return `\x1b[1m\x1b]8;;${url}\x1b\\${url}\x1b]8;;\x1b\\\x1b[22m`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function dim(text) {
|
||||
if (colors) {
|
||||
return "\x1b[2m" + text + "\x1b[22m";
|
||||
function reset(): string {
|
||||
if (enableANSIColors) {
|
||||
return "\x1b[49m";
|
||||
}
|
||||
|
||||
return text;
|
||||
return "";
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
},
|
||||
},
|
||||
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();
|
||||
}
|
||||
|
||||
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<DebuggerWithMessageQueue>({
|
||||
...serveOptions,
|
||||
port: portNumber++,
|
||||
});
|
||||
if (isUnix) {
|
||||
notify();
|
||||
}
|
||||
} catch (e) {
|
||||
lastError = e;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
server = Bun.serve<DebuggerWithMessageQueue>({
|
||||
...serveOptions,
|
||||
port: portNumber,
|
||||
});
|
||||
if (isUnix) {
|
||||
notify();
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
|
||||
if (!this.url) {
|
||||
return server;
|
||||
}
|
||||
|
||||
// yellow foreground
|
||||
writeToConsole(dim(`------------------ Bun Inspector ------------------` + "\n"));
|
||||
if (colors) {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
function notify(): void {
|
||||
const unix = process.env["BUN_INSPECT_NOTIFY"];
|
||||
if (!unix || !unix.startsWith("unix://")) {
|
||||
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;
|
||||
type ConnectionOwner = {
|
||||
data: Connection;
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
type Connection = {
|
||||
refEventLoop: boolean;
|
||||
client?: Writer;
|
||||
backend?: Writer;
|
||||
};
|
||||
|
||||
return `http://${listener.server.hostname}:${listener.server.port}${listener.url}`;
|
||||
}
|
||||
type Writer = {
|
||||
write: (message: string) => boolean;
|
||||
drain?: () => void;
|
||||
close: () => void;
|
||||
};
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user