Skip to content
This repository has been archived by the owner on Jul 31, 2023. It is now read-only.

Commit

Permalink
Merge pull request #586 from jvilk-stripe/jvilk/add-unix-domain-socke…
Browse files Browse the repository at this point in the history
…t-support

Add Unix domain socket support to the debugger.
  • Loading branch information
wingrunr21 authored Feb 23, 2020
2 parents 9739ca4 + 308ec1a commit fdc33a3
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 65 deletions.
4 changes: 4 additions & 0 deletions packages/vscode-ruby-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,10 @@
"description": "Port for remote debugging.",
"default": "1234"
},
"localSocketPath": {
"type": "string",
"description": "Path to UNIX domain socket for remote debugging."
},
"remoteWorkspaceRoot": {
"type": "string",
"description": "Remote workspace root, this parameter is required for remote debugging.",
Expand Down
12 changes: 7 additions & 5 deletions packages/vscode-ruby-debugger/src/interface.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {DebugProtocol} from 'vscode-debugprotocol';
import { DebugProtocol } from 'vscode-debugprotocol';

/**
* This interface should always match the schema found in the vscode-ruby extension manifest.
*/
export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments{
export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments {
/** An absolute path to the program to debug. */
program: string;
/** Optional arguments passed to the program being debugged. */
Expand All @@ -16,16 +16,18 @@ export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArgum
cwd?: string;
}

export interface AttachRequestArguments extends DebugProtocol.AttachRequestArguments{
/** Executable working directory. */
export interface AttachRequestArguments extends DebugProtocol.AttachRequestArguments {
/** Executable working directory. */
cwd?: string;
/** Optional host address for remote debugging. */
remoteHost?: string;
/** Optional port for remote debugging. */
remotePort?: string;
/** Path to UNIX domain socket for remote debugging. */
localSocketPath?: string;
/** Optional remote workspace root, this parameter is required for remote debugging */
remoteWorkspaceRoot?: string;
/** Show debugger process output. If not specified, there will only be executable output */
/** Show debugger process output. If not specified, there will only be executable output */
showDebuggerOutput?: boolean;
}

Expand Down
134 changes: 74 additions & 60 deletions packages/vscode-ruby-debugger/src/ruby.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
'use strict';

import {basename, dirname} from 'path';
import { basename, dirname } from 'path';
import * as fs from 'fs';
import * as net from 'net';
import * as childProcess from 'child_process';
import {EventEmitter} from 'events';
import {DOMParser} from 'xmldom';
import {LaunchRequestArguments, AttachRequestArguments, IRubyEvaluationResult, IDebugVariable, ICommand} from './interface';
import {SocketClientState, Mode} from './common';
import {includes} from './helper';
import { EventEmitter } from 'events';
import { DOMParser } from 'xmldom';
import { LaunchRequestArguments, AttachRequestArguments, IRubyEvaluationResult, IDebugVariable, ICommand } from './interface';
import { SocketClientState, Mode } from './common';
import { includes } from './helper';

var domErrorLocator: any = {};

const ELEMENT_NODE:number = 1; // Node.ELEMENT_NODE
const ELEMENT_NODE: number = 1; // Node.ELEMENT_NODE

type ExecutableCommandConfiguration = {
pathToRuby : string;
useBundler : boolean;
pathToBundler : string;
rdebugIdePath : string;
pathToRuby: string;
useBundler: boolean;
pathToBundler: string;
rdebugIdePath: string;
};

export class RubyProcess extends EventEmitter {
private debugSocketClient : net.Socket = null;
private debugSocketClient: net.Socket = null;
private buffer: string;
private socketConnected: boolean;
private parser: DOMParser;
Expand All @@ -31,7 +32,7 @@ export class RubyProcess extends EventEmitter {
private _state: SocketClientState;

private domErrors: any;
private domErrorHandler(type: string, error: string){
private domErrorHandler(type: string, error: string) {
this.domErrors.push({
lineNumber: domErrorLocator.lineNumber,
columnNumber: domErrorLocator.columnNumber,
Expand All @@ -49,19 +50,19 @@ export class RubyProcess extends EventEmitter {
}

public executableCommandConfiguration(args) {
let rdebugIdeDefault : string;
let rdebugIdeDefault: string;
if (process.platform === 'win32') {
rdebugIdeDefault = 'rdebug-ide.bat';
}
else {
rdebugIdeDefault = 'rdebug-ide';
}

let result : ExecutableCommandConfiguration = {
let result: ExecutableCommandConfiguration = {
pathToRuby: 'ruby',
useBundler: false,
pathToBundler : 'bundle',
rdebugIdePath : rdebugIdeDefault
pathToBundler: 'bundle',
rdebugIdePath: rdebugIdeDefault
}

if (args.pathToRuby) {
Expand Down Expand Up @@ -89,7 +90,7 @@ export class RubyProcess extends EventEmitter {

this.buffer = '';
this.parser = new DOMParser({
errorHandler: (type,msg)=>this.domErrorHandler(type,msg),
errorHandler: (type, msg) => this.domErrorHandler(type, msg),
locator: domErrorLocator
});

Expand All @@ -99,7 +100,7 @@ export class RubyProcess extends EventEmitter {
this.state = SocketClientState.connected;
//first thing we have to send is the start - if stopOnEntry is
//selected, rdebug-ide stops on the first executable line
this.pendingCommands.forEach( cmd => {
this.pendingCommands.forEach(cmd => {
this.pendingResponses.push(cmd);
this.debugSocketClient.write(cmd.command + '\n');
});
Expand All @@ -113,16 +114,16 @@ export class RubyProcess extends EventEmitter {
this.emit('debuggerComplete');
});

this.debugSocketClient.on('close', d=> {
this.debugSocketClient.on('close', d => {
this.state = SocketClientState.closed;
});

this.debugSocketClient.on('error', d=> {
this.debugSocketClient.on('error', d => {
var msg = 'Client: ' + d;
this.emit('nonTerminalError', msg);
});

this.debugSocketClient.on('timeout', d=> {
this.debugSocketClient.on('timeout', d => {
var msg = 'Timeout: ' + d;
this.emit('nonTerminalError', msg);
});
Expand All @@ -132,62 +133,62 @@ export class RubyProcess extends EventEmitter {
var threadId: any;
//ensure the dom is stable (complete)
this.domErrors = [];
var document: XMLDocument = this.parser.parseFromString(this.buffer,'application/xml');
if ( this.domErrors.length ){
var document: XMLDocument = this.parser.parseFromString(this.buffer, 'application/xml');
if (this.domErrors.length) {
//don't report stuff we can deal with happily
if ( !(
includes(this.domErrors[0].error, 'unclosed xml attribute', 0)||
if (!(
includes(this.domErrors[0].error, 'unclosed xml attribute', 0) ||
includes(this.domErrors[0].error, 'attribute space is required', 0) ||
includes(this.domErrors[0].error, "elements closed character '/' and '>' must be connected", 0)
))
this.emit('debuggerOutput','Debugger failed to parse: ' + this.domErrors[0].error + "\nFor: " + this.buffer.slice(0,20));
if ( this.buffer.indexOf('<eval ') >= 0 &&
))
this.emit('debuggerOutput', 'Debugger failed to parse: ' + this.domErrors[0].error + "\nFor: " + this.buffer.slice(0, 20));
if (this.buffer.indexOf('<eval ') >= 0 &&
(includes(this.domErrors[0].error, 'attribute space is required', 0) ||
includes(this.domErrors[0].error, "elements closed character '/' and '>' must be connected", 0))){
includes(this.domErrors[0].error, "elements closed character '/' and '>' must be connected", 0))) {
//potentially an issue with the 'eval' tagName
let start = this.buffer.indexOf('<eval ');
let end = this.buffer.indexOf('" />',start);
if ( end < 0 ) return; //perhaps not all in yet
start = this.buffer.indexOf(' value="',start);
if ( start < 0 ) return; //not the right structure
let end = this.buffer.indexOf('" />', start);
if (end < 0) return; //perhaps not all in yet
start = this.buffer.indexOf(' value="', start);
if (start < 0) return; //not the right structure
start += 8;
let inner = this.buffer.slice(start,end).replace(/\"/g,'&quot;');
this.buffer = this.buffer.slice(0,start) + inner + this.buffer.slice(end);
let inner = this.buffer.slice(start, end).replace(/\"/g, '&quot;');
this.buffer = this.buffer.slice(0, start) + inner + this.buffer.slice(end);
this.domErrors = [];
document = this.parser.parseFromString(this.buffer,'application/xml');
document = this.parser.parseFromString(this.buffer, 'application/xml');
} else return; //one of the xml elements is incomplete
}
//if it's still bad: - we need to do something else with this
if ( this.domErrors.length ) return;
if (this.domErrors.length) return;

for (let idx = 0; idx < document.childNodes.length; idx++){
for (let idx = 0; idx < document.childNodes.length; idx++) {
let node: any = document.childNodes[idx];
let attributes: any = {};
if (node.attributes && node.attributes.length){
for (let attrIdx = 0; attrIdx < node.attributes.length; attrIdx++){
if (node.attributes && node.attributes.length) {
for (let attrIdx = 0; attrIdx < node.attributes.length; attrIdx++) {
attributes[node.attributes[attrIdx].name] = node.attributes[attrIdx].value;
}
if ( attributes.threadId ) attributes.threadId = +attributes.threadId;
if (attributes.threadId) attributes.threadId = +attributes.threadId;
}
//the structure here only has one or the other
if (node.childNodes && node.childNodes.length){
if (node.childNodes && node.childNodes.length) {
let finalAttributes = [];
//all of the child nodes are the same type in our responses
for (let nodeIdx = 0; nodeIdx < node.childNodes.length; nodeIdx++){
for (let nodeIdx = 0; nodeIdx < node.childNodes.length; nodeIdx++) {
let childNode = node.childNodes[nodeIdx];
if ( childNode.nodeType !== ELEMENT_NODE ) continue;
if (childNode.nodeType !== ELEMENT_NODE) continue;
attributes = {}
if ( childNode.attributes && childNode.attributes.length ){
for (let attrIdx = 0; attrIdx < childNode.attributes.length; attrIdx++){
if (childNode.attributes && childNode.attributes.length) {
for (let attrIdx = 0; attrIdx < childNode.attributes.length; attrIdx++) {
attributes[childNode.attributes[attrIdx].name] = childNode.attributes[attrIdx].value;
}
}
finalAttributes.push(attributes);
}
attributes = finalAttributes;
}
if ( ['breakpoint','suspended','exception'].indexOf(node.tagName) >= 0){
this.emit(node.tagName, attributes );
if (['breakpoint', 'suspended', 'exception'].indexOf(node.tagName) >= 0) {
this.emit(node.tagName, attributes);
}
//this just assumes we don't get anything in between
else this.FinishCmd(attributes);
Expand All @@ -198,7 +199,7 @@ export class RubyProcess extends EventEmitter {
let executableCommandConfiguration = this.executableCommandConfiguration(args);

if (mode == Mode.launch) {
var runtimeArgs : string[];
var runtimeArgs: string[];
var runtimeExecutable: string;

if (args.noDebug) {
Expand All @@ -209,16 +210,16 @@ export class RubyProcess extends EventEmitter {
runtimeExecutable = executableCommandConfiguration.rdebugIdePath;
runtimeArgs = ['--evaluation-timeout', '10']

if (args.showDebuggerOutput){
if (args.showDebuggerOutput) {
runtimeArgs.push('-x');
}

if (args.debuggerPort && args.debuggerPort !== '1234'){
if (args.debuggerPort && args.debuggerPort !== '1234') {
runtimeArgs.push('-p');
runtimeArgs.push(args.debuggerPort);
}

if (args.stopOnEntry){
if (args.stopOnEntry) {
runtimeArgs.push('--stop');
}
}
Expand All @@ -227,29 +228,29 @@ export class RubyProcess extends EventEmitter {

var processEnv = {};
//use process environment
for( var env in process.env) {
for (var env in process.env) {
processEnv[env] = process.env[env];
}
//merge supplied environment
for( var env in args.env) {
for (var env in args.env) {
processEnv[env] = args.env[env];
}

if (executableCommandConfiguration.useBundler){
if (executableCommandConfiguration.useBundler) {
runtimeArgs.unshift(runtimeExecutable);
runtimeArgs.unshift('exec');
runtimeExecutable = executableCommandConfiguration.pathToBundler;
}

if (args.includes){
if (args.includes) {
args.includes.forEach((path) => {
runtimeArgs.push('-I')
runtimeArgs.push(path)
})
}

// '--' forces process arguments (args.args) not to be swollowed by rdebug-ide
this.debugprocess = childProcess.spawn(runtimeExecutable, [...runtimeArgs, '--', args.program, ...args.args || []], {cwd: processCwd, env: processEnv});
this.debugprocess = childProcess.spawn(runtimeExecutable, [...runtimeArgs, '--', args.program, ...args.args || []], { cwd: processCwd, env: processEnv });

// redirect output to debug console
this.debugprocess.stdout.on('data', (data: Buffer) => {
Expand Down Expand Up @@ -277,7 +278,20 @@ export class RubyProcess extends EventEmitter {
});
}
else {
this.debugSocketClient.connect(args.remotePort, args.remoteHost);
if (args.localSocketPath) {
fs.access(args.localSocketPath, err => {
if (err) {
this.emit('debuggerOutput', 'Error: ' + err.toString());
this.emit('debuggerComplete');
} else {
this.emit('debuggerOutput', 'Connecting to ' + args.localSocketPath);
this.debugSocketClient.connect(args.localSocketPath);
}
});
}
else {
this.debugSocketClient.connect(args.remotePort, args.remoteHost);
}
}
}

Expand All @@ -296,7 +310,7 @@ export class RubyProcess extends EventEmitter {
}

public Enqueue(cmd: string): Promise<any> {
var pro = new Promise<any>((resolve, reject) => {
var pro = new Promise<any>((resolve, reject) => {
var newCommand = {
command: cmd,
resolve: resolve,
Expand Down

0 comments on commit fdc33a3

Please sign in to comment.