/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/processExplorer'; import { clipboard } from 'electron'; import { totalmem } from 'os'; import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import product from 'vs/platform/product/common/product'; 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 * as platform from 'vs/base/common/platform'; 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 { addDisposableListener, addClass } from 'vs/base/browser/dom'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { isRemoteDiagnosticError, IRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics'; let mapPidToWindowTitle = new Map(); const DEBUG_FLAGS_PATTERN = /\s--(inspect|debug)(-brk|port)?=(\d+)?/; const DEBUG_PORT_PATTERN = /\s--(inspect|debug)-port=(\d+)/; const listeners = new DisposableStore(); const collapsedStateCache: Map = new Map(); let lastRequestTime: number; interface FormattedProcessItem { cpu: number; memory: number; pid: string; name: string; formattedName: string; cmd: string; } function getProcessList(rootProcess: ProcessItem, isLocal: boolean): FormattedProcessItem[] { const processes: FormattedProcessItem[] = []; if (rootProcess) { getProcessItem(processes, rootProcess, 0, isLocal); } return processes; } function getProcessItem(processes: FormattedProcessItem[], item: ProcessItem, indent: number, isLocal: boolean): void { const isRoot = (indent === 0); const MB = 1024 * 1024; let name = item.name; if (isRoot) { name = isLocal ? `${product.applicationName} main` : 'remote agent'; } if (name === 'window') { const windowTitle = mapPidToWindowTitle.get(item.pid); name = windowTitle !== undefined ? `${name} (${mapPidToWindowTitle.get(item.pid)})` : name; } // Format name with indent const formattedName = isRoot ? name : `${' '.repeat(indent)} ${name}`; const memory = process.platform === 'win32' ? item.mem : (totalmem() * (item.mem / 100)); processes.push({ cpu: item.load, memory: (memory / MB), pid: item.pid.toFixed(0), name, formattedName, cmd: item.cmd }); // Recurse into children if any if (Array.isArray(item.children)) { item.children.forEach(child => { if (child) { getProcessItem(processes, child, indent + 1, isLocal); } }); } } 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: FormattedProcessItem) { 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] }); } function getProcessIdWithHighestProperty(processList: any[], propertyName: string) { let max = 0; let maxProcessId; processList.forEach(process => { if (process[propertyName] > max) { max = process[propertyName]; maxProcessId = process.pid; } }); return maxProcessId; } function updateSectionCollapsedState(shouldExpand: boolean, body: HTMLElement, twistie: HTMLImageElement, sectionName: string) { if (shouldExpand) { body.classList.remove('hidden'); collapsedStateCache.set(sectionName, false); twistie.src = './media/expanded.svg'; } else { body.classList.add('hidden'); collapsedStateCache.set(sectionName, true); twistie.src = './media/collapsed.svg'; } } function renderProcessFetchError(sectionName: string, errorMessage: string) { const container = document.getElementById('process-list'); if (!container) { return; } const body = document.createElement('tbody'); renderProcessGroupHeader(sectionName, body, container); const errorRow = document.createElement('tr'); const data = document.createElement('td'); data.textContent = errorMessage; data.className = 'error'; data.colSpan = 4; errorRow.appendChild(data); body.appendChild(errorRow); container.appendChild(body); } function renderProcessGroupHeader(sectionName: string, body: HTMLElement, container: HTMLElement) { const headerRow = document.createElement('tr'); const data = document.createElement('td'); data.textContent = sectionName; data.colSpan = 4; headerRow.appendChild(data); const twistie = document.createElement('img'); updateSectionCollapsedState(!collapsedStateCache.get(sectionName), body, twistie, sectionName); data.prepend(twistie); listeners.add(addDisposableListener(data, 'click', (e) => { const isHidden = body.classList.contains('hidden'); updateSectionCollapsedState(isHidden, body, twistie, sectionName); })); container.appendChild(headerRow); } function renderTableSection(sectionName: string, processList: FormattedProcessItem[], renderManySections: boolean, sectionIsLocal: boolean): void { const container = document.getElementById('process-list'); if (!container) { return; } const highestCPUProcess = getProcessIdWithHighestProperty(processList, 'cpu'); const highestMemoryProcess = getProcessIdWithHighestProperty(processList, 'memory'); const body = document.createElement('tbody'); if (renderManySections) { renderProcessGroupHeader(sectionName, body, container); } processList.forEach(p => { const row = document.createElement('tr'); row.id = p.pid.toString(); const cpu = document.createElement('td'); p.pid === highestCPUProcess ? cpu.classList.add('centered', 'highest') : cpu.classList.add('centered'); cpu.textContent = p.cpu.toFixed(0); const memory = document.createElement('td'); p.pid === highestMemoryProcess ? memory.classList.add('centered', 'highest') : memory.classList.add('centered'); memory.textContent = p.memory.toFixed(0); const pid = document.createElement('td'); pid.classList.add('centered'); pid.textContent = p.pid; const name = document.createElement('th'); name.scope = 'row'; name.classList.add('data'); name.title = p.cmd; name.textContent = p.formattedName; row.append(cpu, memory, pid, name); listeners.add(addDisposableListener(row, 'contextmenu', (e) => { showContextMenu(e, p, sectionIsLocal); })); body.appendChild(row); }); container.appendChild(body); } function updateProcessInfo(processLists: [{ name: string, rootProcess: ProcessItem | IRemoteDiagnosticError }]): void { const container = document.getElementById('process-list'); if (!container) { return; } container.innerHTML = ''; listeners.clear(); const tableHead = document.createElement('thead'); tableHead.innerHTML = ` ${localize('cpu', "CPU %")} ${localize('memory', "Memory (MB)")} ${localize('pid', "pid")} ${localize('name', "Name")} `; container.append(tableHead); const hasMultipleMachines = Object.keys(processLists).length > 1; processLists.forEach((remote, i) => { const isLocal = i === 0; if (isRemoteDiagnosticError(remote.rootProcess)) { renderProcessFetchError(remote.name, remote.rootProcess.errorMessage); } else { renderTableSection(remote.name, getProcessList(remote.rootProcess, isLocal), hasMultipleMachines, isLocal); } }); } function applyStyles(styles: ProcessExplorerStyles): void { const styleTag = document.createElement('style'); const content: string[] = []; if (styles.hoverBackground) { content.push(`tbody > tr:hover, table > tr:hover { background-color: ${styles.hoverBackground}; }`); } if (styles.hoverForeground) { content.push(`tbody > tr:hover, table > tr:hover { color: ${styles.hoverForeground}; }`); } if (styles.highlightForeground) { content.push(`.highest { color: ${styles.highlightForeground}; }`); } styleTag.innerHTML = content.join('\n'); if (document.head) { document.head.appendChild(styleTag); } if (styles.color) { document.body.style.color = styles.color; } } function showContextMenu(e: MouseEvent, item: FormattedProcessItem, isLocal: boolean) { e.preventDefault(); const items: IContextMenuItem[] = []; const pid = Number(item.pid); if (isLocal) { items.push({ label: localize('killProcess', "Kill Process"), click() { process.kill(pid, 'SIGTERM'); } }); items.push({ label: localize('forceKillProcess', "Force Kill Process"), click() { process.kill(pid, 'SIGKILL'); } }); items.push({ type: 'separator' }); } items.push({ label: localize('copy', "Copy"), click() { const row = document.getElementById(pid.toString()); if (row) { clipboard.writeText(row.innerText); } } }); items.push({ label: localize('copyAll', "Copy All"), click() { const processList = document.getElementById('process-list'); if (processList) { clipboard.writeText(processList.innerText); } } }); if (item && isLocal && isDebuggable(item.cmd)) { items.push({ type: 'separator' }); items.push({ label: localize('debug', "Debug"), click() { attachTo(item); } }); } popup(items); } function requestProcessList(totalWaitTime: number): void { setTimeout(() => { const nextRequestTime = Date.now(); const waited = totalWaitTime + nextRequestTime - lastRequestTime; lastRequestTime = nextRequestTime; // Wait at least a second between requests. if (waited > 1000) { ipcRenderer.send('vscode:windowsInfoRequest'); ipcRenderer.send('vscode:listProcesses'); } else { requestProcessList(waited); } }, 200); } function createCloseListener(): void { // Cmd/Ctrl + w closes process explorer window.addEventListener('keydown', e => { const cmdOrCtrlKey = platform.isMacintosh ? e.metaKey : e.ctrlKey; if (cmdOrCtrlKey && e.keyCode === 87) { ipcRenderer.send('vscode:closeProcessExplorer'); } }); } export function startup(data: ProcessExplorerData): void { const platformClass = platform.isWindows ? 'windows' : platform.isLinux ? 'linux' : 'mac'; addClass(document.body, platformClass); // used by our fonts applyStyles(data.styles); applyZoom(data.zoomLevel); createCloseListener(); // 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[]) => { mapPidToWindowTitle = new Map(); windows.forEach(window => mapPidToWindowTitle.set(window.pid, window.title)); }); ipcRenderer.on('vscode:listProcessesResponse', (event: unknown, processRoots: [{ name: string, rootProcess: ProcessItem | IRemoteDiagnosticError }]) => { updateProcessInfo(processRoots); requestProcessList(0); }); lastRequestTime = Date.now(); ipcRenderer.send('vscode:windowsInfoRequest'); ipcRenderer.send('vscode:listProcesses'); document.onkeydown = (e: KeyboardEvent) => { const cmdOrCtrlKey = platform.isMacintosh ? e.metaKey : e.ctrlKey; // Cmd/Ctrl + zooms in if (cmdOrCtrlKey && e.keyCode === 187) { zoomIn(); } // Cmd/Ctrl - zooms out if (cmdOrCtrlKey && e.keyCode === 189) { zoomOut(); } }; }