-
Notifications
You must be signed in to change notification settings - Fork 52
Add debugger launch test #989
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,37 @@ | ||
import path from "path"; | ||
import fs from "fs"; | ||
import { ChildProcessWithoutNullStreams, spawn, execSync } from "child_process"; | ||
import net from "net"; | ||
import os from "os"; | ||
import { ChildProcessWithoutNullStreams, spawn } from "child_process"; | ||
|
||
import * as vscode from "vscode"; | ||
|
||
import { LOG_CHANNEL } from "./common"; | ||
import { LOG_CHANNEL, asyncExec } from "./common"; | ||
import { Workspace } from "./workspace"; | ||
|
||
class TerminalLogger { | ||
append(message: string) { | ||
// eslint-disable-next-line no-console | ||
console.log(message); | ||
} | ||
|
||
appendLine(value: string): void { | ||
// eslint-disable-next-line no-console | ||
console.log(value); | ||
} | ||
} | ||
|
||
export class Debugger | ||
implements | ||
vscode.DebugAdapterDescriptorFactory, | ||
vscode.DebugConfigurationProvider | ||
{ | ||
private debugProcess?: ChildProcessWithoutNullStreams; | ||
private readonly console = vscode.debug.activeDebugConsole; | ||
// eslint-disable-next-line no-process-env | ||
private readonly console = process.env.CI | ||
? new TerminalLogger() | ||
: vscode.debug.activeDebugConsole; | ||
|
||
private readonly workspaceResolver: ( | ||
uri: vscode.Uri | undefined, | ||
) => Workspace | undefined; | ||
|
@@ -132,19 +150,17 @@ export class Debugger | |
} | ||
} | ||
|
||
private getSockets(session: vscode.DebugSession): string[] { | ||
const cmd = "bundle exec rdbg --util=list-socks"; | ||
const workspaceFolder = session.workspaceFolder; | ||
if (!workspaceFolder) { | ||
throw new Error("Debugging requires a workspace folder to be opened"); | ||
} | ||
private async getSockets(session: vscode.DebugSession) { | ||
const configuration = session.configuration; | ||
let sockets: string[] = []; | ||
|
||
try { | ||
sockets = execSync(cmd, { | ||
cwd: workspaceFolder.uri.fsPath, | ||
const result = await asyncExec("bundle exec rdbg --util=list-socks", { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just switching to |
||
cwd: session.workspaceFolder?.uri.fsPath, | ||
env: configuration.env, | ||
}) | ||
}); | ||
|
||
sockets = result.stdout | ||
.toString() | ||
.split("\n") | ||
.filter((socket) => socket.length > 0); | ||
|
@@ -159,7 +175,8 @@ export class Debugger | |
): Promise<vscode.DebugAdapterDescriptor> { | ||
// When using attach, a process will be launched using Ruby debug and it will create a socket automatically. We have | ||
// to find the available sockets and ask the user which one they want to attach to | ||
const sockets = this.getSockets(session); | ||
const sockets = await this.getSockets(session); | ||
|
||
if (sockets.length === 0) { | ||
throw new Error(`No debuggee processes found. Is the process running?`); | ||
} | ||
|
@@ -183,7 +200,7 @@ export class Debugger | |
return new vscode.DebugAdapterNamedPipeServer(selectedSocketPath); | ||
} | ||
|
||
private spawnDebuggeeForLaunch( | ||
private async spawnDebuggeeForLaunch( | ||
session: vscode.DebugSession, | ||
): Promise<vscode.DebugAdapterDescriptor | undefined> { | ||
let initialMessage = ""; | ||
|
@@ -192,16 +209,18 @@ export class Debugger | |
const configuration = session.configuration; | ||
const workspaceFolder = configuration.targetFolder; | ||
const cwd = workspaceFolder.path; | ||
const port = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using UNIX sockets doesn't work on Windows, but we can easily spawn the debugger with a port. |
||
os.platform() === "win32" ? await this.availablePort() : undefined; | ||
|
||
return new Promise((resolve, reject) => { | ||
const args = [ | ||
"exec", | ||
"rdbg", | ||
"--open", | ||
"--command", | ||
"--", | ||
configuration.program, | ||
]; | ||
const args = ["exec", "rdbg"]; | ||
|
||
// On Windows, we spawn the debugger with any available port. On Linux and macOS, we spawn it with a UNIX socket | ||
if (port) { | ||
args.push("--port", port.toString()); | ||
} | ||
|
||
args.push("--open", "--command", "--", configuration.program); | ||
|
||
LOG_CHANNEL.info(`Spawning debugger in directory ${cwd}`); | ||
LOG_CHANNEL.info(` Command bundle ${args.join(" ")}`); | ||
|
@@ -228,10 +247,14 @@ export class Debugger | |
initialMessage.includes("DEBUGGER: wait for debugger connection...") | ||
) { | ||
initialized = true; | ||
|
||
const regex = | ||
/DEBUGGER: Debugger can attach via UNIX domain socket \((.*)\)/; | ||
const sockPath = RegExp(regex).exec(initialMessage); | ||
if (sockPath && sockPath.length === 2) { | ||
|
||
if (port) { | ||
resolve(new vscode.DebugAdapterServer(port)); | ||
} else if (sockPath && sockPath.length === 2) { | ||
resolve(new vscode.DebugAdapterNamedPipeServer(sockPath[1])); | ||
} else { | ||
reject(new Error("Debugger not found on UNIX socket")); | ||
|
@@ -253,7 +276,7 @@ export class Debugger | |
// If the Ruby debug exits with an exit code > 1, then an error might've occurred. The reason we don't use only | ||
// code zero here is because debug actually exits with 1 if the user cancels the debug session, which is not | ||
// actually an error | ||
this.debugProcess.on("exit", (code) => { | ||
this.debugProcess.on("close", (code) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The But if we use the |
||
if (code) { | ||
const message = `Debugger exited with status ${code}. Check the output channel for more information.`; | ||
this.console.append(message); | ||
|
@@ -263,4 +286,26 @@ export class Debugger | |
}); | ||
}); | ||
} | ||
|
||
// Find an available port for the debug server to listen on | ||
private async availablePort(): Promise<number | undefined> { | ||
return new Promise((resolve, reject) => { | ||
const server = net.createServer(); | ||
server.unref(); | ||
|
||
server.on("error", reject); | ||
|
||
// By listening on port 0, the system will assign an available port automatically. We close the server and return | ||
// the port that was assigned | ||
server.listen(0, () => { | ||
const address = server.address(); | ||
const port = | ||
typeof address === "string" ? Number(address) : address?.port; | ||
|
||
server.close(() => { | ||
resolve(port); | ||
}); | ||
}); | ||
}); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,8 +6,10 @@ import * as os from "os"; | |
import * as vscode from "vscode"; | ||
|
||
import { Debugger } from "../../debugger"; | ||
import { Ruby } from "../../ruby"; | ||
import { Ruby, VersionManager } from "../../ruby"; | ||
import { Workspace } from "../../workspace"; | ||
import { WorkspaceChannel } from "../../workspaceChannel"; | ||
import { LOG_CHANNEL, asyncExec } from "../../common"; | ||
|
||
suite("Debugger", () => { | ||
test("Provide debug configurations returns the default configs", () => { | ||
|
@@ -134,4 +136,85 @@ suite("Debugger", () => { | |
context.subscriptions.forEach((subscription) => subscription.dispose()); | ||
fs.rmSync(tmpPath, { recursive: true, force: true }); | ||
}); | ||
|
||
test("Launching the debugger", async () => { | ||
// eslint-disable-next-line no-process-env | ||
if (process.env.CI) { | ||
await vscode.workspace | ||
.getConfiguration("rubyLsp") | ||
.update("rubyVersionManager", VersionManager.None, true, true); | ||
} | ||
|
||
// By default, VS Code always saves all open files when launching a debugging session. This is a problem for tests | ||
// because it attempts to save an untitled test file and then we get stuck in the save file dialog with no way of | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Which file is the untitled one here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a new untitled file that gets created when we run tests. |
||
// closing it. We have to disable that before running this test | ||
const currentSaveBeforeStart = await vscode.workspace | ||
.getConfiguration("debug") | ||
.get("saveBeforeStart"); | ||
await vscode.workspace | ||
.getConfiguration("debug") | ||
.update("saveBeforeStart", "none", true, true); | ||
|
||
const tmpPath = fs.mkdtempSync( | ||
path.join(os.tmpdir(), "ruby-lsp-test-debugger"), | ||
); | ||
fs.writeFileSync(path.join(tmpPath, "test.rb"), "1 + 1"); | ||
fs.writeFileSync(path.join(tmpPath, ".ruby-version"), "3.3.0"); | ||
fs.writeFileSync( | ||
path.join(tmpPath, "Gemfile"), | ||
'source "https://rubygems.org"\ngem "debug"', | ||
); | ||
|
||
const context = { subscriptions: [] } as unknown as vscode.ExtensionContext; | ||
const outputChannel = new WorkspaceChannel("fake", LOG_CHANNEL); | ||
const workspaceFolder: vscode.WorkspaceFolder = { | ||
uri: vscode.Uri.from({ scheme: "file", path: tmpPath }), | ||
name: path.basename(tmpPath), | ||
index: 0, | ||
}; | ||
const ruby = new Ruby(context, workspaceFolder, outputChannel); | ||
await ruby.activateRuby(); | ||
|
||
try { | ||
await asyncExec("gem install debug", { env: ruby.env, cwd: tmpPath }); | ||
await asyncExec("bundle install", { env: ruby.env, cwd: tmpPath }); | ||
} catch (error: any) { | ||
assert.fail(`Failed to bundle install: ${error.message}`); | ||
} | ||
|
||
assert.ok(fs.existsSync(path.join(tmpPath, "Gemfile.lock"))); | ||
assert.match( | ||
fs.readFileSync(path.join(tmpPath, "Gemfile.lock")).toString(), | ||
/debug/, | ||
); | ||
|
||
const debug = new Debugger(context, () => { | ||
return { | ||
ruby, | ||
workspaceFolder, | ||
} as Workspace; | ||
}); | ||
|
||
try { | ||
await vscode.debug.startDebugging(workspaceFolder, { | ||
type: "ruby_lsp", | ||
name: "Debug", | ||
request: "launch", | ||
program: `ruby ${path.join(tmpPath, "test.rb")}`, | ||
}); | ||
} catch (error: any) { | ||
assert.fail(`Failed to launch debugger: ${error.message}`); | ||
} | ||
|
||
// The debugger might take a bit of time to disconnect from the editor. We need to perform cleanup when we receive | ||
// the termination callback or else we try to dispose of the debugger client too early | ||
vscode.debug.onDidTerminateDebugSession(async (_session) => { | ||
debug.dispose(); | ||
context.subscriptions.forEach((subscription) => subscription.dispose()); | ||
fs.rmSync(tmpPath, { recursive: true, force: true }); | ||
await vscode.workspace | ||
.getConfiguration("debug") | ||
.update("saveBeforeStart", currentSaveBeforeStart, true, true); | ||
}); | ||
}).timeout(45000); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This terminal logger is so that we can see logs from the debug server when a test fails