From 86052e38f3a25f365e57590cbe2175bb24b84939 Mon Sep 17 00:00:00 2001 From: Rachel Macfarlane Date: Fri, 30 Nov 2018 13:27:27 -0800 Subject: [PATCH] Support debugging process from the process explorer, closes #63147 (#63953) --- .../issue/issueReporterMain.ts | 10 ++- .../processExplorer/processExplorerMain.ts | 51 ++++++++++++++++ .../issue/electron-main/issueService.ts | 50 ++++++++++----- src/vs/platform/windows/common/windows.ts | 1 + src/vs/workbench/electron-browser/window.ts | 2 +- .../parts/debug/browser/debugActions.ts | 10 ++- .../electron-browser/debug.contribution.ts | 61 +++++++++++++++++-- 7 files changed, 159 insertions(+), 26 deletions(-) diff --git a/src/vs/code/electron-browser/issue/issueReporterMain.ts b/src/vs/code/electron-browser/issue/issueReporterMain.ts index 2fd1d37ab3f91..bd3bf12819630 100644 --- a/src/vs/code/electron-browser/issue/issueReporterMain.ts +++ b/src/vs/code/electron-browser/issue/issueReporterMain.ts @@ -378,15 +378,19 @@ export class IssueReporter extends Disposable { this.previewButton.onDidClick(() => this.createIssue()); + function sendWorkbenchCommand(commandId: string) { + ipcRenderer.send('vscode:workbenchCommand', { id: commandId, from: 'issueReporter' }); + } + this.addEventListener('disableExtensions', 'click', () => { - ipcRenderer.send('vscode:workbenchCommand', 'workbench.action.reloadWindowWithExtensionsDisabled'); + sendWorkbenchCommand('workbench.action.reloadWindowWithExtensionsDisabled'); }); this.addEventListener('disableExtensions', 'keydown', (e: KeyboardEvent) => { e.stopPropagation(); if (e.keyCode === 13 || e.keyCode === 32) { - ipcRenderer.send('vscode:workbenchCommand', 'workbench.extensions.action.disableAll'); - ipcRenderer.send('vscode:workbenchCommand', 'workbench.action.reloadWindow'); + sendWorkbenchCommand('workbench.extensions.action.disableAll'); + sendWorkbenchCommand('workbench.action.reloadWindow'); } }); diff --git a/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts b/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts index 5418d03409313..6749a894aaaa4 100644 --- a/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts +++ b/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts @@ -19,6 +19,9 @@ import { popup } from 'vs/base/parts/contextmenu/electron-browser/contextmenu'; let processList: any[]; let mapPidToWindowTitle = new Map(); +const DEBUG_FLAGS_PATTERN = /\s--(inspect|debug)(-brk|port)?=(\d+)?/; +const DEBUG_PORT_PATTERN = /\s--(inspect|debug)-port=(\d+)/; + function getProcessList(rootProcess: ProcessItem) { const processes: any[] = []; @@ -62,6 +65,40 @@ function getProcessItem(processes: any[], item: ProcessItem, indent: number): vo } } +function isDebuggable(cmd: string): boolean { + const matches = DEBUG_FLAGS_PATTERN.exec(cmd); + return (matches && matches.length >= 2) || cmd.indexOf('node ') >= 0 || cmd.indexOf('node.exe') >= 0; +} + +function attachTo(item: ProcessItem) { + const config: any = { + type: 'node', + request: 'attach', + name: `process ${item.pid}` + }; + + let matches = DEBUG_FLAGS_PATTERN.exec(item.cmd); + if (matches && matches.length >= 2) { + // attach via port + if (matches.length === 4 && matches[3]) { + config.port = parseInt(matches[3]); + } + config.protocol = matches[1] === 'debug' ? 'legacy' : 'inspector'; + } else { + // no port -> try to attach via pid (send SIGUSR1) + config.processId = String(item.pid); + } + + // a debug-port=n or inspect-port=n overrides the port + matches = DEBUG_PORT_PATTERN.exec(item.cmd); + if (matches && matches.length === 3) { + // override port + config.port = parseInt(matches[2]); + } + + ipcRenderer.send('vscode:workbenchCommand', { id: 'workbench.action.debug.start', from: 'processExplorer', args: [config] }); +} + function getProcessIdWithHighestProperty(processList, propertyName: string) { let max = 0; let maxProcessId; @@ -190,6 +227,20 @@ function showContextMenu(e) { } } }); + + const item = processList.filter(process => process.pid === pid)[0]; + if (item && isDebuggable(item.cmd)) { + items.push({ + type: 'separator' + }); + + items.push({ + label: localize('debug', "Debug"), + click() { + attachTo(item); + } + }); + } } else { items.push({ label: localize('copyAll', "Copy All"), diff --git a/src/vs/platform/issue/electron-main/issueService.ts b/src/vs/platform/issue/electron-main/issueService.ts index 7ebf968ed4154..19d5f0c8db425 100644 --- a/src/vs/platform/issue/electron-main/issueService.ts +++ b/src/vs/platform/issue/electron-main/issueService.ts @@ -21,8 +21,9 @@ const DEFAULT_BACKGROUND_COLOR = '#1E1E1E'; export class IssueService implements IIssueService { _serviceBrand: any; _issueWindow: BrowserWindow | null; - _issueParentWindow: BrowserWindow; + _issueParentWindow: BrowserWindow | null; _processExplorerWindow: BrowserWindow | null; + _processExplorerParentWindow: BrowserWindow | null; constructor( private machineId: string, @@ -49,9 +50,23 @@ export class IssueService implements IIssueService { }); }); - ipcMain.on('vscode:workbenchCommand', (event, arg) => { - if (this._issueParentWindow) { - this._issueParentWindow.webContents.send('vscode:runAction', { id: arg, from: 'issueReporter' }); + ipcMain.on('vscode:workbenchCommand', (_, commandInfo) => { + const { id, from, args } = commandInfo; + + let parentWindow: BrowserWindow | null; + switch (from) { + case 'issueReporter': + parentWindow = this._issueParentWindow; + break; + case 'processExplorer': + parentWindow = this._processExplorerParentWindow; + break; + default: + throw new Error(`Unexpected command source: ${from}`); + } + + if (parentWindow) { + parentWindow.webContents.send('vscode:runAction', { id, from, args }); } }); @@ -75,10 +90,11 @@ export class IssueService implements IIssueService { openReporter(data: IssueReporterData): Promise { return new Promise(_ => { - this._issueParentWindow = BrowserWindow.getFocusedWindow(); - if (this._issueParentWindow) { - const position = this.getWindowPosition(this._issueParentWindow, 700, 800); - if (!this._issueWindow) { + if (!this._issueWindow) { + this._issueParentWindow = BrowserWindow.getFocusedWindow(); + if (this._issueParentWindow) { + const position = this.getWindowPosition(this._issueParentWindow, 700, 800); + this._issueWindow = new BrowserWindow({ width: position.width, height: position.height, @@ -107,7 +123,9 @@ export class IssueService implements IIssueService { } }); } + } + if (this._issueWindow) { this._issueWindow.focus(); } }); @@ -117,9 +135,9 @@ export class IssueService implements IIssueService { return new Promise(_ => { // Create as singleton if (!this._processExplorerWindow) { - const parentWindow = BrowserWindow.getFocusedWindow(); - if (parentWindow) { - const position = this.getWindowPosition(parentWindow, 800, 300); + this._processExplorerParentWindow = BrowserWindow.getFocusedWindow(); + if (this._processExplorerParentWindow) { + const position = this.getWindowPosition(this._processExplorerParentWindow, 800, 300); this._processExplorerWindow = new BrowserWindow({ skipTaskbar: true, resizable: true, @@ -156,18 +174,18 @@ export class IssueService implements IIssueService { this._processExplorerWindow.on('close', () => this._processExplorerWindow = null); - parentWindow.on('close', () => { + this._processExplorerParentWindow.on('close', () => { if (this._processExplorerWindow) { this._processExplorerWindow.close(); this._processExplorerWindow = null; } }); } + } - // Focus - if (this._processExplorerWindow) { - this._processExplorerWindow.focus(); - } + // Focus + if (this._processExplorerWindow) { + this._processExplorerWindow.focus(); } }); } diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index ae3f06480a6f2..c72fd4990b5bf 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -404,6 +404,7 @@ export interface IWindowConfiguration extends ParsedArgs { export interface IRunActionInWindowRequest { id: string; from: 'menu' | 'touchbar' | 'mouse'; + args?: any[]; } export class ActiveWindowManager implements IDisposable { diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index 08f4c41813bd9..6bf48153ccc2b 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -104,7 +104,7 @@ export class ElectronWindow extends Themable { // Support runAction event ipc.on('vscode:runAction', (event: any, request: IRunActionInWindowRequest) => { - const args: any[] = []; + const args: any[] = request.args || []; // If we run an action from the touchbar, we fill in the currently active resource // as payload because the touch bar items are context aware depending on the editor diff --git a/src/vs/workbench/parts/debug/browser/debugActions.ts b/src/vs/workbench/parts/debug/browser/debugActions.ts index fa4fd0de782bd..2a9d55d19f316 100644 --- a/src/vs/workbench/parts/debug/browser/debugActions.ts +++ b/src/vs/workbench/parts/debug/browser/debugActions.ts @@ -10,7 +10,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IFileService } from 'vs/platform/files/common/files'; -import { IDebugService, State, IDebugSession, IThread, IEnablement, IBreakpoint, IStackFrame, REPL_ID } +import { IDebugService, State, IDebugSession, IThread, IEnablement, IBreakpoint, IStackFrame, REPL_ID, IConfig } from 'vs/workbench/parts/debug/common/debug'; import { Variable, Expression, Thread, Breakpoint } from 'vs/workbench/parts/debug/common/debugModel'; import { IPartService } from 'vs/workbench/services/part/common/partService'; @@ -133,7 +133,13 @@ export class StartAction extends AbstractDebugAction { this.toDispose.push(this.contextService.onDidChangeWorkbenchState(() => this.updateEnablement())); } - public run(): Thenable { + // Note: When this action is executed from the process explorer, a config is passed. For all + // other cases it is run with no arguments. + public run(config?: IConfig): Thenable { + if (config) { + return this.debugService.startDebugging(undefined, config, this.isNoDebug()); + } + const configurationManager = this.debugService.getConfigurationManager(); let launch = configurationManager.selectedConfiguration.launch; if (!launch || launch.getConfigurationNames().length === 0) { diff --git a/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts b/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts index a350791401274..32dafae8b892b 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts @@ -10,7 +10,7 @@ import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { Registry } from 'vs/platform/registry/common/platform'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { KeybindingWeight, IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeybindingWeight, IKeybindings, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { IWorkbenchActionRegistry, Extensions as WorkbenchActionRegistryExtensions } from 'vs/workbench/common/actions'; import { ShowViewletAction, Extensions as ViewletExtensions, ViewletRegistry, ViewletDescriptor } from 'vs/workbench/browser/viewlet'; @@ -49,11 +49,13 @@ import { DebugViewlet } from 'vs/workbench/parts/debug/browser/debugViewlet'; import { Repl, ClearReplAction } from 'vs/workbench/parts/debug/electron-browser/repl'; import { DebugQuickOpenHandler } from 'vs/workbench/parts/debug/browser/debugQuickOpen'; import { DebugStatus } from 'vs/workbench/parts/debug/browser/debugStatus'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase, ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { launchSchemaId } from 'vs/workbench/services/configuration/common/configuration'; import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; import { LoadedScriptsView } from 'vs/workbench/parts/debug/browser/loadedScriptsView'; import { TOGGLE_LOG_POINT_ID, TOGGLE_CONDITIONAL_BREAKPOINT_ID, TOGGLE_BREAKPOINT_ID } from 'vs/workbench/parts/debug/browser/debugEditorActions'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; class OpenDebugViewletAction extends ShowViewletAction { public static readonly ID = VIEWLET_ID; @@ -129,8 +131,59 @@ Registry.as(WorkbenchExtensions.Workbench).regi Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(StatusBarColorProvider, LifecyclePhase.Eventually); const debugCategory = nls.localize('debugCategory', "Debug"); -registry.registerWorkbenchAction(new SyncActionDescriptor( - StartAction, StartAction.ID, StartAction.LABEL, { primary: KeyCode.F5 }, CONTEXT_IN_DEBUG_MODE.toNegated()), 'Debug: Start Debugging', debugCategory); + +const startDebugDescriptor = new SyncActionDescriptor(StartAction, StartAction.ID, StartAction.LABEL, { primary: KeyCode.F5 }, CONTEXT_IN_DEBUG_MODE.toNegated()); + +function startDebugHandler(accessor, args): Promise { + const notificationService = accessor.get(INotificationService); + const instantiationService = accessor.get(IInstantiationService); + const lifecycleService = accessor.get(ILifecycleService); + + return Promise.resolve(lifecycleService.when(LifecyclePhase.Ready).then(() => { + const actionInstance = instantiationService.createInstance(startDebugDescriptor.syncDescriptor); + try { + // don't run the action when not enabled + if (!actionInstance.enabled) { + actionInstance.dispose(); + + return void 0; + } + + const from = args && args.from || 'keybinding'; + + if (args) { + delete args.from; + } + + return Promise.resolve(actionInstance.run(args, { from })).then(() => { + actionInstance.dispose(); + }, err => { + actionInstance.dispose(); + + return Promise.reject(err); + }); + } catch (err) { + actionInstance.dispose(); + + return Promise.reject(err); + } + })).then(null, err => notificationService.error(err)); +} + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: StartAction.ID, + weight: KeybindingWeight.WorkbenchContrib, + when: CONTEXT_IN_DEBUG_MODE.toNegated(), + primary: KeyCode.F5, + handler: startDebugHandler +}); + +MenuRegistry.addCommand({ + id: StartAction.ID, + title: StartAction.LABEL, + category: debugCategory +}); + registry.registerWorkbenchAction(new SyncActionDescriptor(StepOverAction, StepOverAction.ID, StepOverAction.LABEL, { primary: KeyCode.F10 }, CONTEXT_IN_DEBUG_MODE), 'Debug: Step Over', debugCategory); registry.registerWorkbenchAction(new SyncActionDescriptor(StepIntoAction, StepIntoAction.ID, StepIntoAction.LABEL, { primary: KeyCode.F11 }, CONTEXT_IN_DEBUG_MODE, KeybindingWeight.WorkbenchContrib + 1), 'Debug: Step Into', debugCategory); registry.registerWorkbenchAction(new SyncActionDescriptor(StepOutAction, StepOutAction.ID, StepOutAction.LABEL, { primary: KeyMod.Shift | KeyCode.F11 }, CONTEXT_IN_DEBUG_MODE), 'Debug: Step Out', debugCategory);