diff --git a/.vscode/launch.json b/.vscode/launch.json index d8951d91273d5..2a12dea7bde00 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -236,6 +236,15 @@ "VSCODE_DEV": "1", "VSCODE_CLI": "1" } + }, + { + "name": "Launch Built-in Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceRoot}/extensions/debug-auto-launch" + ] } ], "compounds": [ diff --git a/build/builtInExtensions.json b/build/builtInExtensions.json index 2831ba28d9ccf..62f320a69b44e 100644 --- a/build/builtInExtensions.json +++ b/build/builtInExtensions.json @@ -1,7 +1,7 @@ [ { "name": "ms-vscode.node-debug", - "version": "1.26.1", + "version": "1.26.2", "repo": "https://github.com/Microsoft/vscode-node-debug" }, { diff --git a/extensions/debug-auto-launch/.vscodeignore b/extensions/debug-auto-launch/.vscodeignore new file mode 100644 index 0000000000000..d43a539fddf5b --- /dev/null +++ b/extensions/debug-auto-launch/.vscodeignore @@ -0,0 +1,2 @@ +src/** +tsconfig.json \ No newline at end of file diff --git a/extensions/debug-auto-launch/package.json b/extensions/debug-auto-launch/package.json new file mode 100644 index 0000000000000..353847ed6adc2 --- /dev/null +++ b/extensions/debug-auto-launch/package.json @@ -0,0 +1,54 @@ +{ + "name": "debug-auto-launch", + "displayName": "%displayName%", + "description": "%description%", + "version": "1.0.0", + "publisher": "vscode", + "engines": { + "vscode": "^1.5.0" + }, + "activationEvents": [ + "*" + ], + "main": "./out/extension", + "scripts": { + "compile": "gulp compile-extension:debug-auto-launch", + "watch": "gulp watch-extension:debug-auto-launch" + }, + "contributes": { + "configuration": { + "title": "Node debug", + "properties": { + "debug.node.autoAttach": { + "scope": "window", + "type": "string", + "enum": [ + "disabled", + "on", + "off" + ], + "enumDescriptions": [ + "%debug.node.autoAttach.disabled.description%", + "%debug.node.autoAttach.on.description%", + "%debug.node.autoAttach.off.description%" + ], + "description": "%debug.node.autoAttach.description%", + "default": "disabled" + } + } + }, + "commands": [ + { + "command": "extension.node-debug.toggleAutoAttach", + "title": "%toggle.auto.attach%", + "category": "Debug" + } + ] + }, + "dependencies": { + "vscode-nls": "^3.2.4" + }, + "devDependencies": { + "@types/node": "8.0.33" + } +} \ No newline at end of file diff --git a/extensions/debug-auto-launch/package.nls.json b/extensions/debug-auto-launch/package.nls.json new file mode 100644 index 0000000000000..030ac5a20a948 --- /dev/null +++ b/extensions/debug-auto-launch/package.nls.json @@ -0,0 +1,11 @@ +{ + "displayName": "Node Debug Auto-attach", + "description": "Helper for auto-attach feature when node-debug extensions are not active.", + + "debug.node.autoAttach.description": "Automatically attach node debugger when node.js was launched in debug mode from integrated terminal.", + "debug.node.autoAttach.disabled.description": "Auto attach is disabled and not shown in status bar.", + "debug.node.autoAttach.on.description": "Auto attach is active.", + "debug.node.autoAttach.off.description": "Auto attach is inactive.", + + "toggle.auto.attach": "Toggle Auto Attach" +} \ No newline at end of file diff --git a/extensions/debug-auto-launch/src/autoAttach.ts b/extensions/debug-auto-launch/src/autoAttach.ts new file mode 100644 index 0000000000000..a6bb925ef4f9b --- /dev/null +++ b/extensions/debug-auto-launch/src/autoAttach.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as nls from 'vscode-nls'; +import { basename } from 'path'; +import { pollProcesses, attachToProcess } from './nodeProcessTree'; + +const localize = nls.loadMessageBundle(); + +export 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); + } + }); +} diff --git a/extensions/debug-auto-launch/src/extension.ts b/extensions/debug-auto-launch/src/extension.ts new file mode 100644 index 0000000000000..e10846e111eab --- /dev/null +++ b/extensions/debug-auto-launch/src/extension.ts @@ -0,0 +1,132 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * 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"); +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 statusItem: vscode.StatusBarItem | undefined = undefined; + + +export function activate(context: vscode.ExtensionContext): void { + + context.subscriptions.push(vscode.commands.registerCommand(TOGGLE_COMMAND, toggleAutoAttach)); + + context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('debug.node.autoAttach')) { + updateAutoAttachInStatus(context); + } + })); + + updateAutoAttachInStatus(context); +} + +export function deactivate(): void { +} + + +function toggleAutoAttach(context: vscode.ExtensionContext) { + + const conf = vscode.workspace.getConfiguration('debug.node'); + + let value = conf.get('autoAttach'); + if (value === 'on') { + value = 'off'; + } else { + value = 'on'; + } + + const info = conf.inspect('autoAttach'); + let target: vscode.ConfigurationTarget = vscode.ConfigurationTarget.Global; + if (info) { + if (info.workspaceFolderValue) { + target = vscode.ConfigurationTarget.WorkspaceFolder; + } else if (info.workspaceValue) { + target = vscode.ConfigurationTarget.Workspace; + } else if (info.globalValue) { + target = vscode.ConfigurationTarget.Global; + } else if (info.defaultValue) { + // setting not yet used: store setting in workspace + if (vscode.workspace.workspaceFolders) { + target = vscode.ConfigurationTarget.Workspace; + } + } + } + conf.update('autoAttach', value, target); + + updateAutoAttachInStatus(context); +} + +function updateAutoAttachInStatus(context: vscode.ExtensionContext) { + + const newState = vscode.workspace.getConfiguration('debug.node').get('autoAttach'); + + if (newState !== currentState) { + + currentState = newState; + + if (newState === 'disabled') { + + // turn everything off + if (statusItem) { + statusItem.hide(); + statusItem.text = OFF_TEXT; + } + if (autoAttacher) { + autoAttacher.dispose(); + autoAttacher = undefined; + } + + } else { // 'on' or 'off' + + // make sure status bar item exists and is visible + if (!statusItem) { + statusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); + statusItem.command = TOGGLE_COMMAND; + statusItem.text = OFF_TEXT; + statusItem.tooltip = localize('status.tooltip.auto.attach', "Automatically attach to node.js processes in debug mode"); + statusItem.show(); + context.subscriptions.push(statusItem); + } else { + statusItem.show(); + } + + if (newState === 'off') { + statusItem.text = OFF_TEXT; + if (autoAttacher) { + autoAttacher.dispose(); + autoAttacher = undefined; + } + } 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); + } + } + } +} + +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); + } + }); +} diff --git a/extensions/debug-auto-launch/src/nodeProcessTree.ts b/extensions/debug-auto-launch/src/nodeProcessTree.ts new file mode 100644 index 0000000000000..16bc55476e117 --- /dev/null +++ b/extensions/debug-auto-launch/src/nodeProcessTree.ts @@ -0,0 +1,139 @@ +/*--------------------------------------------------------------------------------------------- + * 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(); + +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 { + + function walker(node: ProcessTreeNode, terminal: boolean, renderer: number) { + + if (node.args.indexOf('--type=terminal') >= 0 && (renderer === 0 || node.ppid === renderer)) { + terminal = true; + } + + let { protocol } = analyseArguments(node.args); + if (terminal && protocol) { + cb(node.pid, node.command, node.args); + } + + for (const child of node.children || []) { + walker(child, terminal, renderer); + } + } + + function finder(node: ProcessTreeNode, pid: number): ProcessTreeNode | undefined { + if (node.pid === pid) { + return node; + } + for (const child of node.children || []) { + const p = finder(child, pid); + if (p) { + return p; + } + } + return undefined; + } + + return getProcessTree(rootPid).then(tree => { + if (tree) { + + // find the pid of the renderer process + const extensionHost = finder(tree, process.pid); + let rendererPid = extensionHost ? extensionHost.ppid : 0; + + for (const child of tree.children || []) { + walker(child, !inTerminal, rendererPid); + } + } + }); +} diff --git a/extensions/debug-auto-launch/src/processTree.ts b/extensions/debug-auto-launch/src/processTree.ts new file mode 100644 index 0000000000000..f072d9cf58cf8 --- /dev/null +++ b/extensions/debug-auto-launch/src/processTree.ts @@ -0,0 +1,186 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { + + const map = new Map(); + + 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 { + + // 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}`)); + } + }); + }); +} + diff --git a/extensions/debug-auto-launch/src/protocolDetection.ts b/extensions/debug-auto-launch/src/protocolDetection.ts new file mode 100644 index 0000000000000..9ccca40e849c1 --- /dev/null +++ b/extensions/debug-auto-launch/src/protocolDetection.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * 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; +} diff --git a/extensions/debug-auto-launch/src/typings/ref.d.ts b/extensions/debug-auto-launch/src/typings/ref.d.ts new file mode 100644 index 0000000000000..bc057c5587839 --- /dev/null +++ b/extensions/debug-auto-launch/src/typings/ref.d.ts @@ -0,0 +1,7 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/// +/// diff --git a/extensions/debug-auto-launch/tsconfig.json b/extensions/debug-auto-launch/tsconfig.json new file mode 100644 index 0000000000000..2a5517b554375 --- /dev/null +++ b/extensions/debug-auto-launch/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "outDir": "./out", + "lib": [ + "es2015" + ], + "strict": true, + "noUnusedLocals": true, + "downlevelIteration": true + }, + "include": [ + "src/**/*" + ] +} \ No newline at end of file diff --git a/extensions/debug-auto-launch/yarn.lock b/extensions/debug-auto-launch/yarn.lock new file mode 100644 index 0000000000000..5b1dccc447c34 --- /dev/null +++ b/extensions/debug-auto-launch/yarn.lock @@ -0,0 +1,11 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/node@8.0.33": + version "8.0.33" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.33.tgz#1126e94374014e54478092830704f6ea89df04cd" + +vscode-nls@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.4.tgz#2166b4183c8aea884d20727f5449e62be69fd398"