Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix for windows debug support #14048

Merged
merged 19 commits into from
Sep 21, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 93 additions & 28 deletions packages/bun-debug-adapter-protocol/src/debugger/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,19 @@ import type { InspectorEventMap } from "../../../bun-inspector-protocol/src/insp
import type { JSC } from "../../../bun-inspector-protocol/src/protocol";
import type { DAP } from "../protocol";
// @ts-ignore
import type { ChildProcess } from "node:child_process";
import { spawn } from "node:child_process";
import { spawn, ChildProcess } from "node:child_process";
import { EventEmitter } from "node:events";
import { WebSocketInspector, remoteObjectToString } from "../../../bun-inspector-protocol/index";
import { UnixSignal, randomUnixPath } from "./signal";
import { randomUnixPath, UnixSignal, WebSocketSignal } from "./signal";
import { Location, SourceMap } from "./sourcemap";
import { createServer, AddressInfo } from "node:net";

export function getAvailablePort(): number {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to wait for the callback to be called?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it works fine without waiting for callback

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does node gurantee that the address will always be set before the listen callback is invoked?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

server.address() returns null before the 'listening' event has been emitted or after calling server.close().

const server = createServer().listen(0);
const port = (server.address() as AddressInfo).port;
server.close();
return port;
}

const capabilities: DAP.Capabilities = {
supportsConfigurationDoneRequest: true,
Expand Down Expand Up @@ -215,6 +222,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
#variables: Map<number, Variable>;
#initialized?: InitializeRequest;
#options?: DebuggerOptions;
#signal?: WebSocketSignal;

constructor(url?: string | URL) {
super();
Expand Down Expand Up @@ -489,36 +497,93 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
...env,
};

const url = `ws+unix://${randomUnixPath()}`;
const signal = new UnixSignal();
if (process.platform !== "win32") {
// we're on unix
const url = `ws+unix://${randomUnixPath()}`;
const signal = new UnixSignal();

signal.on("Signal.received", () => {
this.#attach({ url });
});
signal.on("Signal.received", () => {
this.#attach({ url });
});

this.once("Adapter.terminated", () => {
signal.close();
});
this.once("Adapter.terminated", () => {
signal.close();
});

const query = stopOnEntry ? "break=1" : "wait=1";
processEnv["BUN_INSPECT"] = `${url}?${query}`;
processEnv["BUN_INSPECT_NOTIFY"] = signal.url;
const query = stopOnEntry ? "break=1" : "wait=1";
processEnv["BUN_INSPECT"] = `${url}?${query}`;
processEnv["BUN_INSPECT_NOTIFY"] = signal.url;

// This is probably not correct, but it's the best we can do for now.
processEnv["FORCE_COLOR"] = "1";
processEnv["BUN_QUIET_DEBUG_LOGS"] = "1";
processEnv["BUN_DEBUG_QUIET_LOGS"] = "1";

const started = await this.#spawn({
command: runtime,
args: processArgs,
env: processEnv,
cwd,
isDebugee: true,
});

// This is probably not correct, but it's the best we can do for now.
processEnv["FORCE_COLOR"] = "1";
processEnv["BUN_QUIET_DEBUG_LOGS"] = "1";
processEnv["BUN_DEBUG_QUIET_LOGS"] = "1";
if (!started) {
throw new Error("Program could not be started.");
}
} else {
// we're on windows
// Create WebSocketSignal
const signalUrl = `ws://localhost:${getAvailablePort()}`;
this.#signal = new WebSocketSignal(signalUrl);

const started = await this.#spawn({
command: runtime,
args: processArgs,
env: processEnv,
cwd,
isDebugee: true,
});
// Set BUN_INSPECT_NOTIFY to signal's URL
processEnv["BUN_INSPECT_NOTIFY"] = this.#signal.url;

// Set BUN_INSPECT to the debugger's inspector URL with ?wait=1
processEnv["BUN_INSPECT"] = `${this.#inspector.url}?wait=1`;

// Spawn the process
const child = spawn(runtime, processArgs, {
cwd,
env: processEnv,
stdio: ["ignore", "pipe", "pipe"],
});

if (!started) {
throw new Error("Program could not be started.");
this.#process = child;

// Attach event listeners for the process
child.stderr?.setEncoding("utf-8");
child.stderr?.on("data", data => this.emit("Process.stderr", data));
child.on("exit", (code, signal) => {
this.emit("Process.exited", code ?? signal ?? null, signal ?? null);
});

this.emit("Process.spawned", child);

// Wait for the signal from the debuggee
await this.#signal.ready;
await new Promise<void>((resolve, reject) => {
this.#signal!.once("Signal.received", () => {
resolve();
});
this.#signal!.once("Signal.error", error => {
reject(error);
});
});

// Connect the debugger to the debuggee
const connected = await this.#inspector.start();
if (!connected) {
this.terminate();
}

// Additional setup if needed
this.emit("Adapter.process", {
name: `${runtime} ${processArgs.join(" ")}`,
systemProcessId: child.pid,
isLocalProcess: true,
startMethod: "launch",
});
}
}

Expand Down Expand Up @@ -2107,7 +2172,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements

close(): void {
this.#process?.kill();
// this.#signal?.close();
if (process.platform === "win32") this.#signal?.close();
this.#inspector.close();
this.#reset();
}
Expand Down
74 changes: 74 additions & 0 deletions packages/bun-debug-adapter-protocol/src/debugger/signal.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { EventEmitter } from "node:events";
import { WebSocketServer } from "ws";
import type { Server } from "node:net";
import { createServer } from "node:net";
import { tmpdir } from "node:os";
Expand Down Expand Up @@ -85,3 +86,76 @@ function parseUnixPath(path: string | URL): string {
throw new Error(`Invalid UNIX path: ${path}`);
}
}

export type WebSocketSignalEventMap = {
"Signal.listening": [string];
"Signal.error": [Error];
"Signal.received": [string];
"Signal.closed": [];
};

/**
* Starts a server that listens for signals over a WebSocket.
*/
export class WebSocketSignal extends EventEmitter<WebSocketSignalEventMap> {
#url: string;
#server: WebSocketServer;
#ready: Promise<void>;

constructor(url: string) {
super();
this.#url = url;
const port = getPortFromUrl(url);
this.#server = new WebSocketServer({ port });

this.#server.on("listening", () => this.emit("Signal.listening", this.#url));
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("message", data => {
this.emit("Signal.received", data.toString());
});
});
this.#ready = new Promise((resolve, reject) => {
this.#server.on("listening", resolve);
this.#server.on("error", reject);
});
}

emit<E extends keyof WebSocketSignalEventMap>(event: E, ...args: WebSocketSignalEventMap[E]): boolean {
if (isDebug) {
console.log(event, ...args);
}
return super.emit(event, ...args);
}

/**
* The WebSocket URL.
*/
get url(): string {
return this.#url;
}

/**
* 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 getPortFromUrl(url: string): number {
try {
const parsedUrl = new URL(url);
return parseInt(parsedUrl.port, 10) || 0;
} catch {
throw new Error(`Invalid WebSocket URL: ${url}`);
}
}
8 changes: 8 additions & 0 deletions packages/bun-vscode/example/hello.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
// @bun
console.log("HELLO");
console.log(process.argv0);
console.log("HELLO 2");
console.log("HELLO 3");
a();

function a() {
console.log("HELLO 4");
}

let i = 0;
while (true) {
console.log(i);
i++;
}
2 changes: 1 addition & 1 deletion packages/bun-vscode/scripts/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ buildSync({
entryPoints: ["src/extension.ts", "src/web-extension.ts"],
outdir: "dist",
bundle: true,
external: ["vscode"],
external: ["vscode", "ws"],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this intentional?

platform: "node",
format: "cjs",
// The following settings are required to allow for extension debugging
Expand Down
14 changes: 10 additions & 4 deletions packages/bun-vscode/src/features/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { DebugSession } from "@vscode/debugadapter";
import { tmpdir } from "node:os";
import * as vscode from "vscode";
import type { DAP } from "../../../bun-debug-adapter-protocol";
import { DebugAdapter, UnixSignal } from "../../../bun-debug-adapter-protocol";
import { DebugAdapter, getAvailablePort, WebSocketSignal, UnixSignal } from "../../../bun-debug-adapter-protocol";

export const DEBUG_CONFIGURATION: vscode.DebugConfiguration = {
type: "bun",
Expand Down Expand Up @@ -176,7 +176,8 @@ class FileDebugSession extends DebugSession {
constructor(sessionId?: string) {
super();
const uniqueId = sessionId ?? Math.random().toString(36).slice(2);
const url = `ws+unix://${tmpdir()}/${uniqueId}.sock`;
const url =
process.platform === "win32" ? `ws://localhost:${getAvailablePort()}` : `ws+unix://${tmpdir()}/${uniqueId}.sock`;

this.adapter = new DebugAdapter(url);
this.adapter.on("Adapter.response", response => this.sendResponse(response));
Expand Down Expand Up @@ -204,11 +205,16 @@ class FileDebugSession extends DebugSession {
}

class TerminalDebugSession extends FileDebugSession {
readonly signal: UnixSignal;
readonly signal: WebSocketSignal | UnixSignal;

constructor() {
super();
this.signal = new UnixSignal();
if (process.platform === "win32") {
const signalUrl = `ws://localhost:${getAvailablePort()}`;
this.signal = new WebSocketSignal(signalUrl);
} else {
this.signal = new UnixSignal();
}
this.signal.on("Signal.received", () => {
vscode.debug.startDebugging(undefined, {
...ATTACH_CONFIGURATION,
Expand Down
36 changes: 29 additions & 7 deletions src/js/internal/debugger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,21 @@ export default function (
Bun.write(Bun.stderr, dim("--------------------- Bun Inspector ---------------------") + reset() + "\n");
}

const unix = process.env["BUN_INSPECT_NOTIFY"];
if (unix) {
const { protocol, pathname } = parseUrl(unix);
if (protocol === "unix:") {
notify(pathname);
if (process.platform === "win32") {
const notifyUrl = process.env["BUN_INSPECT_NOTIFY"];
if (notifyUrl) {
const { protocol } = new URL(notifyUrl);
if (protocol === "ws:" || protocol === "wss:") {
notifyWebSocket(notifyUrl);
}
}
} else {
const unix = process.env["BUN_INSPECT_NOTIFY"];
if (unix) {
const { protocol, pathname } = parseUrl(unix);
if (protocol === "unix:") {
notify(pathname);
}
}
}
}
Expand Down Expand Up @@ -73,7 +83,7 @@ class Debugger {
#listen(): void {
const { protocol, hostname, port, pathname } = this.#url;

if (protocol === "ws:" || protocol === "ws+tcp:") {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're allowing wss, where is the TLS configuration specified?

if (protocol === "ws:" || protocol === "wss:" || protocol === "ws+tcp:") {
const server = Bun.serve({
hostname,
port,
Expand All @@ -94,7 +104,7 @@ class Debugger {
return;
}

throw new TypeError(`Unsupported protocol: '${protocol}' (expected 'ws:', 'ws+unix:', or 'unix:')`);
throw new TypeError(`Unsupported protocol: '${protocol}' (expected 'ws:', 'ws+unix:', or 'wss:')`);
}

get #websocket(): WebSocketHandler<Connection> {
Expand Down Expand Up @@ -321,6 +331,18 @@ function reset(): string {
return "";
}

function notifyWebSocket(url: string): void {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, it's a whole websocket server for this? We can just make a TCP connection.

const ws = new WebSocket(url);
ws.onopen = () => {
ws.send("1");
ws.close();
};
ws.onerror = error => {
// Handle error if needed
console.error("WebSocket error:", error);
};
}

function notify(unix: string): void {
Bun.connect({
unix,
Expand Down
Loading