@applitools/utils
Version:
157 lines (156 loc) • 6.62 kB
JavaScript
;
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);
}