Skip to content
This repository has been archived by the owner on Oct 12, 2022. It is now read-only.

Commit

Permalink
auto attach to child processes; first cut for microsoft/vscode#40123
Browse files Browse the repository at this point in the history
  • Loading branch information
weinand committed Dec 12, 2017
1 parent 09c517f commit 6633a02
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 1 deletion.
164 changes: 164 additions & 0 deletions src/node/extension/childProcesses.ts
@@ -0,0 +1,164 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

'use strict';

import { exec } from 'child_process';
import * as vscode from 'vscode';

const DEBUG_PORT_PATTERN = /\s--(inspect|debug)-port=(\d+)/;
const DEBUG_FLAGS_PATTERN = /\s--(inspect|debug)(-brk)?(=(\d+))?/;

class Cluster {
session: vscode.DebugSession;
pids: Set<number>;
intervalId: NodeJS.Timer;

constructor() {
this.pids = new Set<number>();
}

startWatching(session: vscode.DebugSession) {
this.session = session;

// get the process ID from the debuggee
this.session.customRequest('evaluate', { expression: 'process.pid', global: true }).then(reply => {
const rootPid = parseInt(reply.result);
this.pollChildProcesses(rootPid, (pid, cmd) => {
if (!this.pids.has(pid)) {
this.pids.add(pid);
attachChildProcess(pid, cmd);
}
});
});

}

stopWatching() {
if (this.intervalId) {
clearInterval(this.intervalId);
}
}

private pollChildProcesses(rootPid: number, cb: (pid, cmd) => void) {

function filter(pid: number, cmd: string) {

const matches = DEBUG_PORT_PATTERN.exec(cmd);
const matches2 = DEBUG_FLAGS_PATTERN.exec(cmd);
if ((matches && matches.length >= 3) || (matches2 && matches2.length >= 5)) {
cb(pid, cmd);
}
}

findChildProcesses(rootPid, filter);
this.intervalId = setInterval(() => {
findChildProcesses(rootPid, filter);
}, 1000);
}
}

const clusters = new Map<string,Cluster>();

export function prepareCluster(config: vscode.DebugConfiguration) {
clusters.set(config.name, new Cluster());
}

export function startCluster(session: vscode.DebugSession) {
const cluster = clusters.get(session.name);
if (cluster) {
cluster.startWatching(session);
}
}

export function stopCluster(session: vscode.DebugSession) {
const cluster = clusters.get(session.name);
if (cluster) {
cluster.stopWatching();
clusters.delete(session.name);
}
}

function attachChildProcess(pid: number, cmd: string) {

const config: vscode.DebugConfiguration = {
type: 'node',
request: 'attach',
name: `Child Process ${pid}`,
stopOnEntry: false
};

// match --debug, --debug=1234, --debug-brk, debug-brk=1234, --inspect, --inspect=1234, --inspect-brk, --inspect-brk=1234
let matches = DEBUG_FLAGS_PATTERN.exec(cmd);
if (matches && matches.length >= 2) {
// attach via port
if (matches.length === 5 && matches[4]) {
config.port = parseInt(matches[4]);
}
config.protocol= matches[1] === 'debug' ? 'legacy' : 'inspector';
} else {
// no port -> try to attach via pid (send SIGUSR1)
config.processId = String(pid);
}

// a debug-port=1234 or --inspect-port=1234 overrides the port
matches = DEBUG_PORT_PATTERN.exec(cmd);
if (matches && matches.length === 3) {
// override port
config.port = parseInt(matches[2]);
}

//log(`attach: ${config.protocol} ${config.port}`);

vscode.debug.startDebugging(undefined, config);
}

function findChildProcesses(rootPid: number, cb: (pid: number, cmd: string) => void) {

const set = new Set<number>();

set.add(rootPid);

function addToTree(pid: number, ppid: number, cmd: string) {
if (set.has(ppid)) {
set.add(pid);
cb(pid, cmd);
}
}

if (process.platform === 'win32') {

const CMD = 'wmic process get CommandLine,ParentProcessId,ProcessId';
const CMD_PAT = /^(.+)\s+([0-9]+)\s+([0-9]+)$/;

exec(CMD, { maxBuffer: 1000 * 1024 }, (err, stdout, stderr) => {
if (!err && !stderr) {
const lines = stdout.split('\r\n');
for (let line of lines) {
let matches = CMD_PAT.exec(line.trim());
if (matches && matches.length === 4) {
addToTree(parseInt(matches[3]), parseInt(matches[2]), matches[1].trim());
}
}
}
});
} else { // OS X & Linux

const CMD = 'ps -ax -o pid=,ppid=,command=';
const CMD_PAT = /^\s*([0-9]+)\s+([0-9]+)\s+(.+)$/;

exec(CMD, { maxBuffer: 1000 * 1024 }, (err, stdout, stderr) => {
if (!err && !stderr) {
const lines = stdout.toString().split('\n');
for (const line of lines) {
let matches = CMD_PAT.exec(line.trim());
if (matches && matches.length === 4) {
addToTree(parseInt(matches[1]), parseInt(matches[2]), matches[3]);
}
}
}
});
}
}
5 changes: 5 additions & 0 deletions src/node/extension/configurationProvider.ts
Expand Up @@ -12,6 +12,7 @@ import * as fs from 'fs';
import { log, localize } from './utilities';
import { detectDebugType, detectProtocolForPid, INSPECTOR_PORT_DEFAULT, LEGACY_PORT_DEFAULT } from './protocolDetection';
import { pickProcess } from './processPicker';
import { prepareCluster } from './childProcesses';

//---- NodeConfigurationProvider

Expand Down Expand Up @@ -61,6 +62,10 @@ export class NodeConfigurationProvider implements vscode.DebugConfigurationProvi
}
}

if (config.autoAttachChildren) {
prepareCluster(config);
}

// determine which protocol to use
return determineDebugType(config).then(debugType => {

Expand Down
5 changes: 5 additions & 0 deletions src/node/extension/extension.ts
Expand Up @@ -9,6 +9,7 @@ import * as vscode from 'vscode';
import { NodeConfigurationProvider } from './configurationProvider';
import { LoadedScriptsProvider, pickLoadedScript, openScript } from './loadedScripts';
import { pickProcess } from './processPicker';
import { startCluster, stopCluster } from './childProcesses';


export function activate(context: vscode.ExtensionContext) {
Expand All @@ -30,6 +31,10 @@ export function activate(context: vscode.ExtensionContext) {
vscode.window.registerTreeDataProvider('extension.node-debug.loadedScriptsExplorer.chrome', provider);
context.subscriptions.push(vscode.commands.registerCommand('extension.node-debug.pickLoadedScript', () => pickLoadedScript()));
context.subscriptions.push(vscode.commands.registerCommand('extension.node-debug.openScript', (session: vscode.DebugSession, source) => openScript(session, source)));

// cluster
context.subscriptions.push(vscode.debug.onDidStartDebugSession(session => startCluster(session)));
context.subscriptions.push(vscode.debug.onDidTerminateDebugSession(session => stopCluster(session)));
}

export function deactivate() {
Expand Down
4 changes: 3 additions & 1 deletion src/node/nodeDebug.ts
Expand Up @@ -1514,7 +1514,9 @@ export class NodeDebugSession extends LoggingDebugSession {
// so we use the stopped state of the VM
if (this._attachMode) {
this.log('la', `_startInitialize2: in attach mode we guess stopOnEntry flag to be '${stopped}''`);
this._stopOnEntry = stopped;
if (this._stopOnEntry === undefined) {
this._stopOnEntry = stopped;
}
}

if (this._stopOnEntry) {
Expand Down

0 comments on commit 6633a02

Please sign in to comment.