move some auto-attach code back to node-debug

This commit is contained in:
Andre Weinand 2018-08-22 01:08:34 +02:00
parent 80d6fa0b8e
commit 14595011f1
4 changed files with 10 additions and 390 deletions

View file

@ -7,8 +7,6 @@
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { basename } from 'path';
import { pollProcesses, attachToProcess } from './nodeProcessTree';
const localize = nls.loadMessageBundle();
const ON_TEXT = localize('status.text.auto.attach.on', "Auto Attach: On");
@ -17,10 +15,9 @@ const OFF_TEXT = localize('status.text.auto.attach.off', "Auto Attach: Off");
const TOGGLE_COMMAND = 'extension.node-debug.toggleAutoAttach';
let currentState: string;
let autoAttacher: vscode.Disposable | undefined;
let autoAttachStarted = false;
let statusItem: vscode.StatusBarItem | undefined = undefined;
export function activate(context: vscode.ExtensionContext): void {
context.subscriptions.push(vscode.commands.registerCommand(TOGGLE_COMMAND, toggleAutoAttach));
@ -85,9 +82,9 @@ function updateAutoAttachInStatus(context: vscode.ExtensionContext) {
statusItem.hide();
statusItem.text = OFF_TEXT;
}
if (autoAttacher) {
autoAttacher.dispose();
autoAttacher = undefined;
if (autoAttachStarted) {
autoAttachStarted = false;
vscode.commands.executeCommand('extension.node-debug.stopAutoAttach');
}
} else { // 'on' or 'off'
@ -106,27 +103,18 @@ function updateAutoAttachInStatus(context: vscode.ExtensionContext) {
if (newState === 'off') {
statusItem.text = OFF_TEXT;
if (autoAttacher) {
autoAttacher.dispose();
autoAttacher = undefined;
if (autoAttachStarted) {
autoAttachStarted = false;
vscode.commands.executeCommand('extension.node-debug.stopAutoAttach');
}
} else if (newState === 'on') {
statusItem.text = ON_TEXT;
const vscode_pid = process.env['VSCODE_PID'];
const rootPid = vscode_pid ? parseInt(vscode_pid) : 0;
autoAttacher = startAutoAttach(rootPid);
vscode.commands.executeCommand('extension.node-debug.startAutoAttach', rootPid);
autoAttachStarted = true;
}
}
}
}
function startAutoAttach(rootPid: number): vscode.Disposable {
return pollProcesses(rootPid, true, (pid, cmdPath, args) => {
const cmdName = basename(cmdPath, '.exe');
if (cmdName === 'node') {
const name = localize('process.with.pid.label', "Process {0}", pid);
attachToProcess(undefined, name, pid, args);
}
});
}

View file

@ -1,124 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 * as vscode from 'vscode';
import { getProcessTree, ProcessTreeNode } from './processTree';
import { analyseArguments } from './protocolDetection';
const pids = new Set<number>();
const POLL_INTERVAL = 1000;
/**
* Poll for all subprocesses of given root process.
*/
export function pollProcesses(rootPid: number, inTerminal: boolean, cb: (pid: number, cmd: string, args: string) => void): vscode.Disposable {
let stopped = false;
function poll() {
//const start = Date.now();
findChildProcesses(rootPid, inTerminal, cb).then(_ => {
//console.log(`duration: ${Date.now() - start}`);
setTimeout(_ => {
if (!stopped) {
poll();
}
}, POLL_INTERVAL);
});
}
poll();
return new vscode.Disposable(() => stopped = true);
}
export function attachToProcess(folder: vscode.WorkspaceFolder | undefined, name: string, pid: number, args: string, baseConfig?: vscode.DebugConfiguration) {
if (pids.has(pid)) {
return;
}
pids.add(pid);
const config: vscode.DebugConfiguration = {
type: 'node',
request: 'attach',
name: name,
stopOnEntry: false
};
if (baseConfig) {
// selectively copy attributes
if (baseConfig.timeout) {
config.timeout = baseConfig.timeout;
}
if (baseConfig.sourceMaps) {
config.sourceMaps = baseConfig.sourceMaps;
}
if (baseConfig.outFiles) {
config.outFiles = baseConfig.outFiles;
}
if (baseConfig.sourceMapPathOverrides) {
config.sourceMapPathOverrides = baseConfig.sourceMapPathOverrides;
}
if (baseConfig.smartStep) {
config.smartStep = baseConfig.smartStep;
}
if (baseConfig.skipFiles) {
config.skipFiles = baseConfig.skipFiles;
}
if (baseConfig.showAsyncStacks) {
config.sourceMaps = baseConfig.showAsyncStacks;
}
if (baseConfig.trace) {
config.trace = baseConfig.trace;
}
}
let { usePort, protocol, port } = analyseArguments(args);
if (usePort) {
config.processId = `${protocol}${port}`;
} else {
if (protocol && port > 0) {
config.processId = `${pid}${protocol}${port}`;
} else {
config.processId = pid.toString();
}
}
vscode.debug.startDebugging(folder, config);
}
function findChildProcesses(rootPid: number, inTerminal: boolean, cb: (pid: number, cmd: string, args: string) => void): Promise<void> {
function walker(node: ProcessTreeNode, terminal: boolean, terminalPids: number[]) {
if (terminalPids.indexOf(node.pid) >= 0) {
terminal = true; // found the terminal shell
}
let { protocol } = analyseArguments(node.args);
if (terminal && protocol) {
cb(node.pid, node.command, node.args);
}
for (const child of node.children || []) {
walker(child, terminal, terminalPids);
}
}
return getProcessTree(rootPid).then(tree => {
if (tree) {
const terminals = vscode.window.terminals;
if (terminals.length > 0) {
Promise.all(terminals.map(terminal => terminal.processId)).then(terminalPids => {
walker(tree, !inTerminal, terminalPids);
});
}
}
});
}

View file

@ -1,186 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { spawn, ChildProcess } from 'child_process';
import { join } from 'path';
export class ProcessTreeNode {
children?: ProcessTreeNode[];
constructor(public pid: number, public ppid: number, public command: string, public args: string) {
}
}
export async function getProcessTree(rootPid: number): Promise<ProcessTreeNode | undefined> {
const map = new Map<number, ProcessTreeNode>();
map.set(0, new ProcessTreeNode(0, 0, '???', ''));
try {
await getProcesses((pid: number, ppid: number, command: string, args: string) => {
if (pid !== ppid) {
map.set(pid, new ProcessTreeNode(pid, ppid, command, args));
}
});
} catch (err) {
return undefined;
}
const values = map.values();
for (const p of values) {
const parent = map.get(p.ppid);
if (parent && parent !== p) {
if (!parent.children) {
parent.children = [];
}
parent.children.push(p);
}
}
if (!isNaN(rootPid) && rootPid > 0) {
return map.get(rootPid);
}
return map.get(0);
}
export function getProcesses(one: (pid: number, ppid: number, command: string, args: string, date?: number) => void): Promise<void> {
// returns a function that aggregates chunks of data until one or more complete lines are received and passes them to a callback.
function lines(callback: (a: string) => void) {
let unfinished = ''; // unfinished last line of chunk
return (data: string | Buffer) => {
const lines = data.toString().split(/\r?\n/);
const finishedLines = lines.slice(0, lines.length - 1);
finishedLines[0] = unfinished + finishedLines[0]; // complete previous unfinished line
unfinished = lines[lines.length - 1]; // remember unfinished last line of this chunk for next round
for (const s of finishedLines) {
callback(s);
}
};
}
return new Promise((resolve, reject) => {
let proc: ChildProcess;
if (process.platform === 'win32') {
// attributes columns are in alphabetic order!
const CMD_PAT = /^(.*)\s+([0-9]+)\.[0-9]+[+-][0-9]+\s+([0-9]+)\s+([0-9]+)$/;
const wmic = join(process.env['WINDIR'] || 'C:\\Windows', 'System32', 'wbem', 'WMIC.exe');
proc = spawn(wmic, ['process', 'get', 'CommandLine,CreationDate,ParentProcessId,ProcessId']);
proc.stdout.setEncoding('utf8');
proc.stdout.on('data', lines(line => {
let matches = CMD_PAT.exec(line.trim());
if (matches && matches.length === 5) {
const pid = Number(matches[4]);
const ppid = Number(matches[3]);
const date = Number(matches[2]);
let args = matches[1].trim();
if (!isNaN(pid) && !isNaN(ppid) && args) {
let command = args;
if (args[0] === '"') {
const end = args.indexOf('"', 1);
if (end > 0) {
command = args.substr(1, end - 1);
args = args.substr(end + 2);
}
} else {
const end = args.indexOf(' ');
if (end > 0) {
command = args.substr(0, end);
args = args.substr(end + 1);
} else {
args = '';
}
}
one(pid, ppid, command, args, date);
}
}
}));
} else if (process.platform === 'darwin') { // OS X
proc = spawn('/bin/ps', ['-x', '-o', `pid,ppid,comm=${'a'.repeat(256)},command`]);
proc.stdout.setEncoding('utf8');
proc.stdout.on('data', lines(line => {
const pid = Number(line.substr(0, 5));
const ppid = Number(line.substr(6, 5));
const command = line.substr(12, 256).trim();
const args = line.substr(269 + command.length);
if (!isNaN(pid) && !isNaN(ppid)) {
one(pid, ppid, command, args);
}
}));
} else { // linux
proc = spawn('/bin/ps', ['-ax', '-o', 'pid,ppid,comm:20,command']);
proc.stdout.setEncoding('utf8');
proc.stdout.on('data', lines(line => {
const pid = Number(line.substr(0, 5));
const ppid = Number(line.substr(6, 5));
let command = line.substr(12, 20).trim();
let args = line.substr(33);
let pos = args.indexOf(command);
if (pos >= 0) {
pos = pos + command.length;
while (pos < args.length) {
if (args[pos] === ' ') {
break;
}
pos++;
}
command = args.substr(0, pos);
args = args.substr(pos + 1);
}
if (!isNaN(pid) && !isNaN(ppid)) {
one(pid, ppid, command, args);
}
}));
}
proc.on('error', err => {
reject(err);
});
proc.stderr.setEncoding('utf8');
proc.stderr.on('data', data => {
reject(new Error(data.toString()));
});
proc.on('close', (code, signal) => {
if (code === 0) {
resolve();
} else if (code > 0) {
reject(new Error(`process terminated with exit code: ${code}`));
}
if (signal) {
reject(new Error(`process terminated with signal: ${signal}`));
}
});
proc.on('exit', (code, signal) => {
if (code === 0) {
//resolve();
} else if (code > 0) {
reject(new Error(`process terminated with exit code: ${code}`));
}
if (signal) {
reject(new Error(`process terminated with signal: ${signal}`));
}
});
});
}

View file

@ -1,58 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
export const INSPECTOR_PORT_DEFAULT = 9229;
export const LEGACY_PORT_DEFAULT = 5858;
export interface DebugArguments {
usePort: boolean; // if true debug by using the debug port
protocol?: 'legacy' | 'inspector';
address?: string;
port: number;
}
/*
* analyse the given command line arguments and extract debug port and protocol from it.
*/
export function analyseArguments(args: string): DebugArguments {
const DEBUG_FLAGS_PATTERN = /--(inspect|debug)(-brk)?(=((\[[0-9a-fA-F:]*\]|[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+|[a-zA-Z0-9\.]*):)?(\d+))?/;
const DEBUG_PORT_PATTERN = /--(inspect|debug)-port=(\d+)/;
const result: DebugArguments = {
usePort: false,
port: -1
};
// match --debug, --debug=1234, --debug-brk, debug-brk=1234, --inspect, --inspect=1234, --inspect-brk, --inspect-brk=1234
let matches = DEBUG_FLAGS_PATTERN.exec(args);
if (matches && matches.length >= 2) {
// attach via port
result.usePort = true;
if (matches.length >= 6 && matches[5]) {
result.address = matches[5];
}
if (matches.length >= 7 && matches[6]) {
result.port = parseInt(matches[6]);
}
result.protocol = matches[1] === 'debug' ? 'legacy' : 'inspector';
}
// a debug-port=1234 or --inspect-port=1234 overrides the port
matches = DEBUG_PORT_PATTERN.exec(args);
if (matches && matches.length === 3) {
// override port
result.port = parseInt(matches[2]);
result.protocol = matches[1] === 'debug' ? 'legacy' : 'inspector';
}
if (result.port < 0) {
result.port = result.protocol === 'inspector' ? INSPECTOR_PORT_DEFAULT : LEGACY_PORT_DEFAULT;
}
return result;
}