UNPKG

@j-o-r/sh

Version:

Execute shell commands on Linux-based systems from javascript

162 lines (155 loc) 4.33 kB
import { spawnSync, spawn, exec } from 'node:child_process'; /** * Kills a process and all child processes of a given process ID in Linux/Posix. * @param {number} processPid - The process ID. * @param {string|number} signal - Signal to send. * @retruns {Promise<number[]>} array with killed pid numbers */ const killProcesses = (processPid, signal) => { const killed = []; return new Promise((resolve, reject) => { // Command to get child PIDs of the given process const cmd = `pgrep -P ${processPid}`; exec(cmd, (error, stdout, stderr) => { if (error) { reject(error); return; } if (stderr) { reject(new Error(stderr)); return; } const pids = stdout.split(/\r?\n/).filter(pid => pid) || []; // Kill each child process try { for (const pid of pids) { process.kill(parseInt(pid), signal); killed.push(parseInt(pid)); } } catch (err) { reject(err); return; } // Kill the parent process after all child processes have been killed try { process.kill(processPid, signal); killed.push(processPid); } catch (err) { reject(err); return; } resolve(killed); }); }); } class SHExecute { #forcedKill = false; /** * @type {import('child_process').ChildProcess} */ #proc; #prefix = ''; #command = ''; /** @type {import('child_process').SpawnOptions | import('child_process').SpawnSyncOptions} */ #options = {}; #stdout = ''; #stderr = ''; /** * @param {string} command - linux command to be executed * @param {string} prefix - command prefix (bash, sh etc.) * @param {import('child_process').SpawnOptions | import('child_process').SpawnSyncOptions} options */ constructor(command, prefix, options = {}) { this.#prefix = prefix; this.#command = command; this.#options = options; this.#proc = null; this.#stdout = ''; this.#stderr = ''; } /** * @param {string} [payload] - data to write * @retuns {Promise<object>} */ runSync(payload) { if (payload && typeof payload !== 'string') { throw new Error('Argument is not a string'); } /** @type {import('node:child_process').SpawnSyncOptions} */ const options = this.#options; // pipe need to be set on stdin when posting a payload // @ts-ignore if (payload) options['stdio'][0] = 'pipe'; const input = payload || undefined; if (input) { options.input = input } return spawnSync(this.#prefix, [this.#command], options); } /** * @param {string} [payload] - data to write * @retuns {Promise<string>} */ run(payload) { if (payload && typeof payload !== 'string') { throw new Error('Argument is not a string'); } /** @type {import('node:child_process').SpawnOptions} */ const options = this.#options; // pipe need to be set on stdin when posting a payload // @ts-ignore if (payload) options.stdio[0] = 'pipe'; this.#proc = spawn(this.#prefix, [this.#command], options); this.#proc.stdout?.on('data', (data) => { this.#stdout += data; }); this.#proc.stderr?.on('data', (data) => { this.#stderr += data; }); if (payload) { this.#proc.stdin.end(payload); } return new Promise((resolve, reject) => { this.#proc.on('close', (code) => { if (this.#forcedKill) { // Resolve without content resolve(); return; } // Detached does not closes with an exitcode if (code === 0 || code === null || typeof (code) === 'undefined') { resolve(this.#stdout.trim()); } else { reject(new Error(`${code}: ${this.#command} "${this.#stderr.trim()}"`)); } }); this.#proc.on('error', (err) => { reject(err); }); }); } /** * Kill this process and possible child processes * @param {number | string} signal - kill signal * @returns {Promise<number[]>} */ async kill(signal = 'SIGTERM') { if (!this.#proc) throw new Error('Trying to kill a process without creating one.'); if (!this.#proc.pid) throw new Error('The process pid is undefined.'); this.#forcedKill = true; let res = []; try { // Try to kill pid 'childs' res = await killProcesses(this.#proc.pid, signal); } catch (_e) { } if (!res.includes(this.#proc.pid)) { // Kill self if I am not allready killed res.push(this.#proc.pid); // @ts-ignore this.#proc.kill(signal); } this.#proc = undefined; return res; } } export default SHExecute;