Added remote debugging via SSH (fix #11)
This commit is contained in:
parent
d6c55d4aa8
commit
0ec1a4e8cf
5 changed files with 382 additions and 87 deletions
29
README.md
29
README.md
|
|
@ -66,4 +66,33 @@ port and optionally hostname in `target`.
|
|||
This will attach to the running process managed by gdbserver on localhost:2345. You might
|
||||
need to hit the start button in the debug bar at the top first to start the program.
|
||||
|
||||
### Using ssh for remote debugging
|
||||
|
||||
Debugging using ssh automatically converts all paths between client & server and also optionally
|
||||
redirects X11 output from the server to the client. Simply add a `ssh` object in your `launch`
|
||||
request.
|
||||
|
||||
```
|
||||
"request": "launch",
|
||||
"target": "./executable",
|
||||
"cwd": "${workspaceRoot}",
|
||||
"ssh": {
|
||||
"forwardX11": true,
|
||||
"host": "192.168.178.57",
|
||||
"cwd": "/home/remoteUser/project/",
|
||||
"keyfile": "/path/to/.ssh/key", // OR
|
||||
"password": "password123",
|
||||
"user": "remoteUser",
|
||||
"x11host": "localhost",
|
||||
"x11port": 6000
|
||||
}
|
||||
```
|
||||
|
||||
`cwd` will be used to trim off local paths and `ssh.cwd` will map them to the server. This is
|
||||
required for basically everything except watched variables or user commands to work.
|
||||
|
||||
For X11 forwarding to work you first need to enable it in your Display Manager and allow the
|
||||
connections. To allow connections you can either add an entry for applications or run `xhost +`
|
||||
in the console while you are debugging and turn it off again when you are done using `xhost -`.
|
||||
|
||||
## [Issues](https://github.com/WebFreak001/code-debug)
|
||||
62
package.json
62
package.json
|
|
@ -52,6 +52,62 @@
|
|||
"type": "array",
|
||||
"description": "GDB commands to run when starting to debug",
|
||||
"default": []
|
||||
},
|
||||
"ssh": {
|
||||
"required": [
|
||||
"host",
|
||||
"cwd",
|
||||
"user"
|
||||
],
|
||||
"type": "object",
|
||||
"description": "If this is set then the extension will connect to an ssh host and run GDB there",
|
||||
"properties": {
|
||||
"host": {
|
||||
"type": "string",
|
||||
"description": "Remote host name/ip to connect to"
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string",
|
||||
"description": "Path of project on the remote"
|
||||
},
|
||||
"port": {
|
||||
"type": "number",
|
||||
"description": "Remote port number",
|
||||
"default": 22
|
||||
},
|
||||
"user": {
|
||||
"type": "string",
|
||||
"description": "Username to connect as"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"description": "Plain text password (unsafe; if possible use keyfile instead)"
|
||||
},
|
||||
"keyfile": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to private key"
|
||||
},
|
||||
"forwardX11": {
|
||||
"type": "boolean",
|
||||
"description": "If true, the server will redirect x11 to the local host",
|
||||
"default": true
|
||||
},
|
||||
"x11port": {
|
||||
"type": "number",
|
||||
"description": "Port to redirect X11 data to (by default port = display + 6000)",
|
||||
"default": 6000
|
||||
},
|
||||
"x11host": {
|
||||
"type": "string",
|
||||
"description": "Hostname/ip to redirect X11 data to",
|
||||
"default": "localhost"
|
||||
},
|
||||
"remotex11screen": {
|
||||
"type": "number",
|
||||
"description": "Screen to start the application on the remote side",
|
||||
"default": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -75,7 +131,8 @@
|
|||
},
|
||||
"cwd": {
|
||||
"type": "string",
|
||||
"description": "Path of project"
|
||||
"description": "Path of project",
|
||||
"default": "${workspaceRoot}"
|
||||
},
|
||||
"autorun": {
|
||||
"type": "array",
|
||||
|
|
@ -103,7 +160,8 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"vscode-debugadapter": "^1.5.0",
|
||||
"vscode-debugprotocol": "^1.5.0"
|
||||
"vscode-debugprotocol": "^1.5.0",
|
||||
"ssh2": "^0.4.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^1.7.5",
|
||||
|
|
|
|||
|
|
@ -13,8 +13,22 @@ export interface Stack {
|
|||
line: number;
|
||||
}
|
||||
|
||||
export interface SSHArguments {
|
||||
forwardX11: boolean;
|
||||
host: string;
|
||||
keyfile: string;
|
||||
password: string;
|
||||
cwd: string;
|
||||
port: number;
|
||||
user: string;
|
||||
remotex11screen: number;
|
||||
x11port: number;
|
||||
x11host: string;
|
||||
}
|
||||
|
||||
export interface IBackend {
|
||||
load(cwd: string, target: string): Thenable<any>;
|
||||
ssh(args: SSHArguments, cwd: string, target: string): Thenable<any>;
|
||||
attach(cwd: string, executable: string, target: string): Thenable<any>;
|
||||
connect(cwd: string, executable: string, target: string): Thenable<any>;
|
||||
start(): Thenable<boolean>;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,23 @@
|
|||
import { Breakpoint, IBackend, Stack } from "../backend.ts"
|
||||
import { Breakpoint, IBackend, Stack, SSHArguments } from "../backend.ts"
|
||||
import * as ChildProcess from "child_process"
|
||||
import { EventEmitter } from "events"
|
||||
import { parseMI, MINode } from '../mi_parse';
|
||||
import * as net from "net"
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
var Client = require("ssh2").Client;
|
||||
|
||||
function escape(str: string) {
|
||||
return str.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
||||
}
|
||||
|
||||
let nonOutput = /^[0-9]*[\*\+\=]|[\~\@\&\^]/;
|
||||
|
||||
function couldBeOutput(line: string) {
|
||||
if (nonOutput.exec(line))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export class MI2 extends EventEmitter implements IBackend {
|
||||
constructor(public application: string, public preargs: string[]) {
|
||||
|
|
@ -9,15 +25,107 @@ export class MI2 extends EventEmitter implements IBackend {
|
|||
}
|
||||
|
||||
load(cwd: string, target: string): Thenable<any> {
|
||||
if (!path.isAbsolute(target))
|
||||
target = path.join(cwd, target);
|
||||
return new Promise((resolve, reject) => {
|
||||
this.isSSH = false;
|
||||
this.process = ChildProcess.spawn(this.application, this.preargs.concat([target]), { cwd: cwd });
|
||||
this.process.stdout.on("data", this.stdout.bind(this));
|
||||
this.process.stderr.on("data", this.stdout.bind(this));
|
||||
this.process.on("exit", (() => { this.emit("quit"); }).bind(this));
|
||||
Promise.all([
|
||||
this.sendCommand("gdb-set target-async on"),
|
||||
this.sendCommand("environment-directory \"" + cwd.replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + "\"")
|
||||
]).then(resolve, reject);
|
||||
this.sendCommand("environment-directory \"" + escape(cwd) + "\"")
|
||||
]).then(() => {
|
||||
this.emit("debug-ready")
|
||||
resolve();
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
|
||||
ssh(args: SSHArguments, cwd: string, target: string): Thenable<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!path.isAbsolute(target))
|
||||
target = path.join(cwd, target);
|
||||
|
||||
this.isSSH = true;
|
||||
this.sshReady = false;
|
||||
this.sshConn = new Client();
|
||||
|
||||
if (args.forwardX11) {
|
||||
this.sshConn.on("x11", (info, accept, reject) => {
|
||||
var xserversock = new net.Socket();
|
||||
xserversock.on("error", (err) => {
|
||||
this.log("stderr", "Could not connect to local X11 server! Did you enable it in your display manager?\n" + err);
|
||||
});
|
||||
xserversock.on("connect", () => {
|
||||
let xclientsock = accept();
|
||||
xclientsock.pipe(xserversock).pipe(xclientsock);
|
||||
});
|
||||
xserversock.connect(args.x11port, args.x11host);
|
||||
});
|
||||
}
|
||||
|
||||
let connectionArgs: any = {
|
||||
host: args.host,
|
||||
port: args.port,
|
||||
username: args.user
|
||||
};
|
||||
|
||||
if (args.keyfile) {
|
||||
if (require("fs").existsSync(args.keyfile))
|
||||
connectionArgs.privateKey = require("fs").readFileSync(args.keyfile);
|
||||
else {
|
||||
this.log("stderr", "SSH key file does not exist!");
|
||||
this.emit("quit");
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
connectionArgs.password = args.password;
|
||||
}
|
||||
|
||||
this.sshConn.on("ready", () => {
|
||||
this.log("stdout", "Running " + this.application + " over ssh...");
|
||||
let execArgs: any = {};
|
||||
if (args.forwardX11) {
|
||||
execArgs.x11 = {
|
||||
single: false,
|
||||
screen: args.remotex11screen
|
||||
};
|
||||
}
|
||||
this.sshConn.exec(this.application + " " + this.preargs.join(" "), execArgs, (err, stream) => {
|
||||
if (err) {
|
||||
this.log("stderr", "Could not run " + this.application + " over ssh!");
|
||||
this.log("stderr", err.toString());
|
||||
this.emit("quit");
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
this.sshReady = true;
|
||||
this.stream = stream;
|
||||
stream.on("data", this.stdout.bind(this));
|
||||
stream.stderr.on("data", this.stdout.bind(this));
|
||||
stream.on("exit", (() => {
|
||||
this.emit("quit");
|
||||
this.sshConn.end();
|
||||
}).bind(this));
|
||||
Promise.all([
|
||||
this.sendCommand("gdb-set target-async on"),
|
||||
this.sendCommand("environment-directory \"" + escape(cwd) + "\""),
|
||||
this.sendCommand("environment-cd \"" + escape(cwd) + "\""),
|
||||
this.sendCommand("file-exec-and-symbols \"" + escape(target) + "\"")
|
||||
]).then(() => {
|
||||
this.emit("debug-ready")
|
||||
resolve();
|
||||
}, reject);
|
||||
});
|
||||
}).on("error", (err) => {
|
||||
this.log("stderr", "Could not run " + this.application + " over ssh!");
|
||||
this.log("stderr", err.toString());
|
||||
this.emit("quit");
|
||||
reject();
|
||||
}).connect(connectionArgs);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -33,8 +141,11 @@ export class MI2 extends EventEmitter implements IBackend {
|
|||
this.process.on("exit", (() => { this.emit("quit"); }).bind(this));
|
||||
Promise.all([
|
||||
this.sendCommand("gdb-set target-async on"),
|
||||
this.sendCommand("environment-directory \"" + cwd.replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + "\"")
|
||||
]).then(resolve, reject);
|
||||
this.sendCommand("environment-directory \"" + escape(cwd) + "\"")
|
||||
]).then(() => {
|
||||
this.emit("debug-ready")
|
||||
resolve();
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -51,13 +162,19 @@ export class MI2 extends EventEmitter implements IBackend {
|
|||
this.process.on("exit", (() => { this.emit("quit"); }).bind(this));
|
||||
Promise.all([
|
||||
this.sendCommand("gdb-set target-async on"),
|
||||
this.sendCommand("environment-directory \"" + cwd.replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + "\""),
|
||||
this.sendCommand("environment-directory \"" + escape(cwd) + "\""),
|
||||
this.sendCommand("target-select remote " + target)
|
||||
]).then(resolve, reject);
|
||||
]).then(() => {
|
||||
this.emit("debug-ready")
|
||||
resolve();
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
|
||||
stdout(data) {
|
||||
if (typeof data == "string")
|
||||
this.buffer += data;
|
||||
else
|
||||
this.buffer += data.toString("utf8");
|
||||
let end = this.buffer.lastIndexOf('\n');
|
||||
if (end != -1) {
|
||||
|
|
@ -69,6 +186,11 @@ export class MI2 extends EventEmitter implements IBackend {
|
|||
onOutput(lines) {
|
||||
lines = <string[]>lines.split('\n');
|
||||
lines.forEach(line => {
|
||||
if (couldBeOutput(line)) {
|
||||
if (line.trim() != "(gdb)")
|
||||
this.log("stdout", line);
|
||||
}
|
||||
else {
|
||||
let parsed = parseMI(line);
|
||||
//this.log("log", JSON.stringify(parsed));
|
||||
let handled = false;
|
||||
|
|
@ -103,6 +225,10 @@ export class MI2 extends EventEmitter implements IBackend {
|
|||
this.emit("signal-stop", parsed);
|
||||
else if (reason == "exited-normally")
|
||||
this.emit("exited-normally", parsed);
|
||||
else if (reason == "exited") { // exit with error code != 0
|
||||
this.log("stderr", "Program exited with code " + parsed.record("exit-code"));
|
||||
this.emit("exited-normally", parsed);
|
||||
}
|
||||
else {
|
||||
this.log("console", "Not implemented stop reason (assuming exception): " + reason);
|
||||
this.emit("stopped", parsed);
|
||||
|
|
@ -118,6 +244,7 @@ export class MI2 extends EventEmitter implements IBackend {
|
|||
handled = true;
|
||||
if (!handled)
|
||||
this.log("log", "Unhandled: " + JSON.stringify(parsed));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -125,12 +252,26 @@ export class MI2 extends EventEmitter implements IBackend {
|
|||
return new Promise((resolve, reject) => {
|
||||
this.log("console", "Running executable");
|
||||
this.sendCommand("exec-run").then((info) => {
|
||||
resolve(info.resultRecords.resultClass == "running");
|
||||
if (info.resultRecords.resultClass == "running")
|
||||
resolve();
|
||||
else
|
||||
reject();
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.isSSH) {
|
||||
let proc = this.stream;
|
||||
let to = setTimeout(() => {
|
||||
proc.signal("KILL");
|
||||
}, 1000);
|
||||
this.stream.on("exit", function(code) {
|
||||
clearTimeout(to);
|
||||
})
|
||||
this.sendRaw("-gdb-exit");
|
||||
}
|
||||
else {
|
||||
let proc = this.process;
|
||||
let to = setTimeout(() => {
|
||||
process.kill(-proc.pid);
|
||||
|
|
@ -140,6 +281,7 @@ export class MI2 extends EventEmitter implements IBackend {
|
|||
});
|
||||
this.sendRaw("-gdb-exit");
|
||||
}
|
||||
}
|
||||
|
||||
detach() {
|
||||
let proc = this.process;
|
||||
|
|
@ -331,6 +473,9 @@ export class MI2 extends EventEmitter implements IBackend {
|
|||
}
|
||||
|
||||
sendRaw(raw: string) {
|
||||
if (this.isSSH)
|
||||
this.stream.write(raw + "\n");
|
||||
else
|
||||
this.process.stdin.write(raw + "\n");
|
||||
}
|
||||
|
||||
|
|
@ -345,18 +490,22 @@ export class MI2 extends EventEmitter implements IBackend {
|
|||
else
|
||||
resolve(node);
|
||||
};
|
||||
this.process.stdin.write(this.currentToken + "-" + command + "\n");
|
||||
this.sendRaw(this.currentToken + "-" + command);
|
||||
this.currentToken++;
|
||||
});
|
||||
}
|
||||
|
||||
isReady(): boolean {
|
||||
return !!this.process;
|
||||
return this.isSSH ? this.sshReady : !!this.process;
|
||||
}
|
||||
|
||||
private isSSH: boolean;
|
||||
private sshReady: boolean;
|
||||
private currentToken: number = 1;
|
||||
private handlers: { [index: number]: (info: MINode) => any } = {};
|
||||
private breakpoints: Map<Breakpoint, Number> = new Map();
|
||||
private buffer: string;
|
||||
private process: ChildProcess.ChildProcess;
|
||||
private stream;
|
||||
private sshConn;
|
||||
}
|
||||
49
src/gdb.ts
49
src/gdb.ts
|
|
@ -1,14 +1,16 @@
|
|||
import { DebugSession, InitializedEvent, TerminatedEvent, StoppedEvent, OutputEvent, Thread, StackFrame, Scope, Source, Handles } from 'vscode-debugadapter';
|
||||
import { DebugProtocol } from 'vscode-debugprotocol';
|
||||
import { Breakpoint, IBackend } from './backend/backend'
|
||||
import { Breakpoint, IBackend, SSHArguments } from './backend/backend'
|
||||
import { MINode } from './backend/mi_parse'
|
||||
import { expandValue, isExpandable } from './backend/gdb_expansion'
|
||||
import { MI2 } from './backend/mi2/mi2'
|
||||
import { relative, resolve } from "path"
|
||||
|
||||
export interface LaunchRequestArguments {
|
||||
cwd: string;
|
||||
target: string;
|
||||
autorun: string[];
|
||||
ssh: SSHArguments;
|
||||
}
|
||||
|
||||
export interface AttachRequestArguments {
|
||||
|
|
@ -26,6 +28,9 @@ class MI2DebugSession extends DebugSession {
|
|||
private quit: boolean;
|
||||
private attached: boolean;
|
||||
private needContinue: boolean;
|
||||
private isSSH: boolean;
|
||||
private trimCWD: string;
|
||||
private switchCWD: string;
|
||||
|
||||
public constructor(debuggerLinesStartAt1: boolean, isServer: boolean = false) {
|
||||
super(debuggerLinesStartAt1, isServer);
|
||||
|
|
@ -82,6 +87,32 @@ class MI2DebugSession extends DebugSession {
|
|||
this.quit = false;
|
||||
this.attached = false;
|
||||
this.needContinue = false;
|
||||
this.isSSH = false;
|
||||
if (args.ssh !== undefined) {
|
||||
if (args.ssh.forwardX11 === undefined)
|
||||
args.ssh.forwardX11 = true;
|
||||
if (args.ssh.port === undefined)
|
||||
args.ssh.port = 22;
|
||||
if (args.ssh.x11port === undefined)
|
||||
args.ssh.x11port = 6000;
|
||||
if (args.ssh.x11host === undefined)
|
||||
args.ssh.x11host = "localhost";
|
||||
if (args.ssh.remotex11screen === undefined)
|
||||
args.ssh.remotex11screen = 0;
|
||||
this.isSSH = true;
|
||||
this.trimCWD = args.cwd;
|
||||
this.switchCWD = args.ssh.cwd;
|
||||
this.gdbDebugger.ssh(args.ssh, args.ssh.cwd, args.target).then(() => {
|
||||
if (args.autorun)
|
||||
args.autorun.forEach(command => {
|
||||
this.gdbDebugger.sendUserInput(command);
|
||||
});
|
||||
this.gdbDebugger.start().then(() => {
|
||||
this.sendResponse(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.gdbDebugger.load(args.cwd, args.target).then(() => {
|
||||
if (args.autorun)
|
||||
args.autorun.forEach(command => {
|
||||
|
|
@ -92,11 +123,13 @@ class MI2DebugSession extends DebugSession {
|
|||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments): void {
|
||||
this.quit = false;
|
||||
this.attached = !args.remote;
|
||||
this.needContinue = true;
|
||||
this.isSSH = false;
|
||||
if (args.remote) {
|
||||
this.gdbDebugger.connect(args.cwd, args.executable, args.target).then(() => {
|
||||
if (args.autorun)
|
||||
|
|
@ -126,8 +159,14 @@ class MI2DebugSession extends DebugSession {
|
|||
}
|
||||
|
||||
protected setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments): void {
|
||||
this.gdbDebugger.once("debug-ready", (() => {
|
||||
this.gdbDebugger.clearBreakPoints().then(() => {
|
||||
let path = args.source.path;
|
||||
if (this.isSSH) {
|
||||
path = relative(this.trimCWD, path);
|
||||
path = resolve(this.switchCWD, path);
|
||||
}
|
||||
this.handleMsg("console", "Breakpoints for file " + path + "\n");
|
||||
let all = [];
|
||||
args.breakpoints.forEach(brk => {
|
||||
all.push(this.gdbDebugger.addBreakPoint({ file: path, line: brk.line, condition: brk.condition }));
|
||||
|
|
@ -137,6 +176,7 @@ class MI2DebugSession extends DebugSession {
|
|||
this.sendResponse(response);
|
||||
});
|
||||
});
|
||||
}).bind(this));
|
||||
}
|
||||
|
||||
protected threadsRequest(response: DebugProtocol.ThreadsResponse): void {
|
||||
|
|
@ -152,7 +192,12 @@ class MI2DebugSession extends DebugSession {
|
|||
this.gdbDebugger.getStack(args.levels).then(stack => {
|
||||
let ret: StackFrame[] = [];
|
||||
stack.forEach(element => {
|
||||
ret.push(new StackFrame(element.level, element.function + "@" + element.address, new Source(element.fileName, element.file), element.line, 0));
|
||||
let file = element.file;
|
||||
if (this.isSSH) {
|
||||
file = relative(this.switchCWD, file);
|
||||
file = resolve(this.trimCWD, file);
|
||||
}
|
||||
ret.push(new StackFrame(element.level, element.function + "@" + element.address, new Source(element.fileName, file), element.line, 0));
|
||||
});
|
||||
response.body = {
|
||||
stackFrames: ret
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue