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

@ -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,14 +162,20 @@ 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) {
this.buffer += data.toString("utf8");
if (typeof data == "string")
this.buffer += data;
else
this.buffer += data.toString("utf8");
let end = this.buffer.lastIndexOf('\n');
if (end != -1) {
this.onOutput(this.buffer.substr(0, end));
@ -69,55 +186,65 @@ export class MI2 extends EventEmitter implements IBackend {
onOutput(lines) {
lines = <string[]>lines.split('\n');
lines.forEach(line => {
let parsed = parseMI(line);
//this.log("log", JSON.stringify(parsed));
let handled = false;
if (parsed.token !== undefined) {
if (this.handlers[parsed.token]) {
this.handlers[parsed.token](parsed);
delete this.handlers[parsed.token];
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;
if (parsed.token !== undefined) {
if (this.handlers[parsed.token]) {
this.handlers[parsed.token](parsed);
delete this.handlers[parsed.token];
handled = true;
}
}
if (parsed.resultRecords && parsed.resultRecords.resultClass == "error") {
this.log("log", "An error occured: " + parsed.result("msg"));
}
if (parsed.outOfBandRecord) {
parsed.outOfBandRecord.forEach(record => {
if (record.isStream) {
this.log(record.type, record.content);
} else {
if (record.type == "exec") {
this.emit("exec-async-output", parsed);
if (record.asyncClass == "running")
this.emit("running", parsed);
else if (record.asyncClass == "stopped") {
let reason = parsed.record("reason");
if (reason == "breakpoint-hit")
this.emit("breakpoint", parsed);
else if (reason == "end-stepping-range")
this.emit("step-end", parsed);
else if (reason == "function-finished")
this.emit("step-out-end", parsed);
else if (reason == "signal-received")
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);
}
} else
this.log("log", JSON.stringify(parsed));
}
}
});
handled = true;
}
if (parsed.token == undefined && parsed.resultRecords == undefined && parsed.outOfBandRecord.length == 0)
handled = true;
if (!handled)
this.log("log", "Unhandled: " + JSON.stringify(parsed));
}
if (parsed.resultRecords && parsed.resultRecords.resultClass == "error") {
this.log("log", "An error occured: " + parsed.result("msg"));
}
if (parsed.outOfBandRecord) {
parsed.outOfBandRecord.forEach(record => {
if (record.isStream) {
this.log(record.type, record.content);
} else {
if (record.type == "exec") {
this.emit("exec-async-output", parsed);
if (record.asyncClass == "running")
this.emit("running", parsed);
else if (record.asyncClass == "stopped") {
let reason = parsed.record("reason");
if (reason == "breakpoint-hit")
this.emit("breakpoint", parsed);
else if (reason == "end-stepping-range")
this.emit("step-end", parsed);
else if (reason == "function-finished")
this.emit("step-out-end", parsed);
else if (reason == "signal-received")
this.emit("signal-stop", parsed);
else if (reason == "exited-normally")
this.emit("exited-normally", parsed);
else {
this.log("console", "Not implemented stop reason (assuming exception): " + reason);
this.emit("stopped", parsed);
}
} else
this.log("log", JSON.stringify(parsed));
}
}
});
handled = true;
}
if (parsed.token == undefined && parsed.resultRecords == undefined && parsed.outOfBandRecord.length == 0)
handled = true;
if (!handled)
this.log("log", "Unhandled: " + JSON.stringify(parsed));
});
}
@ -125,20 +252,35 @@ 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() {
let proc = this.process;
let to = setTimeout(() => {
process.kill(-proc.pid);
}, 1000);
this.process.on("exit", function(code) {
clearTimeout(to);
});
this.sendRaw("-gdb-exit");
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);
}, 1000);
this.process.on("exit", function(code) {
clearTimeout(to);
});
this.sendRaw("-gdb-exit");
}
}
detach() {
@ -331,7 +473,10 @@ export class MI2 extends EventEmitter implements IBackend {
}
sendRaw(raw: string) {
this.process.stdin.write(raw + "\n");
if (this.isSSH)
this.stream.write(raw + "\n");
else
this.process.stdin.write(raw + "\n");
}
sendCommand(command: string): Thenable<MINode> {
@ -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,21 +87,49 @@ class MI2DebugSession extends DebugSession {
this.quit = false;
this.attached = false;
this.needContinue = false;
this.gdbDebugger.load(args.cwd, args.target).then(() => {
if (args.autorun)
args.autorun.forEach(command => {
this.gdbDebugger.sendUserInput(command);
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);
});
this.gdbDebugger.start().then(() => {
this.sendResponse(response);
});
});
}
else {
this.gdbDebugger.load(args.cwd, args.target).then(() => {
if (args.autorun)
args.autorun.forEach(command => {
this.gdbDebugger.sendUserInput(command);
});
this.gdbDebugger.start().then(() => {
this.sendResponse(response);
});
});
}
}
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,17 +159,24 @@ class MI2DebugSession extends DebugSession {
}
protected setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments): void {
this.gdbDebugger.clearBreakPoints().then(() => {
let path = args.source.path;
let all = [];
args.breakpoints.forEach(brk => {
all.push(this.gdbDebugger.addBreakPoint({ file: path, line: brk.line, condition: brk.condition }));
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 }));
});
Promise.all(all).then(brkpoints => {
response.body.breakpoints = brkpoints;
this.sendResponse(response);
});
});
Promise.all(all).then(brkpoints => {
response.body.breakpoints = brkpoints;
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