@j-o-r/sh 1.0.2 → 1.0.3

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/SH.js ADDED
@@ -0,0 +1,254 @@
1
+ // Copyright 2021 Google LLC
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // https://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ //
15
+ //
16
+ // Original Source: zx
17
+ // Link to Original Source: https://github.com/google/zx
18
+ // Reason for Using This Code:
19
+ // The core functionality of this code is highly beneficial. However, certain parts of the original code
20
+ // were overwriting the global namespace with core libraries and variables. This was causing conflicts with
21
+ // other packages (for instance, fetch) and introducing unexpected elements into my code base.
22
+ // The main $/SH method is all there is left, with barebone Promises and readable code.
23
+ // Changes Made:
24
+ // - The code has been or is being reformatted to comply with ES2020 standards.
25
+ // - Some methods were added and existing ones were modified or deleted to enhance usability.
26
+ // - Most methods were deleted,
27
+ // - The namespace has been changed from '$' to 'SH'.
28
+ // Modified by: jorrit.duin+sh[AT]gmail.com
29
+ import assert from 'node:assert';
30
+ import SHDispatch from './SHDispatch.js';
31
+
32
+
33
+ /**
34
+ * Creates a new SHDispatch object that represents a command to be executed.
35
+ *
36
+ * @typedef {Function} Shell
37
+ * @property {function(Array, ...*): ProcessPromise} execute - The function to execute the command.
38
+ *
39
+ * @param {Array} pieces - An array of string literals from a template literal.
40
+ * @param {...*} args - The values to be interpolated into the string literals.
41
+ * @returns {SHDispatch} Trigger for the command.
42
+ * @throws {Error} Throws an error if any of the string literals in `pieces` is undefined.
43
+ *
44
+ * @example
45
+ * const command = await SH`echo 'Hello, world!'`.run();
46
+ */
47
+
48
+ /**
49
+ * escape paramater commands
50
+ * @param {string} arg
51
+ * @returns {string}
52
+ */
53
+ const quote = (arg) => {
54
+ if (/^[a-z0-9/_.\-@:=]+$/i.test(arg) || arg === '') {
55
+ return arg;
56
+ }
57
+ return (`'` +
58
+ arg
59
+ .replace(/\\/g, '\\\\')
60
+ .replace(/'/g, "\\'")
61
+ .replace(/\f/g, '\\f')
62
+ .replace(/\n/g, '\\n')
63
+ .replace(/\r/g, '\\r')
64
+ .replace(/\t/g, '\\t')
65
+ .replace(/\v/g, '\\v')
66
+ .replace(/\0/g, '\\0') +
67
+ `'`);
68
+ }
69
+ /**
70
+ * Escape CLI arguments
71
+ * @param {string[]} arg
72
+ * @retruns {string}
73
+ */
74
+ const sanitizeArg = (arg) => {
75
+ const s = `${arg}`;
76
+ return quote(s);
77
+ }
78
+
79
+ /**
80
+ * 4ms, 5s || 5
81
+ * @param {number|string} d
82
+ * @returns {number}
83
+ */
84
+ const parseDuration = (d) => {
85
+ if (typeof d == 'number') {
86
+ if (isNaN(d) || d < 0)
87
+ throw new Error(`Invalid duration: "${d}".`);
88
+ return d;
89
+ }
90
+ else if (/\d+s/.test(d)) {
91
+ return +d.slice(0, -1) * 1000;
92
+ }
93
+ else if (/\d+ms/.test(d)) {
94
+ return +d.slice(0, -2);
95
+ }
96
+ throw new Error(`Unknown duration: "${d}".`);
97
+ }
98
+ /** @type {Shell & { (pieces: TemplateStringsArray, ...args: *): SHDispatch }} */
99
+ const SH = new Proxy(function(pieces, ...args) {
100
+ if (pieces.some((p) => p == undefined)) {
101
+ throw new Error(`Malformed command ${pieces}`);
102
+ }
103
+ let cmd = pieces[0], i = 0;
104
+ while (i < args.length) {
105
+ let s;
106
+ if (Array.isArray(args[i])) {
107
+ s = args[i].map((x) => sanitizeArg(x)).join(' ');
108
+ }
109
+ else {
110
+ s = sanitizeArg(args[i]);
111
+ }
112
+ cmd += s + pieces[++i];
113
+ }
114
+ return new SHDispatch(cmd);
115
+ }, {});
116
+
117
+ /**
118
+ * Create a async context in an sync block
119
+ * @param {function} callback - async function
120
+ * @example
121
+ * const p = within(async () => {
122
+ * const res = await Promise.all([
123
+ * SH`sleep 1; echo 1`.run(),
124
+ * SH`sleep 2; echo 2`.run(),
125
+ * sleep(2),
126
+ * SH`sleep 3; echo 3`.run()
127
+ * ]);
128
+ */
129
+ const within = (callback) => {
130
+ (async () => {
131
+ return await callback()
132
+ })()
133
+ }
134
+ /**
135
+ * This function reads the standard input (stdin) from the current process.
136
+ * @example
137
+ * const content = await stdin();
138
+ */
139
+ const readIn = async () => {
140
+ let buf = '';
141
+ process.stdin.setEncoding('utf8');
142
+ for await (const chunk of process.stdin) {
143
+ buf += chunk;
144
+ }
145
+ return buf;
146
+ }
147
+
148
+ /**
149
+ * Retries a given asynchronous function a specified number of times with optional delays between attempts.
150
+ *
151
+ * @param {number} count - The number of retry attempts.
152
+ * @param {string|expBackoff|Function} a - Either a delay duration as a string, a delay generator object, or the callback function.
153
+ * @param {Function} [b] - The callback function to retry, required if `a` is not a function.
154
+ * @returns {Promise<*>} - The result of the callback function if it succeeds within the retry attempts.
155
+ * @throws {Error} - The last error encountered if all retry attempts fail.
156
+ *
157
+ * @example
158
+ * // Retry a command 3 times
159
+ * const p = await retry(3, () => SH`curl -s https://flipwrsi`);
160
+ *
161
+ * // Retry a command 3 times with an interval of 1 second between each try
162
+ * const p = await retry(3, '1s', () => SH`curl -s https://flipwrsi`);
163
+ *
164
+ * // Retry a command 3 times with irregular intervals using exponential backoff
165
+ * const p = await retry(3, expBackoff(), () => SH`curl -s https://flipwrsi`);
166
+ */
167
+ const retry = async (count, a, b) => {
168
+ // const total = count;
169
+ let callback;
170
+ let delayStatic = 0;
171
+ let delayGen;
172
+ // @ts-ignore
173
+ if (typeof a == 'function') {
174
+ callback = a;
175
+ }
176
+ else {
177
+ if (typeof a == 'object') {
178
+ delayGen = a;
179
+ }
180
+ else {
181
+ delayStatic = parseDuration(a);
182
+ }
183
+ assert(b);
184
+ callback = b;
185
+ }
186
+ let lastErr;
187
+ let attempt = 0;
188
+ while (count-- > 0) {
189
+ attempt++;
190
+ try {
191
+ return await callback();
192
+ }
193
+ catch (err) {
194
+ let delay = 0;
195
+ if (delayStatic > 0)
196
+ delay = delayStatic;
197
+ // @ts-ignore
198
+ if (delayGen) delay = delayGen.next().value;
199
+ lastErr = err;
200
+ if (count == 0)
201
+ break;
202
+ if (delay)
203
+ await sleep(delay);
204
+ }
205
+ }
206
+ throw lastErr;
207
+ }
208
+ /**
209
+ * This function pauses or "sleeps" code execution for a specified duration.
210
+ * @param {string|number} duration - The duration to pause execution for, e.g., '100ms' or '3s'.
211
+ *
212
+ * @example
213
+ *
214
+ * const res = await sleep('5s');
215
+ */
216
+ const sleep = (duration) => {
217
+ return new Promise((resolve) => {
218
+ setTimeout(resolve, parseDuration(duration));
219
+ });
220
+ }
221
+ /**
222
+ * Change working directory
223
+ * @param {string} dir
224
+ */
225
+ const cd = (dir) => {
226
+ // @ts-ignore
227
+ process.chdir(dir);
228
+ }
229
+ /**
230
+ * Generates an exponential backoff time with a random jitter.
231
+ *
232
+ * @generator
233
+ * @param {string} [max='60s'] - The maximum backoff time in a human-readable format (e.g., '60s' for 60 seconds).
234
+ * @param {string} [rand='100ms'] - The maximum random jitter time in a human-readable format (e.g., '100ms' for 100 milliseconds).
235
+ * @yields {number} The backoff time in milliseconds.
236
+ */
237
+ function* expBackoff(max = '60s', rand = '100ms') {
238
+ const maxMs = parseDuration(max);
239
+ const randMs = parseDuration(rand);
240
+ let n = 1;
241
+ while (true) {
242
+ const ms = Math.floor(Math.random() * randMs);
243
+ yield Math.min(2 ** n++, maxMs) + ms;
244
+ }
245
+ }
246
+ export {
247
+ SH,
248
+ cd,
249
+ sleep,
250
+ retry,
251
+ readIn,
252
+ within,
253
+ expBackoff,
254
+ }
@@ -0,0 +1,151 @@
1
+ import SHExec from './SHExecute.js';
2
+ /**
3
+ * @typedef {Object} SpawnSyncResponse
4
+ * @property {number} status - The exit code of the child process. A value of `0` indicates success.
5
+ * @property {Buffer|null} signal - The signal used to terminate the process, if any.
6
+ * @property {Array<string|null>} output - An array containing the standard output and standard error of the child process.
7
+ * @property {number} pid - The process ID of the child process.
8
+ * @property {Buffer|null} stdout - The standard output of the child process.
9
+ * @property {Buffer|null} stderr - The standard error of the child process.
10
+ */
11
+ /**
12
+ * Default options for the execution environment.
13
+ *
14
+ * @typedef {Object} SHOptions
15
+ * @property {string} [cwd] - The current working directory.
16
+ * @property {NodeJS.ProcessEnv} [env] - The environment variables.
17
+ * @property {string} [shell] - The shell to use for execution.
18
+ * @property {string} [prefix] - The prefix commands to ensure a safe execution environment. e.g: prefix: 'set -euo pipefail;/usr/bin/env',
19
+ * @property {StdioOptions|StdioOption} [stdio] - The stdio configuration.
20
+ * @property {number} [timeout] - default 20000: a timeout error is triggerd when a process execution time is exceeded, 0 is no timeout
21
+ */
22
+ /**
23
+ * @typedef {('pipe' | 'ignore' | 'inherit' | number)} StdioOption
24
+ * @description Defines the stdio configuration for each of the standard streams.
25
+ *
26
+ * - 'pipe' creates a pipe between the child process and the parent process.
27
+ * The parent end of the pipe is exposed as a property on the `ChildProcess` object.
28
+ * - 'ignore' indicates that the child process's corresponding stdio file descriptor will be ignored.
29
+ * - 'inherit' passes the corresponding stdio stream to/from the child process.
30
+ * - Stream object to be used for the stdio stream.
31
+ * - Positive integer representing a file descriptor to be used for the stdio stream.
32
+ */
33
+ /**
34
+ * @typedef {Array<StdioOption>|StdioOption} StdioOptions
35
+ * @description
36
+ * Configures the stdio streams for the child process. This can be an array or a single StdioOption.
37
+ *
38
+ * Array Form: Specify the configuration for [stdin, stdout, stderr].
39
+ * - If array length is more than 3, additional positions correspond to extra streams.
40
+ * Single Value: This value will be applied to stdin, stdout, and stderr.
41
+ *
42
+ * Examples:
43
+ * - ['pipe', 'pipe', 'ignore']: Pipe stdin and stdout, ignore stderr.
44
+ * - 'inherit': Inherit all stdio streams from the parent.
45
+ */
46
+ /**
47
+ * 'Code Safe' has own prop
48
+ *
49
+ * @param {any} o - object to examine
50
+ * @param {string} p - property to look for
51
+ * @returns {boolean}
52
+ */
53
+ const hasProp = (o, p) => {
54
+ if (typeof o === 'undefined') {
55
+ return false;
56
+ }
57
+ return Object.prototype.hasOwnProperty.call(o, p);
58
+ };
59
+
60
+ /**
61
+ * Merge property values while maintaining the fixed set of props in the original object
62
+ * @param {SHOptions} predefined - original object
63
+ * @param {SHOptions} options - object with new values
64
+ * @returns {SHOptions}
65
+ */
66
+ const mergeOptions = (predefined, options) => {
67
+ // Extract the keys from the predefined object
68
+ const keys = Object.keys(predefined);
69
+
70
+ // Use reduce to accumulate only the predefined properties from sourceObj
71
+ const mergedObj = keys.reduce((acc, key) => {
72
+ if (hasProp(options, key)) {
73
+ acc[key] = options[key];
74
+ } else {
75
+ acc[key] = predefined[key];
76
+ }
77
+ return acc;
78
+ }, {});
79
+
80
+ return mergedObj;
81
+ }
82
+
83
+
84
+ /** @type {SHOptions} */
85
+ const defaultOptions = {
86
+ cwd: process.cwd(),
87
+ env: process.env,
88
+ shell: 'bash',
89
+ prefix: '/usr/bin/env',
90
+ stdio: ['inherit', 'pipe', 'pipe'],
91
+ timeout: 10000 // when 0 there is no timeout
92
+ };
93
+
94
+
95
+
96
+ class SHDispatch {
97
+ #cmd = '';
98
+ #options = {};
99
+ /**
100
+ * @type {SHExec}
101
+ */
102
+ #proc;
103
+ /**
104
+ * @param {string} cmd - cmd to execute
105
+ */
106
+ constructor(cmd) {
107
+ this.#cmd = cmd;
108
+ this.#options = defaultOptions
109
+ }
110
+ /**
111
+ * @param {SHOptions} options
112
+ * @returns {SHDispatch}
113
+ */
114
+ options(options) {
115
+ if (options.stdio && typeof options.stdio === 'string') {
116
+ // convert stdio to array
117
+ // This sets the default io values
118
+ // but can be overwritten when having a payload
119
+ const io = options.stdio;
120
+ options.stdio = Array(3).fill(io);
121
+ }
122
+ this.#options = mergeOptions(defaultOptions, options)
123
+ return this;
124
+ }
125
+ /**
126
+ * @param {string} [payload]
127
+ * @returns {Promise<string>}
128
+ */
129
+ run(payload) {
130
+ this.#proc = new SHExec(this.#cmd, this.#options);
131
+ return this.#proc.run(payload);
132
+ }
133
+
134
+ /**
135
+ * Works for screen takeovers like editors
136
+ * @param {string} [payload]
137
+ * @returns {SpawnSyncResponse}
138
+ */
139
+ runSync(payload) {
140
+ // @ts-ignore
141
+ return new SHExec(this.#cmd, this.#options).runSync(payload);
142
+ }
143
+ async kill() {
144
+ try {
145
+ await this.#proc.kill();
146
+ } catch (_e) { }
147
+ this.#proc = undefined;
148
+ }
149
+ }
150
+
151
+ export default SHDispatch
@@ -0,0 +1,154 @@
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} 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
+ /**
49
+ * @type {import('child_process').ChildProcess}
50
+ */
51
+ #proc;
52
+ #command = '';
53
+ #options = {};
54
+ #stdout = '';
55
+ #stderr = '';
56
+ /**
57
+ * @param {string} command - linux command to be executed
58
+ * @param {import('./SHDispatch').SHOptions} [options] - ChildProcess options
59
+ */
60
+ constructor(command, options = {}) {
61
+ this.#command = command;
62
+ this.#options = options;
63
+ this.#proc = null;
64
+ this.#stdout = '';
65
+ this.#stderr = '';
66
+ }
67
+ /**
68
+ * @param {string} [payload] - data to write
69
+ * @retuns {Promise<object>}
70
+ */
71
+ runSync(payload) {
72
+ if (payload && typeof payload !== 'string') {
73
+ throw new Error('Argument is not a string');
74
+ }
75
+ let { cwd, shell, env, stdio } = this.#options;
76
+ // pipe need to be set on stdin when posting a payload
77
+ if (payload) stdio[0] = 'pipe';
78
+ const input = payload || undefined;
79
+ return spawnSync(this.#options.prefix, [this.#command], {
80
+ cwd,
81
+ shell,
82
+ stdio,
83
+ windowsHide: true,
84
+ env,
85
+ input
86
+ });
87
+ }
88
+ /**
89
+ * @param {string} [payload] - data to write
90
+ * @retuns {Promise<string>}
91
+ */
92
+ run(payload) {
93
+ let to = 0;
94
+ if (payload && typeof payload !== 'string') {
95
+ throw new Error('Argument is not a string');
96
+ }
97
+ if (this.#options.timeout) {
98
+ to = this.#options.timeout;
99
+ }
100
+ let { cwd, shell, env, stdio } = this.#options;
101
+ // pipe need to be set on stdin when posting a payload
102
+ if (payload) stdio[0] = 'pipe';
103
+ this.#proc = spawn(this.#options.prefix, [this.#command], {
104
+ cwd,
105
+ shell,
106
+ stdio,
107
+ windowsHide: true,
108
+ env,
109
+ });
110
+ this.#proc.stdout?.on('data', (data) => {
111
+ this.#stdout += data;
112
+ });
113
+
114
+ this.#proc.stderr?.on('data', (data) => {
115
+ this.#stderr += data;
116
+ });
117
+ if (payload) {
118
+ this.#proc.stdin.end(payload);
119
+ }
120
+ return new Promise((resolve, reject) => {
121
+ let timeout;
122
+ if (to > 0) {
123
+ timeout = setTimeout(async () => {
124
+ this.#proc.kill();
125
+ reject(new Error(`Process timed out: ${this.#command}`));
126
+ }, to); // options.timeout
127
+ }
128
+ this.#proc.on('close', (code) => {
129
+ if (timeout) clearTimeout(timeout);
130
+ if (code === 0) {
131
+ resolve(this.#stdout.trim());
132
+ } else {
133
+ reject(new Error(`${code}: ${this.#command} "${this.#stderr.trim()}"`));
134
+ }
135
+ });
136
+
137
+ this.#proc.on('error', (err) => {
138
+ reject(err);
139
+ });
140
+ });
141
+ }
142
+ /**
143
+ * @returns {Promise<number[]>}
144
+ */
145
+ async kill(signal = 'SIGTERM') {
146
+ if (!this.#proc) throw new Error('Trying to kill a process without creating one.');
147
+ if (!this.#proc.pid) throw new Error('The process pid is undefined.');
148
+
149
+ return killProcesses(this.#proc.pid, signal);
150
+ }
151
+ }
152
+
153
+ export default SHExecute;
154
+
package/package.json CHANGED
@@ -2,19 +2,19 @@
2
2
  "name": "@j-o-r/sh",
3
3
  "author": "Jorrit Duin <j-o-r@duin.work>",
4
4
  "type": "module",
5
- "version": "1.0.2",
5
+ "version": "1.0.3",
6
6
  "description": "Execute shell commands on Linux-based systems from javascript",
7
- "main": "lib/sh.js",
7
+ "main": "lib/SH.js",
8
+ "types": "types/SH.d.ts",
8
9
  "engines": {
9
10
  "node": ">=20.0.0"
10
11
  },
11
12
  "scripts": {
12
13
  "test": "test/sh.js",
13
- "compile": "npx rollup src/sh.js --generatedCode es2015 --file lib/sh.js --format es --name SH && npm run types",
14
14
  "publish": "npm run release && npm publish --access public",
15
15
  "release": "npm pack --pack-destination=release",
16
- "types": "tsc lib/*.js --module nodenext --moduleResolution nodenext --declaration --allowJs --emitDeclarationOnly --outDir lib/",
17
- "clear:types": "rm lib/*.d.ts"
16
+ "types": "tsc lib/*.js --module nodenext --moduleResolution nodenext --declaration --allowJs --emitDeclarationOnly --outDir types/",
17
+ "clear:types": "rm types/*.d.ts"
18
18
  },
19
19
  "repository": {
20
20
  "type": "git",
@@ -1,60 +1,3 @@
1
- export type SpawnSyncResponse = {
2
- /**
3
- * - The exit code of the child process. A value of `0` indicates success.
4
- */
5
- status: number;
6
- /**
7
- * - The signal used to terminate the process, if any.
8
- */
9
- signal: Buffer | null;
10
- /**
11
- * - An array containing the standard output and standard error of the child process.
12
- */
13
- output: Array<string | null>;
14
- /**
15
- * - The process ID of the child process.
16
- */
17
- pid: number;
18
- /**
19
- * - The standard output of the child process.
20
- */
21
- stdout: Buffer | null;
22
- /**
23
- * - The standard error of the child process.
24
- */
25
- stderr: Buffer | null;
26
- };
27
- /**
28
- * Default options for the execution environment.
29
- */
30
- export type SHOptions = {
31
- /**
32
- * - The current working directory.
33
- */
34
- cwd?: string;
35
- /**
36
- * - The environment variables.
37
- */
38
- env?: NodeJS.ProcessEnv;
39
- /**
40
- * - The shell to use for execution.
41
- */
42
- shell?: string;
43
- /**
44
- * - The prefix commands to ensure a safe execution environment.
45
- */
46
- prefix?: string;
47
- /**
48
- * - The stdio configuration.
49
- */
50
- stdio?: StdioOptions | StdioOption;
51
- /**
52
- * - default 20000: a timeout error is triggerd when a process execution time is exceeded, 0 is no timeout
53
- */
54
- timeout?: number;
55
- };
56
- export type StdioOption = ('pipe' | 'ignore' | 'inherit' | number);
57
- export type StdioOptions = Array<StdioOption> | StdioOption;
58
1
  /**
59
2
  * Creates a new SHDispatch object that represents a command to be executed.
60
3
  */
@@ -69,20 +12,14 @@ export const SH: Shell & {
69
12
  */
70
13
  export function cd(dir: string): void;
71
14
  /**
72
- * Generates an exponential backoff time with a random jitter.
73
- *
74
- * @generator
75
- * @param {string} [max='60s'] - The maximum backoff time in a human-readable format (e.g., '60s' for 60 seconds).
76
- * @param {string} [rand='100ms'] - The maximum random jitter time in a human-readable format (e.g., '100ms' for 100 milliseconds).
77
- * @yields {number} The backoff time in milliseconds.
78
- */
79
- export function expBackoff(max?: string, rand?: string): Generator<number, void, unknown>;
80
- /**
81
- * This function reads the standard input (stdin) from the current process.
15
+ * This function pauses or "sleeps" code execution for a specified duration.
16
+ * @param {string|number} duration - The duration to pause execution for, e.g., '100ms' or '3s'.
17
+ *
82
18
  * @example
83
- * const content = await stdin();
19
+ *
20
+ * const res = await sleep('5s');
84
21
  */
85
- export function readIn(): Promise<string>;
22
+ export function sleep(duration: string | number): Promise<any>;
86
23
  /**
87
24
  * Retries a given asynchronous function a specified number of times with optional delays between attempts.
88
25
  *
@@ -104,14 +41,11 @@ export function readIn(): Promise<string>;
104
41
  */
105
42
  export function retry(count: number, a: string | typeof expBackoff | Function, b?: Function): Promise<any>;
106
43
  /**
107
- * This function pauses or "sleeps" code execution for a specified duration.
108
- * @param {string|number} duration - The duration to pause execution for, e.g., '100ms' or '3s'.
109
- *
44
+ * This function reads the standard input (stdin) from the current process.
110
45
  * @example
111
- *
112
- * const res = await sleep('5s');
46
+ * const content = await stdin();
113
47
  */
114
- export function sleep(duration: string | number): Promise<any>;
48
+ export function readIn(): Promise<string>;
115
49
  /**
116
50
  * Create a async context in an sync block
117
51
  * @param {function} callback - async function
@@ -125,28 +59,13 @@ export function sleep(duration: string | number): Promise<any>;
125
59
  * ]);
126
60
  */
127
61
  export function within(callback: Function): void;
128
- declare class SHDispatch {
129
- /**
130
- * @param {string} cmd - cmd to execute
131
- */
132
- constructor(cmd: string);
133
- /**
134
- * @param {SHOptions} options
135
- * @returns {SHDispatch}
136
- */
137
- options(options: SHOptions): SHDispatch;
138
- /**
139
- * @param {string} [payload]
140
- * @returns {Promise<string>}
141
- */
142
- run(payload?: string): Promise<string>;
143
- /**
144
- * Works for screen takeovers like editors
145
- * @param {string} [payload]
146
- * @returns {SpawnSyncResponse}
147
- */
148
- runSync(payload?: string): SpawnSyncResponse;
149
- kill(): Promise<void>;
150
- #private;
151
- }
152
- export {};
62
+ /**
63
+ * Generates an exponential backoff time with a random jitter.
64
+ *
65
+ * @generator
66
+ * @param {string} [max='60s'] - The maximum backoff time in a human-readable format (e.g., '60s' for 60 seconds).
67
+ * @param {string} [rand='100ms'] - The maximum random jitter time in a human-readable format (e.g., '100ms' for 100 milliseconds).
68
+ * @yields {number} The backoff time in milliseconds.
69
+ */
70
+ export function expBackoff(max?: string, rand?: string): Generator<number, void, unknown>;
71
+ import SHDispatch from './SHDispatch.js';
@@ -0,0 +1,82 @@
1
+ export default SHDispatch;
2
+ export type SpawnSyncResponse = {
3
+ /**
4
+ * - The exit code of the child process. A value of `0` indicates success.
5
+ */
6
+ status: number;
7
+ /**
8
+ * - The signal used to terminate the process, if any.
9
+ */
10
+ signal: Buffer | null;
11
+ /**
12
+ * - An array containing the standard output and standard error of the child process.
13
+ */
14
+ output: Array<string | null>;
15
+ /**
16
+ * - The process ID of the child process.
17
+ */
18
+ pid: number;
19
+ /**
20
+ * - The standard output of the child process.
21
+ */
22
+ stdout: Buffer | null;
23
+ /**
24
+ * - The standard error of the child process.
25
+ */
26
+ stderr: Buffer | null;
27
+ };
28
+ /**
29
+ * Default options for the execution environment.
30
+ */
31
+ export type SHOptions = {
32
+ /**
33
+ * - The current working directory.
34
+ */
35
+ cwd?: string;
36
+ /**
37
+ * - The environment variables.
38
+ */
39
+ env?: NodeJS.ProcessEnv;
40
+ /**
41
+ * - The shell to use for execution.
42
+ */
43
+ shell?: string;
44
+ /**
45
+ * - The prefix commands to ensure a safe execution environment. e.g: prefix: 'set -euo pipefail;/usr/bin/env',
46
+ */
47
+ prefix?: string;
48
+ /**
49
+ * - The stdio configuration.
50
+ */
51
+ stdio?: StdioOptions | StdioOption;
52
+ /**
53
+ * - default 20000: a timeout error is triggerd when a process execution time is exceeded, 0 is no timeout
54
+ */
55
+ timeout?: number;
56
+ };
57
+ export type StdioOption = ('pipe' | 'ignore' | 'inherit' | number);
58
+ export type StdioOptions = Array<StdioOption> | StdioOption;
59
+ declare class SHDispatch {
60
+ /**
61
+ * @param {string} cmd - cmd to execute
62
+ */
63
+ constructor(cmd: string);
64
+ /**
65
+ * @param {SHOptions} options
66
+ * @returns {SHDispatch}
67
+ */
68
+ options(options: SHOptions): SHDispatch;
69
+ /**
70
+ * @param {string} [payload]
71
+ * @returns {Promise<string>}
72
+ */
73
+ run(payload?: string): Promise<string>;
74
+ /**
75
+ * Works for screen takeovers like editors
76
+ * @param {string} [payload]
77
+ * @returns {SpawnSyncResponse}
78
+ */
79
+ runSync(payload?: string): SpawnSyncResponse;
80
+ kill(): Promise<void>;
81
+ #private;
82
+ }
@@ -0,0 +1,24 @@
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ export default SHExecute;
3
+ declare class SHExecute {
4
+ /**
5
+ * @param {string} command - linux command to be executed
6
+ * @param {import('./SHDispatch').SHOptions} [options] - ChildProcess options
7
+ */
8
+ constructor(command: string, options?: any);
9
+ /**
10
+ * @param {string} [payload] - data to write
11
+ * @retuns {Promise<object>}
12
+ */
13
+ runSync(payload?: string): import("child_process").SpawnSyncReturns<Buffer>;
14
+ /**
15
+ * @param {string} [payload] - data to write
16
+ * @retuns {Promise<string>}
17
+ */
18
+ run(payload?: string): Promise<any>;
19
+ /**
20
+ * @returns {Promise<number[]>}
21
+ */
22
+ kill(signal?: string): Promise<number[]>;
23
+ #private;
24
+ }
package/lib/sh.js DELETED
@@ -1,538 +0,0 @@
1
- import assert from 'node:assert';
2
- import { spawnSync, spawn, exec } from 'node:child_process';
3
-
4
- // timeout when a process becomes inresponsive
5
-
6
- /**
7
- * Kills a process and all child processes of a given process ID in Linux/Posix.
8
- * @param {number} processPid - The process ID.
9
- * @param {string} signal - Signal to send.
10
- * @retruns {Promise<number[]>} array with killed pid numbers
11
- */
12
- const killProcesses = (processPid, signal) => {
13
- const killed = [];
14
- return new Promise((resolve, reject) => {
15
- // Command to get child PIDs of the given process
16
- const cmd = `pgrep -P ${processPid}`;
17
- exec(cmd, (error, stdout, stderr) => {
18
- if (error) {
19
- reject(error);
20
- return;
21
- }
22
- if (stderr) {
23
- reject(new Error(stderr));
24
- return;
25
- }
26
- const pids = stdout.split(/\r?\n/).filter(pid => pid);
27
- // Kill each child process
28
- try {
29
- for (const pid of pids) {
30
- process.kill(parseInt(pid), signal);
31
- killed.push(parseInt(pid));
32
- }
33
- } catch (err) {
34
- reject(err);
35
- return;
36
- }
37
- // Kill the parent process after all child processes have been killed
38
- try {
39
- process.kill(processPid, signal);
40
- killed.push(processPid);
41
- } catch (err) {
42
- reject(err);
43
- return;
44
- }
45
- resolve(killed);
46
- });
47
- });
48
- };
49
-
50
- class SHExecute {
51
- #proc;
52
- #command = '';
53
- #options = {};
54
- #stdout = '';
55
- #stderr = '';
56
- constructor(command, options = {}) {
57
- this.#command = command;
58
- this.#options = options;
59
- this.#proc = null;
60
- this.#stdout = '';
61
- this.#stderr = '';
62
- }
63
- /**
64
- * @param {string} [payload] - data to write
65
- * @retuns {Promise<object>}
66
- */
67
- runSync(payload) {
68
- if (payload && typeof payload !== 'string') {
69
- throw new Error('Argument is not a string');
70
- }
71
- let { cwd, shell, env, stdio } = this.#options;
72
- // pipe need to be set on stdin when posting a payload
73
- if (payload) stdio[0] = 'pipe';
74
- const input = payload || undefined;
75
- return spawnSync(this.#options.prefix, [this.#command], {
76
- cwd,
77
- shell,
78
- stdio,
79
- windowsHide: true,
80
- env,
81
- input
82
- });
83
- }
84
- /**
85
- * @param {string} [payload] - data to write
86
- * @retuns {Promise<string>}
87
- */
88
- run(payload) {
89
- let to = 0;
90
- if (payload && typeof payload !== 'string') {
91
- throw new Error('Argument is not a string');
92
- }
93
- if (this.#options.timeout) {
94
- to = this.#options.timeout;
95
- }
96
- let { cwd, shell, env, stdio } = this.#options;
97
- // pipe need to be set on stdin when posting a payload
98
- if (payload) stdio[0] = 'pipe';
99
- this.#proc = spawn(this.#options.prefix, [this.#command], {
100
- cwd,
101
- shell,
102
- stdio,
103
- windowsHide: true,
104
- env,
105
- });
106
-
107
- this.#proc.stdout?.on('data', (data) => {
108
- this.#stdout += data;
109
- });
110
-
111
- this.#proc.stderr?.on('data', (data) => {
112
- this.#stderr += data;
113
- });
114
- if (payload) {
115
- this.#proc.stdin.end(payload);
116
- }
117
- return new Promise((resolve, reject) => {
118
- let timeout;
119
- if (to > 0) {
120
- timeout = setTimeout(async () => {
121
- await this.#proc.kill();
122
- reject(new Error(`Process timed out: ${this.#command}`));
123
- }, to); // options.timeout
124
- }
125
- this.#proc.on('close', (code) => {
126
- if (timeout) clearTimeout(timeout);
127
- if (code === 0) {
128
- resolve(this.#stdout.trim());
129
- } else {
130
- reject(new Error(`${code}: ${this.#command} "${this.#stderr.trim()}"`));
131
- }
132
- });
133
-
134
- this.#proc.on('error', (err) => {
135
- reject(err);
136
- });
137
- });
138
- }
139
- /**
140
- * @returns {Promise<number[]>}
141
- */
142
- async kill(signal = 'SIGTERM') {
143
- if (!this.#proc) throw new Error('Trying to kill a process without creating one.');
144
- if (!this.#proc.pid) throw new Error('The process pid is undefined.');
145
-
146
- return killProcesses(this.#proc.pid, signal);
147
- }
148
- }
149
-
150
- /**
151
- * @typedef {Object} SpawnSyncResponse
152
- * @property {number} status - The exit code of the child process. A value of `0` indicates success.
153
- * @property {Buffer|null} signal - The signal used to terminate the process, if any.
154
- * @property {Array<string|null>} output - An array containing the standard output and standard error of the child process.
155
- * @property {number} pid - The process ID of the child process.
156
- * @property {Buffer|null} stdout - The standard output of the child process.
157
- * @property {Buffer|null} stderr - The standard error of the child process.
158
- */
159
- /**
160
- * Default options for the execution environment.
161
- *
162
- * @typedef {Object} SHOptions
163
- * @property {string} [cwd] - The current working directory.
164
- * @property {NodeJS.ProcessEnv} [env] - The environment variables.
165
- * @property {string} [shell] - The shell to use for execution.
166
- * @property {string} [prefix] - The prefix commands to ensure a safe execution environment.
167
- * @property {StdioOptions|StdioOption} [stdio] - The stdio configuration.
168
- * @property {number} [timeout] - default 20000: a timeout error is triggerd when a process execution time is exceeded, 0 is no timeout
169
- */
170
- /**
171
- * @typedef {('pipe' | 'ignore' | 'inherit' | number)} StdioOption
172
- * @description Defines the stdio configuration for each of the standard streams.
173
- *
174
- * - 'pipe' creates a pipe between the child process and the parent process.
175
- * The parent end of the pipe is exposed as a property on the `ChildProcess` object.
176
- * - 'ignore' indicates that the child process's corresponding stdio file descriptor will be ignored.
177
- * - 'inherit' passes the corresponding stdio stream to/from the child process.
178
- * - Stream object to be used for the stdio stream.
179
- * - Positive integer representing a file descriptor to be used for the stdio stream.
180
- */
181
- /**
182
- * @typedef {Array<StdioOption>|StdioOption} StdioOptions
183
- * @description
184
- * Configures the stdio streams for the child process. This can be an array or a single StdioOption.
185
- *
186
- * Array Form: Specify the configuration for [stdin, stdout, stderr].
187
- * - If array length is more than 3, additional positions correspond to extra streams.
188
- * Single Value: This value will be applied to stdin, stdout, and stderr.
189
- *
190
- * Examples:
191
- * - ['pipe', 'pipe', 'ignore']: Pipe stdin and stdout, ignore stderr.
192
- * - 'inherit': Inherit all stdio streams from the parent.
193
- */
194
- /**
195
- * 'Code Safe' has own prop
196
- *
197
- * @param {any} o - object to examine
198
- * @param {string} p - property to look for
199
- * @returns {boolean}
200
- */
201
- const hasProp = (o, p) => {
202
- if (typeof o === 'undefined') {
203
- return false;
204
- }
205
- return Object.prototype.hasOwnProperty.call(o, p);
206
- };
207
-
208
- /**
209
- * Merge property values while maintaining the fixed set of props in the original object
210
- * @param {SHOptions} predefined - original object
211
- * @param {SHOptions} options - object with new values
212
- * @returns {SHOptions}
213
- */
214
- const mergeOptions = (predefined, options) => {
215
- // Extract the keys from the predefined object
216
- const keys = Object.keys(predefined);
217
-
218
- // Use reduce to accumulate only the predefined properties from sourceObj
219
- const mergedObj = keys.reduce((acc, key) => {
220
- if (hasProp(options, key)) {
221
- acc[key] = options[key];
222
- } else {
223
- acc[key] = predefined[key];
224
- }
225
- return acc;
226
- }, {});
227
-
228
- return mergedObj;
229
- };
230
-
231
-
232
- /** @type {SHOptions} */
233
- const defaultOptions = {
234
- cwd: process.cwd(),
235
- env: process.env,
236
- shell: 'bash',
237
- prefix: 'set -euo pipefail;/usr/bin/env',
238
- stdio: ['inherit', 'pipe', 'pipe'],
239
- timeout: 10000 // when 0 there is no timeout
240
- };
241
-
242
-
243
-
244
- class SHDispatch {
245
- #cmd = '';
246
- #options = {};
247
- #proc;
248
- /**
249
- * @param {string} cmd - cmd to execute
250
- */
251
- constructor(cmd) {
252
- this.#cmd = cmd;
253
- this.#options = defaultOptions;
254
- }
255
- /**
256
- * @param {SHOptions} options
257
- * @returns {SHDispatch}
258
- */
259
- options(options) {
260
- if (options.stdio && typeof options.stdio === 'string') {
261
- // convert stdio to array
262
- // This sets the default io values
263
- // but can be overwritten when having a payload
264
- const io = options.stdio;
265
- options.stdio = Array(3).fill(io);
266
- }
267
- this.#options = mergeOptions(defaultOptions, options);
268
- return this;
269
- }
270
- /**
271
- * @param {string} [payload]
272
- * @returns {Promise<string>}
273
- */
274
- run(payload) {
275
- this.#proc = new SHExecute(this.#cmd, this.#options);
276
- return this.#proc.run(payload);
277
- }
278
-
279
- /**
280
- * Works for screen takeovers like editors
281
- * @param {string} [payload]
282
- * @returns {SpawnSyncResponse}
283
- */
284
- runSync(payload) {
285
- // @ts-ignore
286
- return new SHExecute(this.#cmd, this.#options).runSync(payload);
287
- }
288
- async kill() {
289
- try {
290
- await this.#proc.kill();
291
- } catch (_e) {}
292
- this.#proc = undefined;
293
- }
294
- }
295
-
296
- // Copyright 2021 Google LLC
297
- //
298
- // Licensed under the Apache License, Version 2.0 (the "License");
299
- // you may not use this file except in compliance with the License.
300
- // You may obtain a copy of the License at
301
- //
302
- // https://www.apache.org/licenses/LICENSE-2.0
303
- //
304
- // Unless required by applicable law or agreed to in writing, software
305
- // distributed under the License is distributed on an "AS IS" BASIS,
306
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
307
- // See the License for the specific language governing permissions and
308
- // limitations under the License.
309
- //
310
- //
311
- // Original Source: zx
312
- // Link to Original Source: https://github.com/google/zx
313
- // Reason for Using This Code:
314
- // The core functionality of this code is highly beneficial. However, certain parts of the original code
315
- // were overwriting the global namespace with core libraries and variables. This was causing conflicts with
316
- // other packages (for instance, fetch) and introducing unexpected elements into my code base.
317
- // The main $/SH method is all there is left, with barebone Promises and readable code.
318
- // Changes Made:
319
- // - The code has been or is being reformatted to comply with ES2020 standards.
320
- // - Some methods were added and existing ones were modified or deleted to enhance usability.
321
- // - Most methods were deleted,
322
- // - The namespace has been changed from '$' to 'SH'.
323
- // Modified by: jorrit.duin+sh[AT]gmail.com
324
-
325
-
326
- /**
327
- * Creates a new SHDispatch object that represents a command to be executed.
328
- *
329
- * @typedef {Function} Shell
330
- * @property {function(Array, ...*): ProcessPromise} execute - The function to execute the command.
331
- *
332
- * @param {Array} pieces - An array of string literals from a template literal.
333
- * @param {...*} args - The values to be interpolated into the string literals.
334
- * @returns {SHDispatch} Trigger for the command.
335
- * @throws {Error} Throws an error if any of the string literals in `pieces` is undefined.
336
- *
337
- * @example
338
- * const command = await SH`echo 'Hello, world!'`.run();
339
- */
340
-
341
- /**
342
- * escape paramater commands
343
- * @param {string} arg
344
- * @returns {string}
345
- */
346
- const quote = (arg) => {
347
- if (/^[a-z0-9/_.\-@:=]+$/i.test(arg) || arg === '') {
348
- return arg;
349
- }
350
- return (`'` +
351
- arg
352
- .replace(/\\/g, '\\\\')
353
- .replace(/'/g, "\\'")
354
- .replace(/\f/g, '\\f')
355
- .replace(/\n/g, '\\n')
356
- .replace(/\r/g, '\\r')
357
- .replace(/\t/g, '\\t')
358
- .replace(/\v/g, '\\v')
359
- .replace(/\0/g, '\\0') +
360
- `'`);
361
- };
362
- /**
363
- * Escape CLI arguments
364
- * @param {string[]} arg
365
- * @retruns {string}
366
- */
367
- const sanitizeArg = (arg) => {
368
- const s = `${arg}`;
369
- return quote(s);
370
- };
371
-
372
- /**
373
- * 4ms, 5s || 5
374
- * @param {number|string} d
375
- * @returns {number}
376
- */
377
- const parseDuration = (d) => {
378
- if (typeof d == 'number') {
379
- if (isNaN(d) || d < 0)
380
- throw new Error(`Invalid duration: "${d}".`);
381
- return d;
382
- }
383
- else if (/\d+s/.test(d)) {
384
- return +d.slice(0, -1) * 1000;
385
- }
386
- else if (/\d+ms/.test(d)) {
387
- return +d.slice(0, -2);
388
- }
389
- throw new Error(`Unknown duration: "${d}".`);
390
- };
391
- /** @type {Shell & { (pieces: TemplateStringsArray, ...args: *): SHDispatch }} */
392
- const SH = new Proxy(function(pieces, ...args) {
393
- if (pieces.some((p) => p == undefined)) {
394
- throw new Error(`Malformed command ${pieces}`);
395
- }
396
- let cmd = pieces[0], i = 0;
397
- while (i < args.length) {
398
- let s;
399
- if (Array.isArray(args[i])) {
400
- s = args[i].map((x) => sanitizeArg(x)).join(' ');
401
- }
402
- else {
403
- s = sanitizeArg(args[i]);
404
- }
405
- cmd += s + pieces[++i];
406
- }
407
- return new SHDispatch(cmd);
408
- }, {});
409
-
410
- /**
411
- * Create a async context in an sync block
412
- * @param {function} callback - async function
413
- * @example
414
- * const p = within(async () => {
415
- * const res = await Promise.all([
416
- * SH`sleep 1; echo 1`.run(),
417
- * SH`sleep 2; echo 2`.run(),
418
- * sleep(2),
419
- * SH`sleep 3; echo 3`.run()
420
- * ]);
421
- */
422
- const within = (callback) => {
423
- (async () => {
424
- return await callback()
425
- })();
426
- };
427
- /**
428
- * This function reads the standard input (stdin) from the current process.
429
- * @example
430
- * const content = await stdin();
431
- */
432
- const readIn = async () => {
433
- let buf = '';
434
- process.stdin.setEncoding('utf8');
435
- for await (const chunk of process.stdin) {
436
- buf += chunk;
437
- }
438
- return buf;
439
- };
440
-
441
- /**
442
- * Retries a given asynchronous function a specified number of times with optional delays between attempts.
443
- *
444
- * @param {number} count - The number of retry attempts.
445
- * @param {string|expBackoff|Function} a - Either a delay duration as a string, a delay generator object, or the callback function.
446
- * @param {Function} [b] - The callback function to retry, required if `a` is not a function.
447
- * @returns {Promise<*>} - The result of the callback function if it succeeds within the retry attempts.
448
- * @throws {Error} - The last error encountered if all retry attempts fail.
449
- *
450
- * @example
451
- * // Retry a command 3 times
452
- * const p = await retry(3, () => SH`curl -s https://flipwrsi`);
453
- *
454
- * // Retry a command 3 times with an interval of 1 second between each try
455
- * const p = await retry(3, '1s', () => SH`curl -s https://flipwrsi`);
456
- *
457
- * // Retry a command 3 times with irregular intervals using exponential backoff
458
- * const p = await retry(3, expBackoff(), () => SH`curl -s https://flipwrsi`);
459
- */
460
- const retry = async (count, a, b) => {
461
- // const total = count;
462
- let callback;
463
- let delayStatic = 0;
464
- let delayGen;
465
- // @ts-ignore
466
- if (typeof a == 'function') {
467
- callback = a;
468
- }
469
- else {
470
- if (typeof a == 'object') {
471
- delayGen = a;
472
- }
473
- else {
474
- delayStatic = parseDuration(a);
475
- }
476
- assert(b);
477
- callback = b;
478
- }
479
- let lastErr;
480
- while (count-- > 0) {
481
- try {
482
- return await callback();
483
- }
484
- catch (err) {
485
- let delay = 0;
486
- if (delayStatic > 0)
487
- delay = delayStatic;
488
- // @ts-ignore
489
- if (delayGen) delay = delayGen.next().value;
490
- lastErr = err;
491
- if (count == 0)
492
- break;
493
- if (delay)
494
- await sleep(delay);
495
- }
496
- }
497
- throw lastErr;
498
- };
499
- /**
500
- * This function pauses or "sleeps" code execution for a specified duration.
501
- * @param {string|number} duration - The duration to pause execution for, e.g., '100ms' or '3s'.
502
- *
503
- * @example
504
- *
505
- * const res = await sleep('5s');
506
- */
507
- const sleep = (duration) => {
508
- return new Promise((resolve) => {
509
- setTimeout(resolve, parseDuration(duration));
510
- });
511
- };
512
- /**
513
- * Change working directory
514
- * @param {string} dir
515
- */
516
- const cd = (dir) => {
517
- // @ts-ignore
518
- process.chdir(dir);
519
- };
520
- /**
521
- * Generates an exponential backoff time with a random jitter.
522
- *
523
- * @generator
524
- * @param {string} [max='60s'] - The maximum backoff time in a human-readable format (e.g., '60s' for 60 seconds).
525
- * @param {string} [rand='100ms'] - The maximum random jitter time in a human-readable format (e.g., '100ms' for 100 milliseconds).
526
- * @yields {number} The backoff time in milliseconds.
527
- */
528
- function* expBackoff(max = '60s', rand = '100ms') {
529
- const maxMs = parseDuration(max);
530
- const randMs = parseDuration(rand);
531
- let n = 1;
532
- while (true) {
533
- const ms = Math.floor(Math.random() * randMs);
534
- yield Math.min(2 ** n++, maxMs) + ms;
535
- }
536
- }
537
-
538
- export { SH, cd, expBackoff, readIn, retry, sleep, within };