From 307987fe383950b8fa50c150f7d345e353a8544b Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 29 Nov 2017 12:17:26 +0100 Subject: [PATCH] Add code --ps (#39302) * initial commit * :lipstick: * get the machine id from the main process * first cut formatting * nicer output * added support for Windows * refactor to share common code * simplify regexps * always use the 'type' argument * differentiate between node and electron_node * some polish * add render id to renderer * add memory load (macOS, linux) * :lipstick: --- src/vs/base/node/ps.ts | 167 ++++++++++++++++++ src/vs/code/electron-main/main.ts | 41 ++++- src/vs/code/node/cli.ts | 28 ++- .../environment/common/environment.ts | 1 + src/vs/platform/environment/node/argv.ts | 4 +- 5 files changed, 230 insertions(+), 11 deletions(-) create mode 100644 src/vs/base/node/ps.ts diff --git a/src/vs/base/node/ps.ts b/src/vs/base/node/ps.ts new file mode 100644 index 00000000000..f863e55c3cd --- /dev/null +++ b/src/vs/base/node/ps.ts @@ -0,0 +1,167 @@ +/*--------------------------------------------------------------------------------------------- + * 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, exec } from 'child_process'; + +export interface ProcessItem { + name: string; + cmd: string; + pid: number; + ppid: number; + load: number; + mem: number; + + children?: ProcessItem[]; +} + +export function listProcesses(rootPid: number): Promise { + + return new Promise((resolve, reject) => { + + let rootItem: ProcessItem; + const map = new Map(); + + function addToTree(pid: number, ppid: number, cmd: string, load: number, mem: number) { + + const parent = map.get(ppid); + if (pid === rootPid || parent) { + + const item: ProcessItem = { + name: findName(cmd), + cmd, + pid, + ppid, + load, + mem + }; + map.set(pid, item); + + if (pid === rootPid) { + rootItem = item; + } + + if (parent) { + if (!parent.children) { + parent.children = []; + } + parent.children.push(item); + if (parent.children.length > 1) { + parent.children = parent.children.sort((a, b) => a.pid - b.pid); + } + } + } + } + + function findName(cmd: string): string { + + const RENDERER_PROCESS_HINT = /--disable-blink-features=Auxclick/; + const WINDOWS_WATCHER_HINT = /\\watcher\\win32\\CodeHelper.exe/; + const TYPE = /--type=([a-zA-Z-]+)/; + + // find windows file watcher + if (WINDOWS_WATCHER_HINT.exec(cmd)) { + return 'watcherService'; + } + + // find "--type=xxxx" + let matches = TYPE.exec(cmd); + if (matches && matches.length === 2) { + if (matches[1] === 'renderer') { + if (!RENDERER_PROCESS_HINT.exec(cmd)) { + return 'shared-process'; + } else { + const RID = /--renderer-client-id=([0-9]+)/; + matches = RID.exec(cmd); + if (matches && matches.length === 2) { + return `renderer-${matches[1]}`; + } + } + } + return matches[1]; + } + + // find all xxxx.js + const JS = /[a-zA-Z-]+\.js/g; + let result = ''; + do { + matches = JS.exec(cmd); + if (matches) { + result += matches + ' '; + } + } while (matches); + + if (result) { + if (cmd.indexOf('node ') !== 0) { + return `electron_node ${result}`; + } + } + return cmd; + } + + if (process.platform === 'win32') { + + const CMD = 'wmic process get ProcessId,ParentProcessId,CommandLine \n'; + const CMD_PID = /^(.+)\s+([0-9]+)\s+([0-9]+)$/; + + let stdout = ''; + let stderr = ''; + + const cmd = spawn('cmd'); + + cmd.stdout.on('data', data => { + stdout += data.toString(); + }); + cmd.stderr.on('data', data => { + stderr += data.toString(); + }); + + cmd.on('exit', () => { + + if (stderr.length > 0) { + reject(stderr); + } else { + + const lines = stdout.split('\r\n'); + for (const line of lines) { + let matches = CMD_PID.exec(line.trim()); + if (matches && matches.length === 4) { + addToTree(parseInt(matches[3]), parseInt(matches[2]), matches[1].trim(), 0.0, 0.0); + } + } + + resolve(rootItem); + } + }); + + cmd.stdin.write(CMD); + cmd.stdin.end(); + + } else { // OS X & Linux + + const CMD = 'ps -ax -o pid=,ppid=,pcpu=,pmem=,command='; + const PID_CMD = /^\s*([0-9]+)\s+([0-9]+)\s+([0-9]+\.[0-9]+)\s+([0-9]+\.[0-9]+)\s+(.+)$/; + + exec(CMD, { maxBuffer: 1000 * 1024 }, (err, stdout, stderr) => { + + if (err || stderr) { + reject(err || stderr.toString()); + } else { + + const lines = stdout.toString().split('\n'); + for (const line of lines) { + let matches = PID_CMD.exec(line.trim()); + if (matches && matches.length === 6) { + addToTree(parseInt(matches[1]), parseInt(matches[2]), matches[5], parseFloat(matches[3]), parseFloat(matches[4])); + } + } + + resolve(rootItem); + } + }); + } + }); +} diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 920dfdaa1a4..023357bbf3d 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -41,6 +41,8 @@ import { WorkspacesMainService } from 'vs/platform/workspaces/electron-main/work import { IWorkspacesMainService } from 'vs/platform/workspaces/common/workspaces'; import { localize } from 'vs/nls'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; +import { listProcesses, ProcessItem } from 'vs/base/node/ps'; +import { repeat } from 'vs/base/common/strings'; function createServices(args: ParsedArgs): IInstantiationService { const services = new ServiceCollection(); @@ -101,6 +103,11 @@ function setupIPC(accessor: ServicesAccessor): TPromise { app.dock.show(); // dock might be hidden at this case due to a retry } + // Print --ps usage info + if (environmentService.args.ps) { + console.log('Warning: The --ps argument can only be used if Code is already running. Please run it again after Code has started.'); + } + return server; }, err => { if (err.code !== 'EADDRINUSE') { @@ -125,8 +132,6 @@ function setupIPC(accessor: ServicesAccessor): TPromise { return TPromise.wrapError(new Error(msg)); } - logService.info('Sending env to running instance...'); - // Show a warning dialog after some timeout if it takes long to talk to the other instance // Skip this if we are running with --wait where it is expected that we wait for a while let startupWarningDialogHandle: number; @@ -142,6 +147,21 @@ function setupIPC(accessor: ServicesAccessor): TPromise { const channel = client.getChannel('launch'); const service = new LaunchChannelClient(channel); + // Process Info + if (environmentService.args.ps) { + return service.getMainProcessId().then(mainProcessPid => { + return listProcesses(mainProcessPid).then(rootProcess => { + const output: string[] = []; + formatProcess(output, rootProcess, 0); + console.log(output.join('\n')); + + return TPromise.wrapError(new ExpectedError()); + }); + }); + } + + logService.info('Sending env to running instance...'); + return allowSetForegroundWindow(service) .then(() => service.start(environmentService.args, process.env)) .then(() => client.dispose()) @@ -186,6 +206,23 @@ function setupIPC(accessor: ServicesAccessor): TPromise { return setup(true); } +function formatProcess(output: string[], item: ProcessItem, indent: number): void { + + // Format name with indent + let name: string; + if (indent === 0) { + name = `${product.applicationName} main`; + } else { + name = `${repeat(' ', indent)} ${item.name}`; + } + output.push(name); + + // Recurse into children if any + if (Array.isArray(item.children)) { + item.children.forEach(child => formatProcess(output, child, indent + 1)); + } +} + function showStartupWarningDialog(message: string, detail: string): void { dialog.showMessageBox(null, { title: product.nameLong, diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index b0f83200890..293855d91bc 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -37,14 +37,24 @@ export async function main(argv: string[]): TPromise { return TPromise.as(null); } + // Help if (args.help) { console.log(buildHelpMessage(product.nameLong, product.applicationName, pkg.version)); - } else if (args.version) { + } + + // Version Info + else if (args.version) { console.log(`${pkg.version}\n${product.commit}\n${process.arch}`); - } else if (shouldSpawnCliProcess(args)) { + } + + // Extensions Management + else if (shouldSpawnCliProcess(args)) { const mainCli = new TPromise(c => require(['vs/code/node/cliProcessMain'], c)); return mainCli.then(cli => cli.main(args)); - } else { + } + + // Just Code + else { const env = assign({}, process.env, { // this will signal Code that it was spawned from this module 'VSCODE_CLI': '1', @@ -55,7 +65,9 @@ export async function main(argv: string[]): TPromise { let processCallbacks: ((child: ChildProcess) => Thenable)[] = []; - if (args.verbose) { + const verbose = args.verbose || args.ps; + + if (verbose) { env['ELECTRON_ENABLE_LOGGING'] = '1'; processCallbacks.push(child => { @@ -68,7 +80,7 @@ export async function main(argv: string[]): TPromise { // If we are running with input from stdin, pipe that into a file and // open this file via arguments. Ignore this when we are passed with - // paths to open. + // paths to open. let isReadingFromStdin: boolean; try { isReadingFromStdin = args._.length === 0 && !process.stdin.isTTY; // Via https://twitter.com/MylesBorins/status/782009479382626304 @@ -97,7 +109,7 @@ export async function main(argv: string[]): TPromise { stdinFileError = error; } - if (args.verbose) { + if (verbose) { if (stdinFileError) { console.error(`Failed to create file to read via stdin: ${stdinFileError.toString()}`); } else { @@ -122,7 +134,7 @@ export async function main(argv: string[]): TPromise { waitMarkerError = error; } - if (args.verbose) { + if (verbose) { if (waitMarkerError) { console.error(`Failed to create marker file for --wait: ${waitMarkerError.toString()}`); } else { @@ -195,7 +207,7 @@ export async function main(argv: string[]): TPromise { env }; - if (!args.verbose) { + if (!verbose) { options['stdio'] = 'ignore'; } diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index e32a25f3362..ef5353ab958 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -48,6 +48,7 @@ export interface ParsedArgs { 'disable-updates'?: string; 'disable-crash-reporter'?: string; 'skip-add-to-recently-opened'?: boolean; + 'ps'?: boolean; } export const IEnvironmentService = createDecorator('environmentService'); diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 2a961e11f27..1f8755477f5 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -52,7 +52,8 @@ const options: minimist.Opts = { 'disable-telemetry', 'disable-updates', 'disable-crash-reporter', - 'skip-add-to-recently-opened' + 'skip-add-to-recently-opened', + 'ps' ], alias: { add: 'a', @@ -146,6 +147,7 @@ export const optionsHelp: { [name: string]: string; } = { '--enable-proposed-api ': localize('experimentalApis', "Enables proposed api features for an extension."), '--disable-extensions': localize('disableExtensions', "Disable all installed extensions."), '--disable-gpu': localize('disableGPU', "Disable GPU hardware acceleration."), + '--ps': localize('ps', "Print process usage and diagnostics information."), '-v, --version': localize('version', "Print version."), '-h, --help': localize('help', "Print usage.") }; -- GitLab