/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/processExplorer'; import 'vs/base/browser/ui/codicons/codiconStyles'; // make sure codicon css is loaded import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { NativeHostService } from 'vs/platform/native/electron-sandbox/nativeHostService'; import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { localize } from 'vs/nls'; import { ProcessExplorerStyles, ProcessExplorerData } from 'vs/platform/issue/common/issue'; import { applyZoom, zoomIn, zoomOut } from 'vs/platform/windows/electron-sandbox/window'; import { IContextMenuItem } from 'vs/base/parts/contextmenu/common/contextmenu'; import { popup } from 'vs/base/parts/contextmenu/electron-sandbox/contextmenu'; import { ProcessItem } from 'vs/base/common/processes'; import * as dom from 'vs/base/browser/dom'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { isRemoteDiagnosticError, IRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics'; import { ElectronIPCMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; import { ByteSize } from 'vs/platform/files/common/files'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; import { DataTree } from 'vs/base/browser/ui/tree/dataTree'; const DEBUG_FLAGS_PATTERN = /\s--(inspect|debug)(-brk|port)?=(\d+)?/; const DEBUG_PORT_PATTERN = /\s--(inspect|debug)-port=(\d+)/; class ProcessListDelegate implements IListVirtualDelegate { getHeight(element: MachineProcessInformation | ProcessItem | IRemoteDiagnosticError) { return 22; } getTemplateId(element: ProcessInformation | MachineProcessInformation | ProcessItem | IRemoteDiagnosticError) { if (isProcessItem(element)) { return 'process'; } if (isMachineProcessInformation(element)) { return 'machine'; } if (isRemoteDiagnosticError(element)) { return 'error'; } if (isProcessInformation(element)) { return 'header'; } return ''; } } interface IProcessItemTemplateData extends IProcessRowTemplateData { CPU: HTMLElement; memory: HTMLElement; PID: HTMLElement; } interface IProcessRowTemplateData { name: HTMLElement; } class ProcessTreeDataSource implements IDataSource { hasChildren(element: ProcessTree | ProcessInformation | MachineProcessInformation | ProcessItem | IRemoteDiagnosticError): boolean { if (isRemoteDiagnosticError(element)) { return false; } if (isProcessItem(element)) { return !!element.children?.length; } else { return true; } } getChildren(element: ProcessTree | ProcessInformation | MachineProcessInformation | ProcessItem | IRemoteDiagnosticError) { if (isProcessItem(element)) { return element.children ? element.children : []; } if (isRemoteDiagnosticError(element)) { return []; } if (isProcessInformation(element)) { // If there are multiple process roots, return these, otherwise go directly to the root process if (element.processRoots.length > 1) { return element.processRoots; } else { return [element.processRoots[0].rootProcess]; } } if (isMachineProcessInformation(element)) { return [element.rootProcess]; } return [element.processes]; } } class ProcessHeaderTreeRenderer implements ITreeRenderer { templateId: string = 'header'; renderTemplate(container: HTMLElement): IProcessItemTemplateData { const data = Object.create(null); const row = dom.append(container, dom.$('.row')); data.name = dom.append(row, dom.$('.nameLabel')); data.CPU = dom.append(row, dom.$('.cpu')); data.memory = dom.append(row, dom.$('.memory')); data.PID = dom.append(row, dom.$('.pid')); return data; } renderElement(node: ITreeNode, index: number, templateData: IProcessItemTemplateData, height: number | undefined): void { templateData.name.textContent = localize('name', "Process Name"); templateData.CPU.textContent = localize('cpu', "CPU %"); templateData.PID.textContent = localize('pid', "PID"); templateData.memory.textContent = localize('memory', "Memory (MB)"); } disposeTemplate(templateData: any): void { // Nothing to do } } class MachineRenderer implements ITreeRenderer { templateId: string = 'machine'; renderTemplate(container: HTMLElement): IProcessRowTemplateData { const data = Object.create(null); const row = dom.append(container, dom.$('.row')); data.name = dom.append(row, dom.$('.nameLabel')); return data; } renderElement(node: ITreeNode, index: number, templateData: IProcessRowTemplateData, height: number | undefined): void { templateData.name.textContent = node.element.name; } disposeTemplate(templateData: IProcessRowTemplateData): void { // Nothing to do } } class ErrorRenderer implements ITreeRenderer { templateId: string = 'error'; renderTemplate(container: HTMLElement): IProcessRowTemplateData { const data = Object.create(null); const row = dom.append(container, dom.$('.row')); data.name = dom.append(row, dom.$('.nameLabel')); return data; } renderElement(node: ITreeNode, index: number, templateData: IProcessRowTemplateData, height: number | undefined): void { templateData.name.textContent = node.element.errorMessage; } disposeTemplate(templateData: IProcessRowTemplateData): void { // Nothing to do } } class ProcessRenderer implements ITreeRenderer { constructor(private platform: string, private totalMem: number, private mapPidToWindowTitle: Map) { } templateId: string = 'process'; renderTemplate(container: HTMLElement): IProcessItemTemplateData { const data = Object.create(null); const row = dom.append(container, dom.$('.row')); data.name = dom.append(row, dom.$('.nameLabel')); data.CPU = dom.append(row, dom.$('.cpu')); data.memory = dom.append(row, dom.$('.memory')); data.PID = dom.append(row, dom.$('.pid')); return data; } renderElement(node: ITreeNode, index: number, templateData: IProcessItemTemplateData, height: number | undefined): void { const { element } = node; let name = element.name; if (name === 'window') { const windowTitle = this.mapPidToWindowTitle.get(element.pid); name = windowTitle !== undefined ? `${name} (${this.mapPidToWindowTitle.get(element.pid)})` : name; } templateData.name.textContent = name; templateData.name.title = element.cmd; templateData.CPU.textContent = element.load.toFixed(0); templateData.PID.textContent = element.pid.toFixed(0); const memory = this.platform === 'win32' ? element.mem : (this.totalMem * (element.mem / 100)); templateData.memory.textContent = (memory / ByteSize.MB).toFixed(0); } disposeTemplate(templateData: IProcessItemTemplateData): void { // Nothing to do } } interface MachineProcessInformation { name: string; rootProcess: ProcessItem | IRemoteDiagnosticError } interface ProcessInformation { processRoots: MachineProcessInformation[]; } interface ProcessTree { processes: ProcessInformation; } function isMachineProcessInformation(item: any): item is MachineProcessInformation { return !!item.name && !!item.rootProcess; } function isProcessInformation(item: any): item is ProcessInformation { return !!item.processRoots; } function isProcessItem(item: any): item is ProcessItem { return !!item.pid; } class ProcessExplorer { private lastRequestTime: number; private mapPidToWindowTitle = new Map(); private listeners = new DisposableStore(); private nativeHostService: INativeHostService; private tree: DataTree | undefined; constructor(windowId: number, private data: ProcessExplorerData) { const mainProcessService = new ElectronIPCMainProcessService(windowId); this.nativeHostService = new NativeHostService(windowId, mainProcessService) as INativeHostService; this.applyStyles(data.styles); // Map window process pids to titles, annotate process names with this when rendering to distinguish between them ipcRenderer.on('vscode:windowsInfoResponse', (event: unknown, windows: any[]) => { this.mapPidToWindowTitle = new Map(); windows.forEach(window => this.mapPidToWindowTitle.set(window.pid, window.title)); }); ipcRenderer.on('vscode:listProcessesResponse', async (event: unknown, processRoots: MachineProcessInformation[]) => { processRoots.forEach((info, index) => { if (isProcessItem(info.rootProcess)) { info.rootProcess.name = index === 0 ? `${this.data.applicationName} main` : 'remote agent'; } }); if (!this.tree) { await this.createProcessTree(processRoots); } else { this.tree.setInput({ processes: { processRoots } }); } this.requestProcessList(0); }); this.lastRequestTime = Date.now(); ipcRenderer.send('vscode:windowsInfoRequest'); ipcRenderer.send('vscode:listProcesses'); } private async createProcessTree(processRoots: MachineProcessInformation[]): Promise { const container = document.getElementById('process-list'); if (!container) { return; } const { totalmem } = await this.nativeHostService.getOSStatistics(); const renderers = [ new ProcessRenderer(this.data.platform, totalmem, this.mapPidToWindowTitle), new ProcessHeaderTreeRenderer(), new MachineRenderer(), new ErrorRenderer() ]; this.tree = new DataTree('processExplorer', container, new ProcessListDelegate(), renderers, new ProcessTreeDataSource(), { identityProvider: { getId: (element: ProcessTree | ProcessItem | MachineProcessInformation | ProcessInformation | IRemoteDiagnosticError) => { if (isProcessItem(element)) { return element.pid.toString(); } if (isRemoteDiagnosticError(element)) { return element.hostName; } if (isProcessInformation(element)) { return 'processes'; } if (isMachineProcessInformation(element)) { return element.name; } return 'header'; } } }); this.tree.setInput({ processes: { processRoots } }); this.tree.layout(window.innerHeight, window.innerWidth); this.tree.onContextMenu(e => { if (isProcessItem(e.element)) { this.showContextMenu(e.element, true); } }); } private 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; } private 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: 'debug.startFromConfig', from: 'processExplorer', args: [config] }); } private applyStyles(styles: ProcessExplorerStyles): void { const styleTag = document.createElement('style'); const content: string[] = []; if (styles.hoverBackground) { content.push(`.monaco-list-row:hover { background-color: ${styles.hoverBackground}; }`); } if (styles.hoverForeground) { content.push(`.monaco-list-row:hover { color: ${styles.hoverForeground}; }`); } styleTag.textContent = content.join('\n'); if (document.head) { document.head.appendChild(styleTag); } if (styles.color) { document.body.style.color = styles.color; } } private showContextMenu(item: ProcessItem, isLocal: boolean) { const items: IContextMenuItem[] = []; const pid = Number(item.pid); if (isLocal) { items.push({ label: localize('killProcess', "Kill Process"), click: () => { this.nativeHostService.killProcess(pid, 'SIGTERM'); } }); items.push({ label: localize('forceKillProcess', "Force Kill Process"), click: () => { this.nativeHostService.killProcess(pid, 'SIGKILL'); } }); items.push({ type: 'separator' }); } items.push({ label: localize('copy', "Copy"), click: () => { const row = document.getElementById(pid.toString()); if (row) { this.nativeHostService.writeClipboardText(row.innerText); } } }); items.push({ label: localize('copyAll', "Copy All"), click: () => { const processList = document.getElementById('process-list'); if (processList) { this.nativeHostService.writeClipboardText(processList.innerText); } } }); if (item && isLocal && this.isDebuggable(item.cmd)) { items.push({ type: 'separator' }); items.push({ label: localize('debug', "Debug"), click: () => { this.attachTo(item); } }); } popup(items); } private requestProcessList(totalWaitTime: number): void { setTimeout(() => { const nextRequestTime = Date.now(); const waited = totalWaitTime + nextRequestTime - this.lastRequestTime; this.lastRequestTime = nextRequestTime; // Wait at least a second between requests. if (waited > 1000) { ipcRenderer.send('vscode:windowsInfoRequest'); ipcRenderer.send('vscode:listProcesses'); } else { this.requestProcessList(waited); } }, 200); } public dispose() { this.listeners.dispose(); } } export function startup(windowId: number, data: ProcessExplorerData): void { const platformClass = data.platform === 'win32' ? 'windows' : data.platform === 'linux' ? 'linux' : 'mac'; document.body.classList.add(platformClass); // used by our fonts applyZoom(data.zoomLevel); const processExplorer = new ProcessExplorer(windowId, data); document.onkeydown = (e: KeyboardEvent) => { const cmdOrCtrlKey = data.platform === 'darwin' ? e.metaKey : e.ctrlKey; // Cmd/Ctrl + w closes issue window if (cmdOrCtrlKey && e.keyCode === 87) { e.stopPropagation(); e.preventDefault(); processExplorer.dispose(); ipcRenderer.send('vscode:closeProcessExplorer'); } // Cmd/Ctrl + zooms in if (cmdOrCtrlKey && e.keyCode === 187) { zoomIn(); } // Cmd/Ctrl - zooms out if (cmdOrCtrlKey && e.keyCode === 189) { zoomOut(); } }; }