UNPKG

@applitools/utils

Version:
157 lines (156 loc) 6.62 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.sampleProcessTreeMemory = exports.execute = exports.sh = exports.executeProcess = exports.executeAndControlProcess = void 0; const child_process_1 = require("child_process"); const fs_1 = require("fs"); const util_1 = require("util"); const process_tree_1 = require("./process-tree"); const execFileAsync = (0, util_1.promisify)(child_process_1.execFile); const PROCESS_TABLE_EXEC_OPTS = { timeout: 10000, maxBuffer: 16 * 1024 * 1024, windowsHide: true }; function makeError(error, properties) { if (typeof error === 'string') { error = new Error(error); } if (!properties) return error; return Object.assign(error, properties); } function executeAndControlProcess(command, args = [], options) { const subProcess = (0, child_process_1.spawn)(command, args, { stdio: 'pipe', ...options === null || options === void 0 ? void 0 : options.spawnOptions, }); const exitPromise = new Promise((resolve, reject) => { subProcess.on('error', reject).on('close', (exitCode, signal) => exitCode === 0 ? resolve({ exitCode, stdout, stderr }) : signal ? reject(makeError(new Error(`process exited due to signal ${signal} executing process ${command} with args ${JSON.stringify(args)}`), { signal, stdout, stderr, })) : reject(makeError(new Error(`non-zero exit code (${exitCode}) executing process ${command} with args ${JSON.stringify(args)}`), { exitCode, stdout, stderr, }))); let stdout = subProcess.stdout ? '' : undefined; let stderr = subProcess.stderr ? '' : undefined; subProcess.stdout && subProcess.stdout.on('data', data => (stdout += data.toString())); subProcess.stderr && subProcess.stderr.on('data', data => (stderr += data.toString())); if (options === null || options === void 0 ? void 0 : options.timeout) { setTimeout(() => subProcess.kill(), options.timeout); } return { stdout, stderr }; }); return { subProcess, exitPromise }; } exports.executeAndControlProcess = executeAndControlProcess; async function executeProcess(command, args = [], options) { return await executeAndControlProcess(command, args, options).exitPromise; } exports.executeProcess = executeProcess; async function sh(command, options) { let shell; if (process.platform === 'win32') { shell = 'C:\\Program Files\\Git\\bin\\bash.exe'; } else if ((0, fs_1.existsSync)('/bin/bash')) { shell = '/bin/bash'; } else { shell = '/bin/sh'; } return await executeProcess(command, [], { ...options, spawnOptions: { stdio: 'inherit', shell, ...options === null || options === void 0 ? void 0 : options.spawnOptions, }, }); } exports.sh = sh; async function execute(command, options) { return new Promise(resolve => { (0, child_process_1.exec)(command, options, (error, stdout, stderr) => { if (error) resolve({ stdout: stdout.toString('utf8'), stderr: stderr.toString('utf8'), code: error.code }); resolve({ stdout: stdout.toString('utf8'), stderr: stderr.toString('utf8'), code: 0 }); }); }); } exports.execute = execute; /** * Best-effort resident memory (bytes) of `rootPid` and all of its descendant * processes — approximates the real RAM a multi-process app (e.g. a Chromium * browser: browser + renderers + GPU) consumes, which the V8 JS heap does NOT. * * Purely additive observability: resolves to `null` on ANY failure (unsupported * platform, parse/permission error, missing tools, root process already gone). * Never throws to the caller. Browser builds always resolve `null`. */ async function sampleProcessTreeMemory(rootPid, options = {}) { var _a; if (!rootPid || !Number.isInteger(rootPid)) return null; try { const table = await getProcessTable(); if (!table || table.size === 0) return null; // root gone (e.g. browser already closed) → report "unknown", not a misleading 0 if (!table.has(rootPid)) return null; return (0, process_tree_1.sumTreeRss)(table, rootPid); } catch (err) { (_a = options.logger) === null || _a === void 0 ? void 0 : _a.log(`[process-tree-memory] failed to sample: ${err && err.message}`); return null; } } exports.sampleProcessTreeMemory = sampleProcessTreeMemory; async function getProcessTable() { switch (process.platform) { case 'linux': return getProcessTableLinux(); case 'win32': return getProcessTableWindows(); case 'darwin': return getProcessTablePosixPs(); default: // best-effort: assume a posix-like `ps` on any other platform return getProcessTablePosixPs(); } } // Linux: read /proc/<pid>/status for PPid + VmRSS. No subprocess spawn. async function getProcessTableLinux() { const table = new Map(); const entries = await fs_1.promises.readdir('/proc'); await Promise.all(entries.map(async (entry) => { const pid = Number(entry); if (!Number.isInteger(pid) || pid <= 0) return; try { const parsed = (0, process_tree_1.parseLinuxStatus)(await fs_1.promises.readFile(`/proc/${pid}/status`, 'utf8')); if (parsed) table.set(pid, parsed); } catch { // process may have exited between readdir and readFile — ignore it } })); return table; } // macOS / other posix: `ps -A -o pid=,ppid=,rss=` (rss in kB). async function getProcessTablePosixPs() { const { stdout } = await execFileAsync('ps', ['-A', '-o', 'pid=,ppid=,rss='], PROCESS_TABLE_EXEC_OPTS); return (0, process_tree_1.parsePosixPsOutput)(stdout); } // Windows: PowerShell + CIM (WorkingSetSize is bytes). Works where wmic is removed. async function getProcessTableWindows() { const psCommand = 'Get-CimInstance Win32_Process | ' + 'Select-Object ProcessId,ParentProcessId,WorkingSetSize | ' + 'ConvertTo-Json -Compress'; const { stdout } = await execFileAsync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', psCommand], PROCESS_TABLE_EXEC_OPTS); return (0, process_tree_1.parseWindowsCimJson)(stdout); }