@j-o-r/sh 1.1.21 → 1.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/SHExecute.js +59 -17
- package/package.json +1 -1
- package/tmp.js +169 -0
- package/types/SHExecute.d.ts +4 -2
package/lib/SHExecute.js
CHANGED
|
@@ -54,20 +54,32 @@ class SHExecute {
|
|
|
54
54
|
#command = '';
|
|
55
55
|
/** @type {import('child_process').SpawnOptions | import('child_process').SpawnSyncOptions} */
|
|
56
56
|
#options = {};
|
|
57
|
-
#
|
|
58
|
-
#
|
|
57
|
+
#stdoutChunks = [];
|
|
58
|
+
#stderrChunks = [];
|
|
59
|
+
// Track sizes to avoid unbounded memory growth and RangeError on join/toString
|
|
60
|
+
#stdoutLen = 0;
|
|
61
|
+
#stderrLen = 0;
|
|
62
|
+
// Max bytes to buffer in memory (per stream). Remaining data is ignored.
|
|
63
|
+
#maxBuffer = 40 * 1024 * 1024; // default 40MB
|
|
64
|
+
#truncated = { stdout: false, stderr: false };
|
|
59
65
|
/**
|
|
60
66
|
* @param {string} command - linux command to be executed
|
|
61
67
|
* @param {string} prefix - command prefix (bash, sh etc.)
|
|
62
|
-
* @param {import('child_process').SpawnOptions
|
|
68
|
+
* @param {import('child_process').SpawnOptions & { maxBuffer?: number }} options
|
|
63
69
|
*/
|
|
64
70
|
constructor(command, prefix, options = {}) {
|
|
65
71
|
this.#prefix = prefix;
|
|
66
72
|
this.#command = command;
|
|
67
|
-
|
|
73
|
+
// Extract our custom option to avoid passing unknown keys to spawn/spawnSync
|
|
74
|
+
const { maxBuffer, ...spawnOpts } = options ?? {};
|
|
75
|
+
this.#maxBuffer = Number.isFinite(maxBuffer) && maxBuffer > 0 ? maxBuffer : this.#maxBuffer;
|
|
76
|
+
this.#options = spawnOpts;
|
|
68
77
|
this.#proc = null;
|
|
69
|
-
this.#
|
|
70
|
-
this.#
|
|
78
|
+
this.#stdoutChunks = [];
|
|
79
|
+
this.#stderrChunks = [];
|
|
80
|
+
this.#stdoutLen = 0;
|
|
81
|
+
this.#stderrLen = 0;
|
|
82
|
+
this.#truncated = { stdout: false, stderr: false };
|
|
71
83
|
}
|
|
72
84
|
/**
|
|
73
85
|
* @param {string} [payload] - data to write
|
|
@@ -78,10 +90,15 @@ class SHExecute {
|
|
|
78
90
|
throw new Error('Argument is not a string');
|
|
79
91
|
}
|
|
80
92
|
/** @type {import('node:child_process').SpawnSyncOptions} */
|
|
81
|
-
const options = this.#options;
|
|
93
|
+
const options = { ...this.#options };
|
|
82
94
|
// pipe need to be set on stdin when posting a payload
|
|
83
95
|
// @ts-ignore
|
|
84
|
-
if (payload)
|
|
96
|
+
if (payload) {
|
|
97
|
+
// clone/ensure stdio array exists
|
|
98
|
+
const stdio = Array.isArray(options.stdio) ? [...options.stdio] : ['pipe', 'pipe', 'pipe'];
|
|
99
|
+
stdio[0] = 'pipe';
|
|
100
|
+
options.stdio = stdio;
|
|
101
|
+
}
|
|
85
102
|
const input = payload || undefined;
|
|
86
103
|
if (input) {
|
|
87
104
|
options.input = input
|
|
@@ -97,17 +114,37 @@ class SHExecute {
|
|
|
97
114
|
throw new Error('Argument is not a string');
|
|
98
115
|
}
|
|
99
116
|
/** @type {import('node:child_process').SpawnOptions} */
|
|
100
|
-
const options = this.#options;
|
|
117
|
+
const options = { ...this.#options };
|
|
101
118
|
// pipe need to be set on stdin when posting a payload
|
|
102
119
|
// @ts-ignore
|
|
120
|
+
|
|
103
121
|
if (payload) options.stdio[0] = 'pipe';
|
|
122
|
+
|
|
123
|
+
// reset buffers for each run
|
|
124
|
+
this.#stdoutChunks = [];
|
|
125
|
+
this.#stderrChunks = [];
|
|
126
|
+
this.#stdoutLen = 0;
|
|
127
|
+
this.#stderrLen = 0;
|
|
128
|
+
this.#truncated = { stdout: false, stderr: false };
|
|
129
|
+
|
|
104
130
|
this.#proc = spawn(this.#prefix, [this.#command], options);
|
|
105
|
-
this.#proc.stdout?.on('data', (
|
|
106
|
-
this.#
|
|
131
|
+
this.#proc.stdout?.on('data', (chunk) => {
|
|
132
|
+
this.#stdoutLen += chunk.length;
|
|
133
|
+
if (this.#stdoutLen <= this.#maxBuffer) {
|
|
134
|
+
this.#stdoutChunks.push(chunk);
|
|
135
|
+
} else {
|
|
136
|
+
this.#truncated.stdout = true;
|
|
137
|
+
// ignore further data to cap memory; alternatively implement a ring buffer
|
|
138
|
+
}
|
|
107
139
|
});
|
|
108
140
|
|
|
109
|
-
this.#proc.stderr?.on('data', (
|
|
110
|
-
this.#
|
|
141
|
+
this.#proc.stderr?.on('data', (chunk) => {
|
|
142
|
+
this.#stderrLen += chunk.length;
|
|
143
|
+
if (this.#stderrLen <= this.#maxBuffer) {
|
|
144
|
+
this.#stderrChunks.push(chunk);
|
|
145
|
+
} else {
|
|
146
|
+
this.#truncated.stderr = true;
|
|
147
|
+
}
|
|
111
148
|
});
|
|
112
149
|
if (payload) {
|
|
113
150
|
this.#proc.stdin.end(payload);
|
|
@@ -123,14 +160,20 @@ class SHExecute {
|
|
|
123
160
|
this.#proc.on('close', (code) => {
|
|
124
161
|
if (this.#forcedKill) {
|
|
125
162
|
// Resolve without content
|
|
126
|
-
resolve();
|
|
163
|
+
resolve('');
|
|
127
164
|
return;
|
|
128
165
|
}
|
|
129
166
|
// Detached does not closes with an exitcode
|
|
130
167
|
if (code === 0 || code === null || typeof (code) === 'undefined') {
|
|
131
|
-
|
|
168
|
+
const stdoutBuf = this.#stdoutChunks.length ? Buffer.concat(this.#stdoutChunks) : Buffer.alloc(0);
|
|
169
|
+
let stdout = stdoutBuf.toString('utf8').trim();
|
|
170
|
+
if (this.#truncated.stdout) stdout += '\n[stdout truncated]\n';
|
|
171
|
+
resolve(stdout);
|
|
132
172
|
} else {
|
|
133
|
-
|
|
173
|
+
const stderrBuf = this.#stderrChunks.length ? Buffer.concat(this.#stderrChunks) : Buffer.alloc(0);
|
|
174
|
+
let stderr = stderrBuf.toString('utf8').trim();
|
|
175
|
+
if (this.#truncated.stderr) stderr += '\n[stderr truncated]\n';
|
|
176
|
+
reject(new Error(`${code}: ${this.#command} "${stderr}"`));
|
|
134
177
|
}
|
|
135
178
|
});
|
|
136
179
|
|
|
@@ -165,4 +208,3 @@ class SHExecute {
|
|
|
165
208
|
}
|
|
166
209
|
|
|
167
210
|
export default SHExecute;
|
|
168
|
-
|
package/package.json
CHANGED
package/tmp.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { spawnSync, spawn, exec } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Kills a process and all child processes of a given process ID in Linux/Posix.
|
|
5
|
+
* @param {number} processPid - The process ID.
|
|
6
|
+
* @param {string|number} signal - Signal to send.
|
|
7
|
+
* @retruns {Promise<number[]>} array with killed pid numbers
|
|
8
|
+
*/
|
|
9
|
+
const killProcesses = (processPid, signal) => {
|
|
10
|
+
const killed = [];
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
// Command to get child PIDs of the given process
|
|
13
|
+
const cmd = `pgrep -P ${processPid}`;
|
|
14
|
+
exec(cmd, (error, stdout, stderr) => {
|
|
15
|
+
if (error) {
|
|
16
|
+
reject(error);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (stderr) {
|
|
20
|
+
reject(new Error(stderr));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const pids = stdout.split(/\r?\n/).filter(pid => pid) || [];
|
|
24
|
+
// Kill each child process
|
|
25
|
+
try {
|
|
26
|
+
for (const pid of pids) {
|
|
27
|
+
process.kill(parseInt(pid), signal);
|
|
28
|
+
killed.push(parseInt(pid));
|
|
29
|
+
}
|
|
30
|
+
} catch (err) {
|
|
31
|
+
reject(err);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// Kill the parent process after all child processes have been killed
|
|
35
|
+
try {
|
|
36
|
+
process.kill(processPid, signal);
|
|
37
|
+
killed.push(processPid);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
reject(err);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
resolve(killed);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class SHExecute {
|
|
48
|
+
#forcedKill = false;
|
|
49
|
+
/**
|
|
50
|
+
* @type {import('child_process').ChildProcess}
|
|
51
|
+
*/
|
|
52
|
+
#proc;
|
|
53
|
+
#prefix = '';
|
|
54
|
+
#command = '';
|
|
55
|
+
/** @type {import('child_process').SpawnOptions | import('child_process').SpawnSyncOptions} */
|
|
56
|
+
#options = {};
|
|
57
|
+
#stdoutChunks = [];
|
|
58
|
+
#stderrChunks = [];
|
|
59
|
+
/**
|
|
60
|
+
* @param {string} command - linux command to be executed
|
|
61
|
+
* @param {string} prefix - command prefix (bash, sh etc.)
|
|
62
|
+
* @param {import('child_process').SpawnOptions | import('child_process').SpawnSyncOptions} options
|
|
63
|
+
*/
|
|
64
|
+
constructor(command, prefix, options = {}) {
|
|
65
|
+
this.#prefix = prefix;
|
|
66
|
+
this.#command = command;
|
|
67
|
+
this.#options = options;
|
|
68
|
+
this.#proc = null;
|
|
69
|
+
this.#stdoutChunks = [];
|
|
70
|
+
this.#stderrChunks = [];
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* @param {string} [payload] - data to write
|
|
74
|
+
* @retuns {Promise<object>}
|
|
75
|
+
*/
|
|
76
|
+
runSync(payload) {
|
|
77
|
+
if (payload && typeof payload !== 'string') {
|
|
78
|
+
throw new Error('Argument is not a string');
|
|
79
|
+
}
|
|
80
|
+
/** @type {import('node:child_process').SpawnSyncOptions} */
|
|
81
|
+
const options = this.#options;
|
|
82
|
+
// pipe need to be set on stdin when posting a payload
|
|
83
|
+
// @ts-ignore
|
|
84
|
+
if (payload) options['stdio'][0] = 'pipe';
|
|
85
|
+
const input = payload || undefined;
|
|
86
|
+
if (input) {
|
|
87
|
+
options.input = input
|
|
88
|
+
}
|
|
89
|
+
return spawnSync(this.#prefix, [this.#command], options);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* @param {string} [payload] - data to write
|
|
93
|
+
* @retuns {Promise<string>}
|
|
94
|
+
*/
|
|
95
|
+
run(payload) {
|
|
96
|
+
if (payload && typeof payload !== 'string') {
|
|
97
|
+
throw new Error('Argument is not a string');
|
|
98
|
+
}
|
|
99
|
+
/** @type {import('node:child_process').SpawnOptions} */
|
|
100
|
+
const options = this.#options;
|
|
101
|
+
// pipe need to be set on stdin when posting a payload
|
|
102
|
+
// @ts-ignore
|
|
103
|
+
if (payload) options.stdio[0] = 'pipe';
|
|
104
|
+
this.#proc = spawn(this.#prefix, [this.#command], options);
|
|
105
|
+
this.#proc.stdout?.on('data', (data) => {
|
|
106
|
+
this.#stdoutChunks.push(data);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
this.#proc.stderr?.on('data', (data) => {
|
|
110
|
+
this.#stderrChunks.push(data);
|
|
111
|
+
});
|
|
112
|
+
if (payload) {
|
|
113
|
+
this.#proc.stdin.end(payload);
|
|
114
|
+
}
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
// https://nodejs.org/docs/latest/api/child_process.html#optionsdetached
|
|
117
|
+
if (options.detached) {
|
|
118
|
+
setTimeout(() => {
|
|
119
|
+
resolve('');
|
|
120
|
+
this.#proc.unref();
|
|
121
|
+
}, 1000);
|
|
122
|
+
}
|
|
123
|
+
this.#proc.on('close', (code) => {
|
|
124
|
+
if (this.#forcedKill) {
|
|
125
|
+
// Resolve without content
|
|
126
|
+
resolve();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Detached does not closes with an exitcode
|
|
130
|
+
if (code === 0 || code === null || typeof (code) === 'undefined') {
|
|
131
|
+
const stdout = this.#stdoutChunks.map(chunk => chunk.toString()).join('').trim();
|
|
132
|
+
resolve(stdout);
|
|
133
|
+
} else {
|
|
134
|
+
const stderr = this.#stderrChunks.map(chunk => chunk.toString()).join('').trim();
|
|
135
|
+
reject(new Error(`${code}: \${this.#command} "${stderr}"`));
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
this.#proc.on('error', (err) => {
|
|
140
|
+
reject(err);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Kill this process and possible child processes
|
|
146
|
+
* @param {number | string} signal - kill signal
|
|
147
|
+
* @returns {Promise<number[]>}
|
|
148
|
+
*/
|
|
149
|
+
async kill(signal = 'SIGTERM') {
|
|
150
|
+
if (!this.#proc) throw new Error('Trying to kill a process without creating one.');
|
|
151
|
+
if (!this.#proc.pid) throw new Error('The process pid is undefined.');
|
|
152
|
+
this.#forcedKill = true;
|
|
153
|
+
let res = [];
|
|
154
|
+
try {
|
|
155
|
+
// Try to kill pid 'childs'
|
|
156
|
+
res = await killProcesses(this.#proc.pid, signal);
|
|
157
|
+
} catch (_e) { }
|
|
158
|
+
if (!res.includes(this.#proc.pid)) {
|
|
159
|
+
// Kill self if I am not allready killed
|
|
160
|
+
res.push(this.#proc.pid);
|
|
161
|
+
// @ts-ignore
|
|
162
|
+
this.#proc.kill(signal);
|
|
163
|
+
}
|
|
164
|
+
this.#proc = undefined;
|
|
165
|
+
return res;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export default SHExecute;
|
package/types/SHExecute.d.ts
CHANGED
|
@@ -3,9 +3,11 @@ declare class SHExecute {
|
|
|
3
3
|
/**
|
|
4
4
|
* @param {string} command - linux command to be executed
|
|
5
5
|
* @param {string} prefix - command prefix (bash, sh etc.)
|
|
6
|
-
* @param {import('child_process').SpawnOptions
|
|
6
|
+
* @param {import('child_process').SpawnOptions & { maxBuffer?: number }} options
|
|
7
7
|
*/
|
|
8
|
-
constructor(command: string, prefix: string, options?: import("child_process").SpawnOptions
|
|
8
|
+
constructor(command: string, prefix: string, options?: import("child_process").SpawnOptions & {
|
|
9
|
+
maxBuffer?: number;
|
|
10
|
+
});
|
|
9
11
|
/**
|
|
10
12
|
* @param {string} [payload] - data to write
|
|
11
13
|
* @retuns {Promise<object>}
|