fix for windows debug support (#14048)

Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
This commit is contained in:
snwy
2024-09-21 00:20:33 -07:00
committed by GitHub
parent 3fc092d23f
commit 722e3fa481
10 changed files with 241 additions and 65 deletions

View File

@@ -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;
}

View File

@@ -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.

View File

@@ -1,8 +0,0 @@
console.log("HELLO");
console.log("HELLO 2");
console.log("HELLO 3");
a();
function a() {
console.log("HELLO 4");
}

View File

@@ -0,0 +1,9 @@
type OS = "Windows";
Bun.serve({
fetch(req: Request) {
return new Response(
`Hello, ${"Windows" as OS}!`
);
}
});

View File

@@ -1,6 +1,6 @@
{
"name": "bun-vscode",
"version": "0.0.8",
"version": "0.0.13",
"author": "oven",
"repository": {
"type": "git",

View File

@@ -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",
});

View File

@@ -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",
});

View File

@@ -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"),

View File

@@ -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> {