Skip to content

Commit

Permalink
fix for windows debug support (#14048)
Browse files Browse the repository at this point in the history
Co-authored-by: Jarred Sumner <[email protected]>
  • Loading branch information
snoglobe and Jarred-Sumner authored Sep 21, 2024
1 parent 3fc092d commit 722e3fa
Show file tree
Hide file tree
Showing 10 changed files with 241 additions and 65 deletions.
139 changes: 106 additions & 33 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,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,
Expand Down Expand Up @@ -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";

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 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());

const started = await this.#spawn({
command: runtime,
args: processArgs,
env: processEnv,
cwd,
isDebugee: true,
});
signal.on("Signal.received", async () => {
this.#attach({ url });
});

if (!started) {
throw new Error("Program could not be started.");
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.");
}
}
}

Expand Down Expand Up @@ -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", {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 = []));
Expand Down Expand Up @@ -2107,7 +2166,6 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements

close(): void {
this.#process?.kill();
// this.#signal?.close();
this.#inspector.close();
this.#reset();
}
Expand Down Expand Up @@ -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";
Expand All @@ -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`;
Expand Down Expand Up @@ -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;
}
74 changes: 73 additions & 1 deletion packages/bun-debug-adapter-protocol/src/debugger/signal.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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 modified packages/bun-vscode/bun.lockb
Binary file not shown.
8 changes: 0 additions & 8 deletions packages/bun-vscode/example/hello.js

This file was deleted.

9 changes: 9 additions & 0 deletions packages/bun-vscode/example/hello.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
type OS = "Windows";

Bun.serve({
fetch(req: Request) {
return new Response(
`Hello, ${"Windows" as OS}!`
);
}
});
2 changes: 1 addition & 1 deletion packages/bun-vscode/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bun-vscode",
"version": "0.0.8",
"version": "0.0.13",
"author": "oven",
"repository": {
"type": "git",
Expand Down
10 changes: 7 additions & 3 deletions packages/bun-vscode/scripts/build.mjs
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -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",
});
16 changes: 10 additions & 6 deletions packages/bun-vscode/scripts/test.mjs
Original file line number Diff line number Diff line change
@@ -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",
});
Loading

0 comments on commit 722e3fa

Please sign in to comment.