@j-o-r/sh 1.1.22 → 1.1.24
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/README.md +20 -1
- package/lib/SH.js +30 -4
- package/lib/SHDispatch.js +27 -12
- package/lib/SHExecute.js +230 -204
- package/lib/Test.js +9 -9
- package/module.md +154 -0
- package/package.json +1 -1
- package/types/SH.d.ts +20 -8
- package/types/SHDispatch.d.ts +16 -16
- package/types/SHExecute.d.ts +28 -16
- package/tmp.js +0 -169
package/README.md
CHANGED
|
@@ -64,11 +64,14 @@ The module also provides additional utilities for common tasks:
|
|
|
64
64
|
- `sleep(duration)`: Pause execution for a specified duration.
|
|
65
65
|
- `retry(count, interval, callback)`: Retry a command a specified number of times with an optional interval.
|
|
66
66
|
- `readIn()`: Read from standard input.
|
|
67
|
+
- `userIn(prompt)`: Prompt the user for input and return an object with `input` (Promise resolving to the user input) and `abort()` method to cancel.
|
|
67
68
|
- `within(callback)`: Create an async context in a sync block.
|
|
68
69
|
- `expBackoff(max, rand)`: Generate intervals for exponential backoff.
|
|
69
70
|
- `jsType(any)`: Get the 'real' javascript variable type
|
|
71
|
+
- `hasProp(object, property)`: Safely check if an object has its own property (handles null/undefined objects).
|
|
70
72
|
- `assert.`: Node assert library
|
|
71
73
|
- `new Test()`: A small sync/async minimal test framework
|
|
74
|
+
- `AsyncTracker`: A class for tracking asynchronous operations using Node.js async hooks.
|
|
72
75
|
|
|
73
76
|
## SHDispatch
|
|
74
77
|
|
|
@@ -118,11 +121,27 @@ This class is returned by the `SH` function. Here's a summary of its methods and
|
|
|
118
121
|
- Retry with exponential backoff:
|
|
119
122
|
```javascript
|
|
120
123
|
try {
|
|
121
|
-
const p = await retry(3, expBackoff(), () => SH`curl -s https://
|
|
124
|
+
const p = await retry(3, expBackoff(), () => SH`curl -s https://unreachable`.run());
|
|
122
125
|
} catch (e) {
|
|
123
126
|
console.error('Retry failed:', e);
|
|
124
127
|
}
|
|
125
128
|
```
|
|
129
|
+
|
|
130
|
+
- Prompt user for input:
|
|
131
|
+
```javascript
|
|
132
|
+
const user = userIn('Enter your name: ');
|
|
133
|
+
const name = await user.input;
|
|
134
|
+
console.log('Hello,', name);
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
- Check if object has property:
|
|
138
|
+
```javascript
|
|
139
|
+
const obj = { a: 1 };
|
|
140
|
+
console.log(hasProp(obj, 'a')); // true
|
|
141
|
+
console.log(hasProp(obj, 'b')); // false
|
|
142
|
+
console.log(hasProp(null, 'a')); // false
|
|
143
|
+
```
|
|
144
|
+
|
|
126
145
|
- Method for copying data to the clipboard:
|
|
127
146
|
```javascript
|
|
128
147
|
/**
|
package/lib/SH.js
CHANGED
|
@@ -81,10 +81,8 @@ const jsType = (fn) => {
|
|
|
81
81
|
* @returns {boolean}
|
|
82
82
|
*/
|
|
83
83
|
const hasProp = (o, p) => {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
return Object.prototype.hasOwnProperty.call(o, p);
|
|
84
|
+
if (o == null) return false;
|
|
85
|
+
return Object.prototype.hasOwnProperty.call(o, p);
|
|
88
86
|
};
|
|
89
87
|
/**
|
|
90
88
|
* 4ms, 5s || 5
|
|
@@ -112,6 +110,15 @@ const parseDuration = (d) => {
|
|
|
112
110
|
/**
|
|
113
111
|
* @type {Shell & SHTag}
|
|
114
112
|
*/
|
|
113
|
+
/**
|
|
114
|
+
* SH template tag
|
|
115
|
+
*
|
|
116
|
+
* Interpolation rules:
|
|
117
|
+
* - Arrays: each element is String(x).trim(), with newlines/carriage returns/tabs escaped; if an element contains shell metacharacters or spaces, it is wrapped in single-quotes and internal single-quotes are escaped as '\''.
|
|
118
|
+
* - Non-array values: currently coerced with String(value) and inserted as-is (NOT shell-escaped). Be careful when interpolating untrusted input.
|
|
119
|
+
*
|
|
120
|
+
* Returns an SHDispatch that can be configured via .options() and executed with .run() / .runSync().
|
|
121
|
+
*/
|
|
115
122
|
const SH = new Proxy(function(pieces, ...args) {
|
|
116
123
|
if (pieces.some((p) => p == undefined)) {
|
|
117
124
|
throw new Error(`Malformed command ${pieces}`);
|
|
@@ -165,6 +172,10 @@ const within = async (callback) => {
|
|
|
165
172
|
* @example
|
|
166
173
|
* const content = await readIn();
|
|
167
174
|
*/
|
|
175
|
+
/**
|
|
176
|
+
* Read entire stdin as UTF-8. If stdin is a TTY, resolves to an empty string.
|
|
177
|
+
* @returns {Promise<string>}
|
|
178
|
+
*/
|
|
168
179
|
const readIn = async () => {
|
|
169
180
|
if (process.stdin.isTTY) return ''; // nothing was piped
|
|
170
181
|
let buf = '';
|
|
@@ -345,6 +356,21 @@ function* expBackoff(max = '60s', rand = '100ms') {
|
|
|
345
356
|
* - The value is either the next argument or `true` if no value is provided,
|
|
346
357
|
* - The `_` property contains an array of unbound arguments.
|
|
347
358
|
*/
|
|
359
|
+
/**
|
|
360
|
+
* Parse command-line args into an object.
|
|
361
|
+
*
|
|
362
|
+
* Supported:
|
|
363
|
+
* - --key value, -k value (no grouped short flags)
|
|
364
|
+
* - Bare values collected under `_.`
|
|
365
|
+
* - Duplicate keys throw an error; keys without a following value become true.
|
|
366
|
+
*
|
|
367
|
+
* Not supported:
|
|
368
|
+
* - --key=value syntax
|
|
369
|
+
* - Grouped short flags like -abc
|
|
370
|
+
*
|
|
371
|
+
* @param {string[]} [args]
|
|
372
|
+
* @returns {ArgsObject}
|
|
373
|
+
*/
|
|
348
374
|
const parseArgs = (args) => {
|
|
349
375
|
if (!args) args = process.argv.slice(2);
|
|
350
376
|
const result = { _: [] };
|
package/lib/SHDispatch.js
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
import SHExec from './SHExecute.js';
|
|
2
2
|
/**
|
|
3
3
|
* @typedef {Object} SpawnSyncResponse
|
|
4
|
-
* @property {number} status -
|
|
5
|
-
* @property {
|
|
6
|
-
* @property {
|
|
7
|
-
* @property {number} pid -
|
|
8
|
-
* @property {Buffer|null} stdout -
|
|
9
|
-
* @property {Buffer|null} stderr -
|
|
4
|
+
* @property {number|null} status - Exit code of the child process (null if terminated by signal).
|
|
5
|
+
* @property {string|null} signal - Name of the terminating signal, if any.
|
|
6
|
+
* @property {(string|Buffer|null)[]} output - [stdin, stdout, stderr] per Node's SpawnSyncReturns.
|
|
7
|
+
* @property {number} pid - PID of the spawned process.
|
|
8
|
+
* @property {string|Buffer|null} stdout - Stdout collected (type depends on encoding option).
|
|
9
|
+
* @property {string|Buffer|null} stderr - Stderr collected (type depends on encoding option).
|
|
10
10
|
*/
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
12
|
* @typedef {Object} SHOptions
|
|
13
13
|
* @property {string} [cwd] - Current working directory of the child process.
|
|
14
14
|
* @property {Object} [env] - Environment key-value pairs.
|
|
15
|
-
* @property {
|
|
15
|
+
* @property {string} [argv0] - Explicitly set the value of `argv[0]` sent to the child process.
|
|
16
16
|
* @property {boolean} [detached=false] - If true, the child will be a process group leader.
|
|
17
17
|
* @property {number} [uid] - Sets the user identity of the process.
|
|
18
18
|
* @property {number} [gid] - Sets the group identity of the process.
|
|
19
19
|
* @property {StdioOptions|StdioOption} [stdio='pipe'] - Child's stdio configuration.
|
|
20
|
-
* @property {boolean|string} [shell=
|
|
21
|
-
* @property {number} [timeout=0] -
|
|
22
|
-
* @property {string|Buffer|
|
|
20
|
+
* @property {boolean|string} [shell='bash'] - If string or true, runs the command via that shell ('bash' if true).
|
|
21
|
+
* @property {number} [timeout=0] - Milliseconds before sending SIGTERM (0 = no timeout).
|
|
22
|
+
* @property {string|Buffer|Uint8Array} [input] - Optional stdin payload for spawnSync. Use .run(payload) for async.
|
|
23
23
|
*/
|
|
24
24
|
/**
|
|
25
25
|
* @typedef {('pipe' | 'ignore' | 'inherit' | number)} StdioOption
|
|
@@ -64,6 +64,21 @@ const mergeOptions = (predefined, options) => {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/** @type {SHOptions} */
|
|
67
|
+
/**
|
|
68
|
+
* SHOptions — effective defaults and semantics used by SHDispatch/SHExecute
|
|
69
|
+
*
|
|
70
|
+
* Defaults:
|
|
71
|
+
* - cwd: process.cwd()
|
|
72
|
+
* - env: process.env
|
|
73
|
+
* - shell: 'bash' (string). If a string, that shell is used. If true, 'bash' is used. If false/undefined, no shell is used.
|
|
74
|
+
* - stdio: ['inherit', 'pipe', 'pipe'] — inherit stdin, capture stdout/stderr
|
|
75
|
+
* - timeout: 0 — no timeout
|
|
76
|
+
* - maxBuffer?: number — optional (bytes per stream). Passed through to SHExecute.
|
|
77
|
+
*
|
|
78
|
+
* Notes:
|
|
79
|
+
* - The prefix (see options(prefix)) is only applied when a shell is used; it is ignored in no-shell mode.
|
|
80
|
+
* - Each call to options() resets to the defaults and merges the provided options; it does not accumulate from prior calls.
|
|
81
|
+
*/
|
|
67
82
|
const defaultOptions = {
|
|
68
83
|
cwd: process.cwd(),
|
|
69
84
|
env: process.env,
|
|
@@ -76,7 +91,7 @@ const defaultOptions = {
|
|
|
76
91
|
|
|
77
92
|
class SHDispatch {
|
|
78
93
|
// #prefix = 'set -euo pipefail;/usr/bin/env'
|
|
79
|
-
#prefix = 'set -euo pipefail
|
|
94
|
+
#prefix = 'set -euo pipefail'
|
|
80
95
|
#cmd = '';
|
|
81
96
|
#options = {};
|
|
82
97
|
/**
|
package/lib/SHExecute.js
CHANGED
|
@@ -1,210 +1,236 @@
|
|
|
1
|
-
import { spawnSync, spawn
|
|
1
|
+
import { spawnSync, spawn} from 'node:child_process';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
}
|
|
3
|
+
// Local helpers
|
|
4
|
+
const isFinitePosInt = (n) => Number.isFinite(n) && n >= 0;
|
|
5
|
+
// Parse duration: accepts numbers (ms) or strings like "250", "250ms", or "2s". null/undefined yields 0; negatives or invalid forms throw.
|
|
6
|
+
const parseDuration = (d) => {
|
|
7
|
+
if (typeof d === 'number') return isFinitePosInt(d) ? d : (() => { throw new Error('Invalid duration'); })();
|
|
8
|
+
if (typeof d === 'string') {
|
|
9
|
+
const m = d.match(/^(\d+)(ms|s)?$/);
|
|
10
|
+
if (!m) throw new Error('Invalid duration string');
|
|
11
|
+
const n = Number(m[1]);
|
|
12
|
+
return m[2] === 's' ? n * 1000 : n;
|
|
13
|
+
}
|
|
14
|
+
if (d == null) return 0;
|
|
15
|
+
throw new Error('Invalid duration type');
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Kill a process tree using pgrep without going through a shell
|
|
19
|
+
const childrenOf = (pid) => new Promise((resolve, reject) => {
|
|
20
|
+
const p = spawn('pgrep', ['-P', String(pid)], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
21
|
+
const out = [];
|
|
22
|
+
p.stdout.on('data', (b) => out.push(b));
|
|
23
|
+
p.on('close', (code) => {
|
|
24
|
+
if (code === 0) {
|
|
25
|
+
const ids = Buffer.concat(out).toString('utf8').trim().split(/\s+/).map(Number).filter(Boolean);
|
|
26
|
+
resolve(ids);
|
|
27
|
+
} else if (code === 1) {
|
|
28
|
+
resolve([]); // no children
|
|
29
|
+
} else {
|
|
30
|
+
reject(new Error(`pgrep -P ${pid} failed with code ${code}`));
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
p.on('error', reject);
|
|
34
|
+
});
|
|
46
35
|
|
|
36
|
+
/**
|
|
37
|
+
* SHExecute
|
|
38
|
+
* Low-level process runner used by SHDispatch.
|
|
39
|
+
*
|
|
40
|
+
* Features:
|
|
41
|
+
* - Optional shell execution: if options.shell is a string (e.g., 'bash') or true, runs `${prefix}; ${command}` via that shell; otherwise runs without a shell using `/usr/bin/env -S`.
|
|
42
|
+
* - Timeout: options.timeout may be a number (ms) or string like '1500ms' or '2s'. On timeout, run() rejects with a descriptive error.
|
|
43
|
+
* - Buffering: Captures stdout/stderr up to maxBuffer bytes per stream (default 40 MiB). Appends "[stdout truncated]" / "[stderr truncated]" markers if exceeded.
|
|
44
|
+
* - Payload: Passing a payload writes it to stdin and forces stdin to be a pipe.
|
|
45
|
+
* - Detached mode: If options.detached is true, run() resolves to '' after ~1s and unrefs the process.
|
|
46
|
+
* - Kill support: kill(signal) attempts to terminate the process (and some children) and causes run() to reject with "Process killed (forced).".
|
|
47
|
+
*/
|
|
47
48
|
class SHExecute {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
49
|
+
#forcedKill = false;
|
|
50
|
+
/** @type {import('child_process').ChildProcess} */
|
|
51
|
+
#proc;
|
|
52
|
+
#prefix = '';
|
|
53
|
+
#command = '';
|
|
54
|
+
/** @type {import('child_process').SpawnOptions | import('child_process').SpawnSyncOptions} */
|
|
55
|
+
#options = {};
|
|
56
|
+
#stdoutChunks = [];
|
|
57
|
+
#stderrChunks = [];
|
|
58
|
+
#stdoutLen = 0;
|
|
59
|
+
#stderrLen = 0;
|
|
60
|
+
#maxBuffer = 40 * 1024 * 1024; // 40MB per stream
|
|
61
|
+
#truncated = { stdout: false, stderr: false };
|
|
62
|
+
#timedOut = false;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {string} command - linux command to be executed
|
|
66
|
+
* @param {string} prefix - command prefix (shell prelude, e.g. 'set -euo pipefail')
|
|
67
|
+
* @param {import('child_process').SpawnOptions & { maxBuffer?: number }} options
|
|
68
|
+
*/
|
|
69
|
+
constructor(command, prefix, options = {}) {
|
|
70
|
+
this.#prefix = prefix;
|
|
71
|
+
this.#command = command;
|
|
72
|
+
const { maxBuffer, ...spawnOpts } = options ?? {};
|
|
73
|
+
this.#maxBuffer = Number.isFinite(maxBuffer) && maxBuffer > 0 ? maxBuffer : this.#maxBuffer;
|
|
74
|
+
this.#options = spawnOpts;
|
|
75
|
+
this.#proc = null;
|
|
76
|
+
this.#stdoutChunks = [];
|
|
77
|
+
this.#stderrChunks = [];
|
|
78
|
+
this.#stdoutLen = 0;
|
|
79
|
+
this.#stderrLen = 0;
|
|
80
|
+
this.#truncated = { stdout: false, stderr: false };
|
|
81
|
+
this.#timedOut = false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {string} [payload] - data to write
|
|
86
|
+
* @returns {import('child_process').SpawnSyncReturns}
|
|
87
|
+
*/
|
|
88
|
+
runSync(payload) {
|
|
89
|
+
this.#forcedKill = false;
|
|
90
|
+
if (payload && typeof payload !== 'string') {
|
|
91
|
+
throw new Error('Argument is not a string');
|
|
92
|
+
}
|
|
93
|
+
/** @type {import('node:child_process').SpawnSyncOptions} */
|
|
94
|
+
const options = { ...this.#options, shell: false };
|
|
95
|
+
if (payload) {
|
|
96
|
+
const stdio = Array.isArray(options.stdio) ? [...options.stdio] : ['pipe', 'pipe', 'pipe'];
|
|
97
|
+
stdio[0] = 'pipe';
|
|
98
|
+
// @ts-ignore
|
|
99
|
+
options.stdio = stdio;
|
|
100
|
+
options.input = payload;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const shellOpt = this.#options?.shell;
|
|
104
|
+
if (shellOpt) {
|
|
105
|
+
const sh = typeof shellOpt === 'string' ? shellOpt : 'bash';
|
|
106
|
+
const cmd = `${this.#prefix}; ${this.#command}`;
|
|
107
|
+
return spawnSync(sh, ['-c', cmd], options);
|
|
108
|
+
}
|
|
109
|
+
// no-shell mode
|
|
110
|
+
return spawnSync('/usr/bin/env', ['-S', this.#command], options);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {string} [payload] - data to write
|
|
115
|
+
* @returns {Promise<string>}
|
|
116
|
+
*/
|
|
117
|
+
run(payload) {
|
|
118
|
+
this.#forcedKill = false;
|
|
119
|
+
if (payload && typeof payload !== 'string') {
|
|
120
|
+
throw new Error('Argument is not a string');
|
|
121
|
+
}
|
|
122
|
+
/** @type {import('node:child_process').SpawnOptions} */
|
|
123
|
+
const options = { ...this.#options, shell: false };
|
|
124
|
+
if (payload) {
|
|
125
|
+
const stdio = Array.isArray(options.stdio) ? [...options.stdio] : ['pipe', 'pipe', 'pipe'];
|
|
126
|
+
stdio[0] = 'pipe';
|
|
127
|
+
// @ts-ignore
|
|
128
|
+
options.stdio = stdio;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// reset buffers for each run
|
|
132
|
+
this.#stdoutChunks = [];
|
|
133
|
+
this.#stderrChunks = [];
|
|
134
|
+
this.#stdoutLen = 0;
|
|
135
|
+
this.#stderrLen = 0;
|
|
136
|
+
this.#truncated = { stdout: false, stderr: false };
|
|
137
|
+
this.#timedOut = false;
|
|
138
|
+
|
|
139
|
+
const shellOpt = this.#options?.shell;
|
|
140
|
+
if (shellOpt) {
|
|
141
|
+
const sh = typeof shellOpt === 'string' ? shellOpt : 'bash';
|
|
142
|
+
const cmd = `${this.#prefix}; ${this.#command}`;
|
|
143
|
+
this.#proc = spawn(sh, ['-c', cmd], options);
|
|
144
|
+
} else {
|
|
145
|
+
this.#proc = spawn('/usr/bin/env', ['-S', this.#command], options);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (payload) this.#proc.stdin?.end(payload);
|
|
149
|
+
|
|
150
|
+
const ms = parseDuration(this.#options?.timeout ?? 0);
|
|
151
|
+
let timeoutId = null;
|
|
152
|
+
if (ms > 0) {
|
|
153
|
+
timeoutId = setTimeout(() => {
|
|
154
|
+
this.#timedOut = true;
|
|
155
|
+
this.kill('SIGTERM').catch(() => {});
|
|
156
|
+
}, ms);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
this.#proc.stdout?.on('data', (chunk) => {
|
|
160
|
+
this.#stdoutLen += chunk.length;
|
|
161
|
+
if (this.#stdoutLen <= this.#maxBuffer) {
|
|
162
|
+
this.#stdoutChunks.push(chunk);
|
|
163
|
+
} else {
|
|
164
|
+
this.#truncated.stdout = true;
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
this.#proc.stderr?.on('data', (chunk) => {
|
|
169
|
+
this.#stderrLen += chunk.length;
|
|
170
|
+
if (this.#stderrLen <= this.#maxBuffer) {
|
|
171
|
+
this.#stderrChunks.push(chunk);
|
|
172
|
+
} else {
|
|
173
|
+
this.#truncated.stderr = true;
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return new Promise((resolve, reject) => {
|
|
178
|
+
if (options.detached) {
|
|
179
|
+
setTimeout(() => {
|
|
180
|
+
resolve('');
|
|
181
|
+
this.#proc.unref();
|
|
182
|
+
}, 1000);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
this.#proc.on('close', (code, signal) => {
|
|
186
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
187
|
+
if (this.#forcedKill) {
|
|
188
|
+
reject(new Error('Process killed (forced).'));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (this.#timedOut) {
|
|
192
|
+
reject(new Error(`Process timed out after ${ms}ms.`));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (code === 0) {
|
|
196
|
+
const stdoutBuf = this.#stdoutChunks.length ? Buffer.concat(this.#stdoutChunks) : Buffer.alloc(0);
|
|
197
|
+
let stdout = stdoutBuf.toString('utf8').trim();
|
|
198
|
+
if (this.#truncated.stdout) stdout += '\n[stdout truncated]\n';
|
|
199
|
+
resolve(stdout);
|
|
200
|
+
} else {
|
|
201
|
+
const stderrBuf = this.#stderrChunks.length ? Buffer.concat(this.#stderrChunks) : Buffer.alloc(0);
|
|
202
|
+
let stderr = stderrBuf.toString('utf8').trim();
|
|
203
|
+
if (this.#truncated.stderr) stderr += '\n[stderr truncated]\n';
|
|
204
|
+
reject(new Error(`Exit ${code ?? 'null'}${signal ? `, signal ${signal}` : ''}: ${this.#command}${stderr ? ` :: ${stderr}` : ''}`));
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
this.#proc.on('error', (err) => reject(err));
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Kill this process and possible child processes
|
|
214
|
+
* @param {number | string} signal - kill signal
|
|
215
|
+
* @returns {Promise<number[]>}
|
|
216
|
+
*/
|
|
217
|
+
async kill(signal = 'SIGTERM') {
|
|
218
|
+
if (!this.#proc) throw new Error('Trying to kill a process without creating one.');
|
|
219
|
+
if (!this.#proc.pid) throw new Error('The process pid is undefined.');
|
|
220
|
+
this.#forcedKill = true;
|
|
221
|
+
const pid = this.#proc.pid;
|
|
222
|
+
let killed = [];
|
|
223
|
+
try {
|
|
224
|
+
const kids = await childrenOf(pid).catch(() => []);
|
|
225
|
+
for (const k of kids) {
|
|
226
|
+
try { process.kill(k, signal); killed.push(k); } catch (e) { if (!e || e.code !== 'ESRCH') throw e; }
|
|
227
|
+
}
|
|
228
|
+
try { process.kill(pid, signal); killed.push(pid); } catch (e) { if (!e || e.code !== 'ESRCH') throw e; }
|
|
229
|
+
} finally {
|
|
230
|
+
this.#proc = undefined;
|
|
231
|
+
}
|
|
232
|
+
return killed;
|
|
233
|
+
}
|
|
208
234
|
}
|
|
209
235
|
|
|
210
236
|
export default SHExecute;
|
package/lib/Test.js
CHANGED
|
@@ -64,7 +64,7 @@ class Test {
|
|
|
64
64
|
*/
|
|
65
65
|
#errors = [];
|
|
66
66
|
/** verbosed **/
|
|
67
|
-
#
|
|
67
|
+
#quiet = false;
|
|
68
68
|
/** Timeout in ms to settle async code blocks called from sync methods */
|
|
69
69
|
#TO = SETTLE_ASYNC;
|
|
70
70
|
/**
|
|
@@ -72,7 +72,7 @@ class Test {
|
|
|
72
72
|
*/
|
|
73
73
|
constructor(quiet = false) {
|
|
74
74
|
if (quiet) {
|
|
75
|
-
this.#
|
|
75
|
+
this.#quiet = true;
|
|
76
76
|
}
|
|
77
77
|
// Track for unresolved promises
|
|
78
78
|
this.#promiseTracker = new AsyncTracker();
|
|
@@ -149,7 +149,7 @@ class Test {
|
|
|
149
149
|
this.#reports[i] = { description: cb.description, duration, executed };
|
|
150
150
|
let error;
|
|
151
151
|
try {
|
|
152
|
-
if (!this.#
|
|
152
|
+
if (!this.#quiet) process.stdout.write(`${i}. ${cb.description} `);
|
|
153
153
|
await Promise.resolve(cb.callback());
|
|
154
154
|
executed = true;
|
|
155
155
|
if (type === 'Function') {
|
|
@@ -167,7 +167,7 @@ class Test {
|
|
|
167
167
|
error = e;
|
|
168
168
|
}
|
|
169
169
|
duration = getDuration(start);
|
|
170
|
-
if (!this.#
|
|
170
|
+
if (!this.#quiet && !error) process.stdout.write(`(duration: ${duration} ms)\n`);
|
|
171
171
|
this.#reports[i].duration = duration;
|
|
172
172
|
this.#reports[i].executed = executed;
|
|
173
173
|
if (error) {
|
|
@@ -198,14 +198,14 @@ class Test {
|
|
|
198
198
|
|
|
199
199
|
}
|
|
200
200
|
const errors = this.#errors.length;
|
|
201
|
-
if (!this.#
|
|
201
|
+
if (!this.#quiet) {
|
|
202
202
|
console.log('--------------------------------------------------');
|
|
203
203
|
console.log(`Total: ${tests} tests, executed: ${executed} in ${duration} ms - errors: ${errors}`);
|
|
204
204
|
if (tests !== executed) {
|
|
205
|
-
if (!this.#
|
|
205
|
+
if (!this.#quiet) console.log('** Not all tests have been executed **');
|
|
206
206
|
return { tests, executed, duration, errors };
|
|
207
207
|
}
|
|
208
|
-
// Report on unresolved promises when NOT
|
|
208
|
+
// Report on unresolved promises when NOT quiet
|
|
209
209
|
// This must be on a next tick because the current Promise (where this code is in) is not resolved yet
|
|
210
210
|
nextTick(() => {
|
|
211
211
|
const count = this.#promiseTracker.report(true);
|
|
@@ -235,9 +235,9 @@ class Test {
|
|
|
235
235
|
// A global error is an error catched outside the callstack of the test
|
|
236
236
|
const ERR = outside ? 'GLOBAL_ERROR' : 'ERROR'
|
|
237
237
|
if (this.#currentTest > -1) {
|
|
238
|
-
if (!this.#
|
|
238
|
+
if (!this.#quiet) process.stdout.write(`\n`);
|
|
239
239
|
// Register this error
|
|
240
|
-
// Always print out errors despite #
|
|
240
|
+
// Always print out errors despite #quiet
|
|
241
241
|
const description = this.#reports[this.#currentTest].description;
|
|
242
242
|
const executed = this.#reports[this.#currentTest].executed;
|
|
243
243
|
if (executed) {
|
package/module.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# @j-o-r/sh Module Documentation
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
**Name:** @j-o-r/sh
|
|
6
|
+
**Version:** 1.1.23
|
|
7
|
+
**Description:** Execute shell commands on Linux-based systems from javascript.
|
|
8
|
+
|
|
9
|
+
This module simplifies the execution of shell commands within JavaScript applications, providing utilities to handle shell scripts and manage their output efficiently. It is inspired by the zx library and supports features like command execution, retries, user input, and more.
|
|
10
|
+
|
|
11
|
+
**Repository:** https://codeberg.org/duin/sh
|
|
12
|
+
**License:** Apache License, Version 2.0
|
|
13
|
+
|
|
14
|
+
Key features include:
|
|
15
|
+
- Execute shell commands synchronously or asynchronously.
|
|
16
|
+
- Utilities for changing directory, sleeping, retrying commands with backoff.
|
|
17
|
+
- Parsing command-line arguments.
|
|
18
|
+
- User input prompts.
|
|
19
|
+
- A minimal test framework.
|
|
20
|
+
- Async operation tracking.
|
|
21
|
+
|
|
22
|
+
## Installation Notes
|
|
23
|
+
|
|
24
|
+
The module is already installed in the current environment. For new installations, use:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
npm install @j-o-r/sh
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Requires Node.js >= 20.0.0.
|
|
31
|
+
|
|
32
|
+
## API Usage Examples
|
|
33
|
+
|
|
34
|
+
### Basic Usage
|
|
35
|
+
|
|
36
|
+
To execute a shell command, use the `SH` function:
|
|
37
|
+
|
|
38
|
+
```javascript
|
|
39
|
+
import { SH } from '@j-o-r/sh';
|
|
40
|
+
|
|
41
|
+
SH`your_shell_command`.run()
|
|
42
|
+
.then(output => {
|
|
43
|
+
console.log('Output:', output);
|
|
44
|
+
})
|
|
45
|
+
.catch(error => {
|
|
46
|
+
console.error('Error:', error);
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Advanced Usage
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
import { SH, cd, within, sleep, retry, expBackoff } from '@j-o-r/sh';
|
|
54
|
+
|
|
55
|
+
const res = await SH`ls -FLa | grep package.json | wc -l`.run();
|
|
56
|
+
console.log(res);
|
|
57
|
+
|
|
58
|
+
const ar = within(async () => {
|
|
59
|
+
const res = await Promise.all([
|
|
60
|
+
SH`sleep 1; echo 1`.run(),
|
|
61
|
+
SH`sleep 2; echo 2`.run(),
|
|
62
|
+
sleep(2),
|
|
63
|
+
SH`sleep 3; echo 3`.run()
|
|
64
|
+
]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const p = await retry(3, expBackoff(), () => SH`curl -s https://unreachable`.run());
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Additional Examples
|
|
71
|
+
|
|
72
|
+
- Prompt user for input:
|
|
73
|
+
```javascript
|
|
74
|
+
const user = userIn('Enter your name: ');
|
|
75
|
+
const name = await user.input;
|
|
76
|
+
console.log('Hello,', name);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
- Create and run a test:
|
|
80
|
+
```javascript
|
|
81
|
+
import { assert, jsType, Test } from '@j-o-r/sh';
|
|
82
|
+
|
|
83
|
+
const test = new Test();
|
|
84
|
+
test.add('Test is test in sync', () => {
|
|
85
|
+
assert.strictEqual(jsType(test), 'Test');
|
|
86
|
+
});
|
|
87
|
+
const report = await test.run();
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Full API Reference
|
|
91
|
+
|
|
92
|
+
### Main Exports
|
|
93
|
+
|
|
94
|
+
- **SH**: A template tag function that returns an `SHDispatch` object for executing shell commands.
|
|
95
|
+
- **cd(dir: string)**: Changes the working directory.
|
|
96
|
+
- **sleep(duration: string | number)**: Pauses execution for the specified duration (e.g., '5s').
|
|
97
|
+
- **retry(count: number, a: string | number | expBackoff | Function, b?: Function)**: Retries a function with optional intervals.
|
|
98
|
+
- **readIn()**: Reads from stdin as a promise.
|
|
99
|
+
- **userIn(prompt: string)**: Prompts for user input, returns object with `input` promise and `abort` method.
|
|
100
|
+
- **within(callback: Function)**: Creates an async context.
|
|
101
|
+
- **expBackoff(max?: string, rand?: string)**: Generator for exponential backoff intervals.
|
|
102
|
+
- **parseArgs(args?: string[])**: Parses command-line arguments into an object.
|
|
103
|
+
- **jsType(any)**: Returns the 'real' JavaScript type.
|
|
104
|
+
- **hasProp(o: any, p: string)**: Safely checks if an object has a property.
|
|
105
|
+
- **Test**: Class for a minimal test framework.
|
|
106
|
+
- **assert**: Node.js assert library.
|
|
107
|
+
- **AsyncTracker**: Class for tracking async operations.
|
|
108
|
+
|
|
109
|
+
### SHDispatch Class
|
|
110
|
+
|
|
111
|
+
Returned by the `SH` template tag. Methods:
|
|
112
|
+
|
|
113
|
+
- **options(options: SpawnOptions | SpawnSyncOptions, prefix?: string)**: Sets execution options.
|
|
114
|
+
- **run(payload?: string)**: Executes the command asynchronously, returns Promise<string>.
|
|
115
|
+
- **runSync(payload?: string)**: Executes synchronously, returns SpawnSyncReturns.
|
|
116
|
+
- **kill(signal?: string)**: Kills the process.
|
|
117
|
+
|
|
118
|
+
### Test Class
|
|
119
|
+
|
|
120
|
+
For running tests:
|
|
121
|
+
|
|
122
|
+
- **constructor(quiet?: boolean)**: Creates a test suite.
|
|
123
|
+
- **syncTimeout(timeout: number)**: Sets timeout for sync tests.
|
|
124
|
+
- **add(description: string, callback: Function | AsyncFunction)**: Adds a test.
|
|
125
|
+
- **run(execute?: number[])**: Runs tests, returns Promise<Report>.
|
|
126
|
+
- **reset()**: Clears tests.
|
|
127
|
+
|
|
128
|
+
### AsyncTracker Class
|
|
129
|
+
|
|
130
|
+
Tracks async operations:
|
|
131
|
+
|
|
132
|
+
- **enable(type?: SystemTypes)**: Enables tracking.
|
|
133
|
+
- **disable()**: Disables tracking.
|
|
134
|
+
- **reset()**: Clears tracked items.
|
|
135
|
+
- **report(verbose?: boolean)**: Reports unresolved async operations.
|
|
136
|
+
- **getUnresolved(type?: SystemTypes)**: Gets unresolved items.
|
|
137
|
+
- **getTypeDescription(type: SystemTypes)**: Gets type description.
|
|
138
|
+
- **addCustomType(type: string, description: string)**: Adds custom type.
|
|
139
|
+
|
|
140
|
+
### Types
|
|
141
|
+
|
|
142
|
+
- **ArgsObject**: Object for parsed args, with `_` for unnamed.
|
|
143
|
+
- **SpawnSyncResponse**: Result of sync spawn.
|
|
144
|
+
- **SHOptions**: Options for SH execution.
|
|
145
|
+
- **AsyncHookItem**: Item in async tracking.
|
|
146
|
+
- **testDefinition, testReport, Report**: Types for test framework.
|
|
147
|
+
|
|
148
|
+
## Dependencies
|
|
149
|
+
|
|
150
|
+
**Runtime Dependencies:** None
|
|
151
|
+
|
|
152
|
+
**Dev Dependencies:**
|
|
153
|
+
- @types/node: ^22.10.10
|
|
154
|
+
|
package/package.json
CHANGED
package/types/SH.d.ts
CHANGED
|
@@ -11,14 +11,7 @@ export type Shell = Function;
|
|
|
11
11
|
* A template-tag that returns an SHDispatch.
|
|
12
12
|
*/
|
|
13
13
|
export type SHTag = (pieces: TemplateStringsArray, ...args: unknown[]) => SHDispatch;
|
|
14
|
-
|
|
15
|
-
* A template-tag that returns an SHDispatch.
|
|
16
|
-
* @typedef {(pieces: TemplateStringsArray, ...args: unknown[]) => SHDispatch} SHTag
|
|
17
|
-
*/
|
|
18
|
-
/**
|
|
19
|
-
* @type {Shell & SHTag}
|
|
20
|
-
*/
|
|
21
|
-
export const SH: Shell & SHTag;
|
|
14
|
+
export function SH(pieces: any, ...args: any[]): SHDispatch;
|
|
22
15
|
/**
|
|
23
16
|
* Change working directory
|
|
24
17
|
* @param {string} dir
|
|
@@ -59,6 +52,10 @@ export function retry(count: number, a: string | number | typeof expBackoff | Fu
|
|
|
59
52
|
* @example
|
|
60
53
|
* const content = await readIn();
|
|
61
54
|
*/
|
|
55
|
+
/**
|
|
56
|
+
* Read entire stdin as UTF-8. If stdin is a TTY, resolves to an empty string.
|
|
57
|
+
* @returns {Promise<string>}
|
|
58
|
+
*/
|
|
62
59
|
export function readIn(): Promise<string>;
|
|
63
60
|
/**
|
|
64
61
|
* Get user input from the command line (stdin)
|
|
@@ -113,6 +110,21 @@ export function expBackoff(max?: string, rand?: string): Generator<number, void,
|
|
|
113
110
|
* - The value is either the next argument or `true` if no value is provided,
|
|
114
111
|
* - The `_` property contains an array of unbound arguments.
|
|
115
112
|
*/
|
|
113
|
+
/**
|
|
114
|
+
* Parse command-line args into an object.
|
|
115
|
+
*
|
|
116
|
+
* Supported:
|
|
117
|
+
* - --key value, -k value (no grouped short flags)
|
|
118
|
+
* - Bare values collected under `_.`
|
|
119
|
+
* - Duplicate keys throw an error; keys without a following value become true.
|
|
120
|
+
*
|
|
121
|
+
* Not supported:
|
|
122
|
+
* - --key=value syntax
|
|
123
|
+
* - Grouped short flags like -abc
|
|
124
|
+
*
|
|
125
|
+
* @param {string[]} [args]
|
|
126
|
+
* @returns {ArgsObject}
|
|
127
|
+
*/
|
|
116
128
|
export function parseArgs(args?: string[]): ArgsObject;
|
|
117
129
|
/**
|
|
118
130
|
* @typedef {Object.<string, string>} ArgsObject
|
package/types/SHDispatch.d.ts
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
export default SHDispatch;
|
|
2
2
|
export type SpawnSyncResponse = {
|
|
3
3
|
/**
|
|
4
|
-
* -
|
|
4
|
+
* - Exit code of the child process (null if terminated by signal).
|
|
5
5
|
*/
|
|
6
|
-
status: number;
|
|
6
|
+
status: number | null;
|
|
7
7
|
/**
|
|
8
|
-
* -
|
|
8
|
+
* - Name of the terminating signal, if any.
|
|
9
9
|
*/
|
|
10
|
-
signal:
|
|
10
|
+
signal: string | null;
|
|
11
11
|
/**
|
|
12
|
-
* -
|
|
12
|
+
* - [stdin, stdout, stderr] per Node's SpawnSyncReturns.
|
|
13
13
|
*/
|
|
14
|
-
output:
|
|
14
|
+
output: (string | Buffer | null)[];
|
|
15
15
|
/**
|
|
16
|
-
* -
|
|
16
|
+
* - PID of the spawned process.
|
|
17
17
|
*/
|
|
18
18
|
pid: number;
|
|
19
19
|
/**
|
|
20
|
-
* -
|
|
20
|
+
* - Stdout collected (type depends on encoding option).
|
|
21
21
|
*/
|
|
22
|
-
stdout: Buffer | null;
|
|
22
|
+
stdout: string | Buffer | null;
|
|
23
23
|
/**
|
|
24
|
-
* -
|
|
24
|
+
* - Stderr collected (type depends on encoding option).
|
|
25
25
|
*/
|
|
26
|
-
stderr: Buffer | null;
|
|
26
|
+
stderr: string | Buffer | null;
|
|
27
27
|
};
|
|
28
28
|
export type SHOptions = {
|
|
29
29
|
/**
|
|
@@ -37,7 +37,7 @@ export type SHOptions = {
|
|
|
37
37
|
/**
|
|
38
38
|
* - Explicitly set the value of `argv[0]` sent to the child process.
|
|
39
39
|
*/
|
|
40
|
-
argv0?: string |
|
|
40
|
+
argv0?: string | undefined;
|
|
41
41
|
/**
|
|
42
42
|
* - If true, the child will be a process group leader.
|
|
43
43
|
*/
|
|
@@ -55,17 +55,17 @@ export type SHOptions = {
|
|
|
55
55
|
*/
|
|
56
56
|
stdio?: number | "pipe" | "ignore" | "inherit" | StdioOption[] | undefined;
|
|
57
57
|
/**
|
|
58
|
-
* - If true, runs command
|
|
58
|
+
* - If string or true, runs the command via that shell ('bash' if true).
|
|
59
59
|
*/
|
|
60
60
|
shell?: string | boolean | undefined;
|
|
61
61
|
/**
|
|
62
|
-
* -
|
|
62
|
+
* - Milliseconds before sending SIGTERM (0 = no timeout).
|
|
63
63
|
*/
|
|
64
64
|
timeout?: number | undefined;
|
|
65
65
|
/**
|
|
66
|
-
* -
|
|
66
|
+
* - Optional stdin payload for spawnSync. Use .run(payload) for async.
|
|
67
67
|
*/
|
|
68
|
-
input?: string |
|
|
68
|
+
input?: string | Uint8Array<ArrayBufferLike> | Buffer<ArrayBufferLike> | undefined;
|
|
69
69
|
};
|
|
70
70
|
export type StdioOption = ("pipe" | "ignore" | "inherit" | number);
|
|
71
71
|
export type StdioOptions = Array<StdioOption> | StdioOption;
|
package/types/SHExecute.d.ts
CHANGED
|
@@ -1,28 +1,40 @@
|
|
|
1
1
|
export default SHExecute;
|
|
2
|
+
/**
|
|
3
|
+
* SHExecute
|
|
4
|
+
* Low-level process runner used by SHDispatch.
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Optional shell execution: if options.shell is a string (e.g., 'bash') or true, runs `${prefix}; ${command}` via that shell; otherwise runs without a shell using `/usr/bin/env -S`.
|
|
8
|
+
* - Timeout: options.timeout may be a number (ms) or string like '1500ms' or '2s'. On timeout, run() rejects with a descriptive error.
|
|
9
|
+
* - Buffering: Captures stdout/stderr up to maxBuffer bytes per stream (default 40 MiB). Appends "[stdout truncated]" / "[stderr truncated]" markers if exceeded.
|
|
10
|
+
* - Payload: Passing a payload writes it to stdin and forces stdin to be a pipe.
|
|
11
|
+
* - Detached mode: If options.detached is true, run() resolves to '' after ~1s and unrefs the process.
|
|
12
|
+
* - Kill support: kill(signal) attempts to terminate the process (and some children) and causes run() to reject with "Process killed (forced).".
|
|
13
|
+
*/
|
|
2
14
|
declare class SHExecute {
|
|
3
15
|
/**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
16
|
+
* @param {string} command - linux command to be executed
|
|
17
|
+
* @param {string} prefix - command prefix (shell prelude, e.g. 'set -euo pipefail')
|
|
18
|
+
* @param {import('child_process').SpawnOptions & { maxBuffer?: number }} options
|
|
19
|
+
*/
|
|
8
20
|
constructor(command: string, prefix: string, options?: import("child_process").SpawnOptions & {
|
|
9
21
|
maxBuffer?: number;
|
|
10
22
|
});
|
|
11
23
|
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
runSync(payload?: string): import("child_process").SpawnSyncReturns<
|
|
24
|
+
* @param {string} [payload] - data to write
|
|
25
|
+
* @returns {import('child_process').SpawnSyncReturns}
|
|
26
|
+
*/
|
|
27
|
+
runSync(payload?: string): import("child_process").SpawnSyncReturns<any>;
|
|
16
28
|
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
run(payload?: string): Promise<
|
|
29
|
+
* @param {string} [payload] - data to write
|
|
30
|
+
* @returns {Promise<string>}
|
|
31
|
+
*/
|
|
32
|
+
run(payload?: string): Promise<string>;
|
|
21
33
|
/**
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
34
|
+
* Kill this process and possible child processes
|
|
35
|
+
* @param {number | string} signal - kill signal
|
|
36
|
+
* @returns {Promise<number[]>}
|
|
37
|
+
*/
|
|
26
38
|
kill(signal?: number | string): Promise<number[]>;
|
|
27
39
|
#private;
|
|
28
40
|
}
|
package/tmp.js
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
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;
|