mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
fix for windows debug support (#14048)
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
This commit is contained in:
@@ -2,12 +2,26 @@ 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, TCPSocketSignal, UnixSignal } from "./signal";
|
||||
import { Location, SourceMap } from "./sourcemap";
|
||||
import { createServer, AddressInfo } from "node:net";
|
||||
import * as path from "node:path";
|
||||
|
||||
export async function getAvailablePort(): Promise<number> {
|
||||
const server = createServer();
|
||||
server.listen(0);
|
||||
return new Promise((resolve, reject) => {
|
||||
server.on("listening", () => {
|
||||
const { port } = server.address() as AddressInfo;
|
||||
server.close(() => {
|
||||
resolve(port);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const capabilities: DAP.Capabilities = {
|
||||
supportsConfigurationDoneRequest: true,
|
||||
@@ -489,36 +503,73 @@ 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";
|
||||
// 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,
|
||||
});
|
||||
const started = await this.#spawn({
|
||||
command: runtime,
|
||||
args: processArgs,
|
||||
env: processEnv,
|
||||
cwd,
|
||||
isDebugee: true,
|
||||
});
|
||||
|
||||
if (!started) {
|
||||
throw new Error("Program could not be started.");
|
||||
if (!started) {
|
||||
throw new Error("Program could not be started.");
|
||||
}
|
||||
} else {
|
||||
// we're on windows
|
||||
// Create TCPSocketSignal
|
||||
const url = `ws://127.0.0.1:${await getAvailablePort()}/${getRandomId()}`; // 127.0.0.1 so it resolves correctly on windows
|
||||
const signal = new TCPSocketSignal(await getAvailablePort());
|
||||
|
||||
signal.on("Signal.received", async () => {
|
||||
this.#attach({ url });
|
||||
});
|
||||
|
||||
this.once("Adapter.terminated", () => {
|
||||
signal.close();
|
||||
});
|
||||
|
||||
const query = stopOnEntry ? "break=1" : "wait=1";
|
||||
processEnv["BUN_INSPECT"] = `${url}?${query}`;
|
||||
processEnv["BUN_INSPECT_NOTIFY"] = signal.url; // 127.0.0.1 so it resolves correctly on windows
|
||||
|
||||
// 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,
|
||||
});
|
||||
|
||||
if (!started) {
|
||||
throw new Error("Program could not be started.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -684,6 +735,9 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
|
||||
async breakpointLocations(request: DAP.BreakpointLocationsRequest): Promise<DAP.BreakpointLocationsResponse> {
|
||||
const { line, endLine, column, endColumn, source: source0 } = request;
|
||||
if (process.platform === "win32") {
|
||||
source0.path = source0.path ? normalizeWindowsPath(source0.path) : source0.path;
|
||||
}
|
||||
const source = await this.#getSource(sourceToId(source0));
|
||||
|
||||
const { locations } = await this.send("Debugger.getBreakpointLocations", {
|
||||
@@ -788,6 +842,9 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
}
|
||||
|
||||
async #setBreakpointsByUrl(url: string, requests: DAP.SourceBreakpoint[], unsetOld?: boolean): Promise<Breakpoint[]> {
|
||||
if (process.platform === "win32") {
|
||||
url = url ? normalizeWindowsPath(url) : url;
|
||||
}
|
||||
const source = this.#getSourceIfPresent(url);
|
||||
|
||||
// If the source is not loaded, set a placeholder breakpoint at the start of the file.
|
||||
@@ -1161,6 +1218,9 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
|
||||
async gotoTargets(request: DAP.GotoTargetsRequest): Promise<DAP.GotoTargetsResponse> {
|
||||
const { source: source0 } = request;
|
||||
if (process.platform === "win32") {
|
||||
source0.path = source0.path ? normalizeWindowsPath(source0.path) : source0.path;
|
||||
}
|
||||
const source = await this.#getSource(sourceToId(source0));
|
||||
|
||||
const { breakpoints } = await this.breakpointLocations(request);
|
||||
@@ -1327,7 +1387,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
// 1. If it has a `path`, the client retrieves the source from the file system.
|
||||
// 2. If it has a `sourceReference`, the client sends a `source` request.
|
||||
// Moreover, the code is usually shown in a read-only editor.
|
||||
const isUserCode = url.startsWith("/");
|
||||
const isUserCode = path.isAbsolute(url);
|
||||
const sourceMap = SourceMap(sourceMapURL);
|
||||
const name = sourceName(url);
|
||||
const presentationHint = sourcePresentationHint(url);
|
||||
@@ -1646,12 +1706,11 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
|
||||
// If the source does not have a path or is a builtin module,
|
||||
// it cannot be retrieved from the file system.
|
||||
if (typeof sourceId === "number" || !sourceId.startsWith("/")) {
|
||||
if (typeof sourceId === "number" || !path.isAbsolute(sourceId)) {
|
||||
throw new Error(`Source not found: ${sourceId}`);
|
||||
}
|
||||
|
||||
// If the source is not present, it may not have been loaded yet.
|
||||
// In that case, wait for it to be loaded.
|
||||
let resolves = this.#pendingSources.get(sourceId);
|
||||
if (!resolves) {
|
||||
this.#pendingSources.set(sourceId, (resolves = []));
|
||||
@@ -2107,7 +2166,6 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
|
||||
close(): void {
|
||||
this.#process?.kill();
|
||||
// this.#signal?.close();
|
||||
this.#inspector.close();
|
||||
this.#reset();
|
||||
}
|
||||
@@ -2149,10 +2207,10 @@ function titleize(name: string): string {
|
||||
}
|
||||
|
||||
function sourcePresentationHint(url?: string): DAP.Source["presentationHint"] {
|
||||
if (!url || !url.startsWith("/")) {
|
||||
if (!url || !path.isAbsolute(url)) {
|
||||
return "deemphasize";
|
||||
}
|
||||
if (url.includes("/node_modules/")) {
|
||||
if (url.includes("/node_modules/") || url.includes("\\node_modules\\")) {
|
||||
return "normal";
|
||||
}
|
||||
return "emphasize";
|
||||
@@ -2163,6 +2221,9 @@ function sourceName(url?: string): string {
|
||||
return "unknown.js";
|
||||
}
|
||||
if (isJavaScript(url)) {
|
||||
if (process.platform === "win32") {
|
||||
url = url.replaceAll("\\", "/");
|
||||
}
|
||||
return url.split("/").pop() || url;
|
||||
}
|
||||
return `${url}.js`;
|
||||
@@ -2567,3 +2628,15 @@ let sequence = 1;
|
||||
function nextId(): number {
|
||||
return sequence++;
|
||||
}
|
||||
|
||||
export function getRandomId() {
|
||||
return Math.random().toString(36).slice(2);
|
||||
}
|
||||
|
||||
export function normalizeWindowsPath(winPath: string): string {
|
||||
winPath = path.normalize(winPath);
|
||||
if (winPath[1] === ":" && (winPath[2] === "\\" || winPath[2] === "/")) {
|
||||
return (winPath.charAt(0).toUpperCase() + winPath.slice(1)).replaceAll("\\\\", "\\");
|
||||
}
|
||||
return winPath;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import type { Server } from "node:net";
|
||||
import type { Server, Socket } from "node:net";
|
||||
import { createServer } from "node:net";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
@@ -85,3 +85,75 @@ function parseUnixPath(path: string | URL): string {
|
||||
throw new Error(`Invalid UNIX path: ${path}`);
|
||||
}
|
||||
}
|
||||
|
||||
export type TCPSocketSignalEventMap = {
|
||||
"Signal.listening": [];
|
||||
"Signal.error": [Error];
|
||||
"Signal.closed": [];
|
||||
"Signal.received": [string];
|
||||
};
|
||||
|
||||
export class TCPSocketSignal extends EventEmitter {
|
||||
#port: number;
|
||||
#server: ReturnType<typeof createServer>;
|
||||
#ready: Promise<void>;
|
||||
|
||||
constructor(port: number) {
|
||||
super();
|
||||
this.#port = port;
|
||||
|
||||
this.#server = createServer((socket: Socket) => {
|
||||
socket.on("data", data => {
|
||||
this.emit("Signal.received", data.toString());
|
||||
});
|
||||
|
||||
socket.on("error", error => {
|
||||
this.emit("Signal.error", error);
|
||||
});
|
||||
|
||||
socket.on("close", () => {
|
||||
this.emit("Signal.closed");
|
||||
});
|
||||
});
|
||||
|
||||
this.#ready = new Promise((resolve, reject) => {
|
||||
this.#server.listen(this.#port, () => {
|
||||
this.emit("Signal.listening");
|
||||
resolve();
|
||||
});
|
||||
this.#server.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
emit<E extends keyof TCPSocketSignalEventMap>(event: E, ...args: TCPSocketSignalEventMap[E]): boolean {
|
||||
if (isDebug) {
|
||||
console.log(event, ...args);
|
||||
}
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* The TCP port.
|
||||
*/
|
||||
get port(): number {
|
||||
return this.#port;
|
||||
}
|
||||
|
||||
get url(): string {
|
||||
return `tcp://127.0.0.1:${this.#port}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -1,8 +0,0 @@
|
||||
console.log("HELLO");
|
||||
console.log("HELLO 2");
|
||||
console.log("HELLO 3");
|
||||
a();
|
||||
|
||||
function a() {
|
||||
console.log("HELLO 4");
|
||||
}
|
||||
9
packages/bun-vscode/example/hello.ts
Normal file
9
packages/bun-vscode/example/hello.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
type OS = "Windows";
|
||||
|
||||
Bun.serve({
|
||||
fetch(req: Request) {
|
||||
return new Response(
|
||||
`Hello, ${"Windows" as OS}!`
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bun-vscode",
|
||||
"version": "0.0.8",
|
||||
"version": "0.0.13",
|
||||
"author": "oven",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { buildSync } from "esbuild";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { execSync } from "node:child_process";
|
||||
import { cpSync, mkdirSync, rmSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const { pathname } = new URL("..", import.meta.url);
|
||||
let { pathname } = new URL("..", import.meta.url);
|
||||
if (process.platform === "win32") {
|
||||
pathname = path.normalize(pathname).substring(1); // remove leading slash
|
||||
}
|
||||
process.chdir(pathname);
|
||||
|
||||
buildSync({
|
||||
@@ -26,7 +30,7 @@ cpSync("LICENSE", "extension/LICENSE");
|
||||
cpSync("package.json", "extension/package.json");
|
||||
|
||||
const cmd = process.isBun ? "bunx" : "npx";
|
||||
spawnSync(cmd, ["vsce", "package"], {
|
||||
execSync(`${cmd} vsce package --no-dependencies`, {
|
||||
cwd: "extension",
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { exec } from "node:child_process";
|
||||
import { readdirSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const { pathname } = new URL("..", import.meta.url);
|
||||
let { pathname } = new URL("..", import.meta.url);
|
||||
if (process.platform === "win32") {
|
||||
pathname = path.normalize(pathname).substring(1); // remove leading slash
|
||||
}
|
||||
process.chdir(pathname);
|
||||
|
||||
let path;
|
||||
let extPath;
|
||||
for (const filename of readdirSync("extension")) {
|
||||
if (filename.endsWith(".vsix")) {
|
||||
path = `extension/${filename}`;
|
||||
extPath = `extension/${filename}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!path) {
|
||||
if (!extPath) {
|
||||
throw new Error("No .vsix file found");
|
||||
}
|
||||
|
||||
spawn("code", ["--new-window", `--install-extension=${path}`, `--extensionDevelopmentPath=${pathname}`, "example"], {
|
||||
exec(`code --new-window --install-extension=${path} --extensionDevelopmentPath=${pathname} example`, {
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
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 { DAP, TCPSocketSignal } from "../../../bun-debug-adapter-protocol";
|
||||
import { DebugAdapter, getAvailablePort, UnixSignal, getRandomId } from "../../../bun-debug-adapter-protocol";
|
||||
|
||||
export const DEBUG_CONFIGURATION: vscode.DebugConfiguration = {
|
||||
type: "bun",
|
||||
@@ -81,7 +81,7 @@ function debugFileCommand(resource?: vscode.Uri) {
|
||||
if (path) debugCommand(path);
|
||||
}
|
||||
|
||||
function injectDebugTerminal(terminal: vscode.Terminal): void {
|
||||
async function injectDebugTerminal(terminal: vscode.Terminal): Promise<void> {
|
||||
if (!getConfig("debugTerminal.enabled")) return;
|
||||
|
||||
const { name, creationOptions } = terminal;
|
||||
@@ -97,14 +97,16 @@ function injectDebugTerminal(terminal: vscode.Terminal): void {
|
||||
const stopOnEntry = getConfig("debugTerminal.stopOnEntry") === true;
|
||||
const query = stopOnEntry ? "break=1" : "wait=1";
|
||||
|
||||
const { adapter, signal } = new TerminalDebugSession();
|
||||
const debugSession = new TerminalDebugSession();
|
||||
await debugSession.initialize();
|
||||
const { adapter, signal } = debugSession;
|
||||
const debug = vscode.window.createTerminal({
|
||||
...creationOptions,
|
||||
name: "JavaScript Debug Terminal",
|
||||
env: {
|
||||
...env,
|
||||
"BUN_INSPECT": `${adapter.url}?${query}`,
|
||||
"BUN_INSPECT_NOTIFY": `${signal.url}`,
|
||||
"BUN_INSPECT_NOTIFY": signal.url,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -153,7 +155,9 @@ class DebugConfigurationProvider implements vscode.DebugConfigurationProvider {
|
||||
}
|
||||
|
||||
class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory {
|
||||
createDebugAdapterDescriptor(session: vscode.DebugSession): vscode.ProviderResult<vscode.DebugAdapterDescriptor> {
|
||||
async createDebugAdapterDescriptor(
|
||||
session: vscode.DebugSession,
|
||||
): Promise<vscode.ProviderResult<vscode.DebugAdapterDescriptor>> {
|
||||
const { configuration } = session;
|
||||
const { request, url } = configuration;
|
||||
|
||||
@@ -166,18 +170,28 @@ class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory
|
||||
}
|
||||
|
||||
const adapter = new FileDebugSession(session.id);
|
||||
await adapter.initialize();
|
||||
return new vscode.DebugAdapterInlineImplementation(adapter);
|
||||
}
|
||||
}
|
||||
|
||||
class FileDebugSession extends DebugSession {
|
||||
readonly adapter: DebugAdapter;
|
||||
adapter: DebugAdapter;
|
||||
sessionId?: string;
|
||||
|
||||
constructor(sessionId?: string) {
|
||||
super();
|
||||
const uniqueId = sessionId ?? Math.random().toString(36).slice(2);
|
||||
const url = `ws+unix://${tmpdir()}/${uniqueId}.sock`;
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
const uniqueId = this.sessionId ?? Math.random().toString(36).slice(2);
|
||||
let url;
|
||||
if (process.platform === "win32") {
|
||||
url = `ws://127.0.0.1:${await getAvailablePort()}/${getRandomId()}`;
|
||||
} else {
|
||||
url = `ws+unix://${tmpdir()}/${uniqueId}.sock`;
|
||||
}
|
||||
this.adapter = new DebugAdapter(url);
|
||||
this.adapter.on("Adapter.response", response => this.sendResponse(response));
|
||||
this.adapter.on("Adapter.event", event => this.sendEvent(event));
|
||||
@@ -204,11 +218,19 @@ class FileDebugSession extends DebugSession {
|
||||
}
|
||||
|
||||
class TerminalDebugSession extends FileDebugSession {
|
||||
readonly signal: UnixSignal;
|
||||
signal: TCPSocketSignal | UnixSignal;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.signal = new UnixSignal();
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
await super.initialize();
|
||||
if (process.platform === "win32") {
|
||||
this.signal = new TCPSocketSignal(await getAvailablePort());
|
||||
} else {
|
||||
this.signal = new UnixSignal();
|
||||
}
|
||||
this.signal.on("Signal.received", () => {
|
||||
vscode.debug.startDebugging(undefined, {
|
||||
...ATTACH_CONFIGURATION,
|
||||
@@ -222,7 +244,7 @@ class TerminalDebugSession extends FileDebugSession {
|
||||
name: "Bun Terminal",
|
||||
env: {
|
||||
"BUN_INSPECT": `${this.adapter.url}?wait=1`,
|
||||
"BUN_INSPECT_NOTIFY": `${this.signal.url}`,
|
||||
"BUN_INSPECT_NOTIFY": this.signal.url,
|
||||
},
|
||||
isTransient: true,
|
||||
iconPath: new vscode.ThemeIcon("debug-console"),
|
||||
|
||||
@@ -111,7 +111,7 @@ class Debugger {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new TypeError(`Unsupported protocol: '${protocol}' (expected 'ws:', 'ws+unix:', or 'wss:')`);
|
||||
throw new TypeError(`Unsupported protocol: '${protocol}' (expected 'ws:' or 'ws+unix:')`);
|
||||
}
|
||||
|
||||
get #websocket(): WebSocketHandler<Connection> {
|
||||
|
||||
Reference in New Issue
Block a user