@j-o-r/sh
Version:
Execute shell commands on Linux-based systems from javascript
162 lines (155 loc) • 4.33 kB
JavaScript
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;