Added remote debugging via SSH (fix #11)

This commit is contained in:
WebFreak001 2016-02-09 21:51:44 +01:00
commit 0ec1a4e8cf
5 changed files with 382 additions and 87 deletions

View file

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

View file

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

View file

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

View file

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

View file

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