Skip to content
This repository has been archived by the owner on Jan 13, 2021. It is now read-only.

Commit

Permalink
feat(remote): initial support for remote debugging
Browse files Browse the repository at this point in the history
Adding support for remote debugging

```bash
  # Start remote debugger in vs code on port 5000 then:
$ PERLDB_OPTS="RemotePort=localhost:5000" perl -d test.pl
```
  • Loading branch information
Morten Henriksen committed Feb 9, 2018
1 parent a826b16 commit 6b1819e
Show file tree
Hide file tree
Showing 8 changed files with 207 additions and 33 deletions.
14 changes: 13 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@
"type": "boolean",
"description": "Enable logging for the Debug Adapter Protocol",
"default": false
},
"port": {
"type": "number",
"description": "Port to listen for remote debuggers.",
"default": 5000
}
}
}
Expand All @@ -159,7 +164,14 @@
"args": [],
"env": {},
"stopOnEntry": true
"initialConfigurations": "extension.perl-debug.provideInitialConfigurations"
},
{
"type": "perl",
"request": "launch",
"name": "Perl-Debug remote",
"program": "${workspaceFolder}/${relativeFile}",
"root": "${workspaceRoot}/",
"port": 5000
}
]
}
Expand Down
12 changes: 12 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ A debugger for perl in vs code.
* `args` Can be an array of strings / program arguments
* `env` Used for setting environment variables when debugging, `PATH` and `PERL5LIB` default to system unless overwritten
* `trace` Boolean value to enable Debug Adapter Logging in `perl-debug.log` file
* `port` Number for port to listen for remote debuggers to connect to. *(Used only for remote debugging)*

### Setup notes

Expand All @@ -50,6 +51,17 @@ A standard `launch.json` will resemble the following (on Windows, *nix distros w
]
}

### Remote debugger

When setting the `port` attribute in `launch.json` the vs code debug extension will start a debug server for the remote perl debug instance to connect to.

eg.:
```bash
# Start remote debugger in vs code on port 5000 then:
$ PERLDB_OPTS="RemotePort=localhost:5000" perl -d test.pl
```
*`localhost` should be replaced by the ip address*

### Stability

Tests matrix running between os and perl versions:
Expand Down
50 changes: 19 additions & 31 deletions src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import {spawn} from 'child_process';
import {StreamCatcher} from './streamCatcher';
import * as RX from './regExp';
import variableParser, { ParsedVariable, ParsedVariableScope } from './variableParser';
import { DebugSession, LaunchOptions } from './session';
import { LocalSession } from './localSession';
import { RemoteSession } from './remoteSession';

interface ResponseError {
filename: string,
Expand All @@ -20,12 +23,6 @@ interface Variable {
variablesReference: number,
}

interface LaunchOptions {
exec?: string;
args?: string[];
env?: {},
}

interface StackFrame {
v: string,
name: string,
Expand Down Expand Up @@ -113,7 +110,7 @@ function relativeFilename(root: string, filename: string): string {

export class perlDebuggerConnection {
public debug: boolean = false;
private perlDebugger;
public perlDebugger: DebugSession;
public streamCatcher: StreamCatcher;
public perlVersion: string;
public commandRunning: string = '';
Expand Down Expand Up @@ -284,43 +281,34 @@ export class perlDebuggerConnection {
console.log(`env.${key}: "${options.env[key]}"`);
});
}
if (this.debug) console.log(`Launch "perl -d ${sourceFile}" in "${cwd}"`);

this.logOutput(`Platform: ${process.platform}`);

this.logOutput(`Launch "perl -d ${sourceFile}" in "${cwd}"`);

// Verify file and folder existence
// xxx: We can improve the error handling
if (!fs.existsSync(sourceFile)) this.logOutput( `Error: File ${sourceFile} not found`);
if (cwd && !fs.existsSync(cwd)) this.logOutput( `Error: Folder ${cwd} not found`);

const perlCommand = options.exec || 'perl';
const programArguments = options.args || [];

const commandArgs = [].concat(args, [ '-d', sourceFile /*, '-emacs'*/], programArguments);
this.commandRunning = `${perlCommand} ${commandArgs.join(' ')}`;
this.logOutput(this.commandRunning);

const spawnOptions = {
detached: true,
cwd: cwd || undefined,
env: {
COLUMNS: 80,
LINES: 25,
TERM: 'dumb',
...options.env,
},
};
this.logOutput(`Platform: ${process.platform}`);
this.logOutput(`Launch "perl -d ${sourceFile}" in "${cwd}"`);


// xxx: add failure handling
this.perlDebugger = spawn(perlCommand, commandArgs, spawnOptions);
if (!options.port) {
// If no port is configured then run this locally in a fork
this.perlDebugger = new LocalSession(filename, cwd, args, options);
} else {
// If port is configured then use the remote session.
this.logOutput(`Waiting for remote debugger to connect on port "${options.port}"`);
this.perlDebugger = new RemoteSession(options.port);
}

this.commandRunning = this.perlDebugger.title();
this.logOutput(this.perlDebugger.title());

this.perlDebugger.on('error', (err) => {
if (this.debug) console.log('error:', err);
this.logOutput( `Error`);
this.logOutput( err );
this.logOutput( `DUMP: spawn(${perlCommand}, ${JSON.stringify(commandArgs)}, ${JSON.stringify(spawnOptions)});` );
this.logOutput( `DUMP: ${this.perlDebugger.dump()}` );
});

this.streamCatcher.launch(this.perlDebugger.stdin, this.perlDebugger.stderr);
Expand Down
3 changes: 2 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as vscode from 'vscode';
import * as path from 'path';

const initialConfigurations = {
version: '0.0.6',
version: '0.0.7',
configurations: [
{
type: 'perl',
Expand All @@ -18,6 +18,7 @@ const initialConfigurations = {
args: [],
env: {},
stopOnEntry: true,
port: 0,
}
]}

Expand Down
40 changes: 40 additions & 0 deletions src/localSession.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {spawn} from 'child_process';
import { Readable, Writable } from 'stream';
import { DebugSession, LaunchOptions } from './session';

export class LocalSession implements DebugSession {
public stdin: Writable;
public stdout: Readable;
public stderr: Readable;
public on: Function;
public kill: Function;
public title: Function;
public dump: Function;

constructor(filename: string, cwd: string, args: string[] = [], options: LaunchOptions = {}) {
const perlCommand = options.exec || 'perl';
const programArguments = options.args || [];

const commandArgs = [].concat(args, [ '-d', filename /*, '-emacs'*/], programArguments);

const spawnOptions = {
detached: true,
cwd: cwd || undefined,
env: {
COLUMNS: 80,
LINES: 25,
TERM: 'dumb',
...options.env,
},
};

const session = spawn(perlCommand, commandArgs, spawnOptions);
this.stdin = session.stdin;
this.stdout = session.stdout;
this.stderr = session.stderr;
this.on = (type, callback) => session.on(type, callback);
this.kill = () => session.kill();
this.title = () => `${perlCommand} ${commandArgs.join(' ')}`;
this.dump = () => `spawn(${perlCommand}, ${JSON.stringify(commandArgs)}, ${JSON.stringify(spawnOptions)});`;
}
}
73 changes: 73 additions & 0 deletions src/remoteSession.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import * as net from 'net';
import {spawn} from 'child_process';
import { Readable, Writable } from 'stream';
import { EventEmitter } from 'events';
import { DebugSession, LaunchOptions } from './session';

export class RemoteSession implements DebugSession {
public stdin: Writable;
public stdout: Readable;
public stderr: Readable;
public on: Function;
public kill: Function;
public title: Function;
public dump: Function;
private event = new EventEmitter();

constructor(port: number) {
// Keep track of the chat clients
let client;

this.stdin = new Writable({
write(chunk, encoding, callback) {
if (client) {
client.write(chunk);
callback();
}
},
});

this.stdout = new Readable({
read() {},
});
this.stderr = new Readable({
read() {},
});

const server = net.createServer((socket) => {
const name = `${socket.remoteAddress}:${socket.remotePort}`;

if (!client) {
client = socket;
this.stdout.push(`> Remote debugger at "${name}" connected at port ${port}.`);
} else {
// Already have a client connected, lets close and notify user
this.stderr.push(`> Warning: Additional remote client tried to connect "${name}".`);
socket.end('Remote debugger already connected!');
}

socket.on('data', data => this.stderr.push(data));

socket.on('end', data => {
client = null;
this.event.emit('close', data);
});

socket.on('error', data => this.event.emit('error', data));
});

server.listen(port, '0.0.0.0'); // Listen to port make it remotely available
server.on('error', data => this.event.emit('error', data));

this.on = (type, callback) => this.event.on(type, callback);
this.kill = () => {
server.close();
this.event.removeAllListeners();
this.stdin.removeAllListeners();
this.stdout.removeAllListeners();
this.stderr.removeAllListeners();
};
this.title = () => `Running debug server for remote session to connect on port "${port}"`;
this.dump = () => `debug server port ${port}`;
}
}
18 changes: 18 additions & 0 deletions src/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Readable, Writable } from 'stream';

export interface DebugSession {
kill: Function,
stderr: Readable,
stdout: Readable,
stdin: Writable,
on: Function, // support "close", "error"
title: Function,
dump: Function, // Dump debug information
}

export interface LaunchOptions {
exec?: string;
args?: string[];
env?: {},
port?: number;
}
30 changes: 30 additions & 0 deletions src/tests/connection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import assert = require('assert');
import asyncAssert from './asyncAssert';
import * as Path from 'path';
import { perlDebuggerConnection, RequestResponse } from '../adapter';
import { LocalSession } from '../localSession';

const PROJECT_ROOT = Path.join(__dirname, '../../');
const DATA_ROOT = Path.join(PROJECT_ROOT, 'src/tests/data/');
Expand Down Expand Up @@ -52,6 +53,35 @@ suite('Perl debugger connection', () => {
assert.equal(res.ln, 5);
});

test('Should be able to connect and launch remote ' + FILE_TEST_PL, async () => {
const port = 5000;
// Listen for remote debugger session
const server = conn.launchRequest(FILE_TEST_PL, DATA_ROOT, [], {
...launchOptions,
port, // Trigger server
});
// Start "remote" debug session
const local = new LocalSession(FILE_TEST_PL, DATA_ROOT, [], {
...launchOptions,
env: {
...launchOptions.env,
PERLDB_OPTS: `RemotePort=localhost:${port}`, // Trigger remote debugger
},
});

// Wait for result
const res = await server;

// Cleanup
local.kill();
conn.perlDebugger.kill();

assert.equal(local.title(), `perl -d ${FILE_TEST_PL}`);
assert.equal(res.finished, false);
assert.equal(res.exception, false);
assert.equal(res.ln, 5); // The first code line in test.pl is 5
});

test('Should error when launching ' + FILE_BROKEN_SYNTAX, async () => {
const res = <RequestResponse>await asyncAssert.throws(conn.launchRequest(FILE_BROKEN_SYNTAX, DATA_ROOT, [], launchOptions));

Expand Down

0 comments on commit 6b1819e

Please sign in to comment.