teen_process 3.0.6 → 4.0.1
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/build/lib/exec.d.ts +32 -107
- package/build/lib/exec.d.ts.map +1 -1
- package/build/lib/exec.js +60 -103
- package/build/lib/exec.js.map +1 -1
- package/build/lib/helpers.d.ts +1 -12
- package/build/lib/helpers.d.ts.map +1 -1
- package/build/lib/helpers.js +17 -13
- package/build/lib/helpers.js.map +1 -1
- package/build/lib/index.d.ts +4 -6
- package/build/lib/index.d.ts.map +1 -1
- package/build/lib/index.js +7 -43
- package/build/lib/index.js.map +1 -1
- package/build/lib/subprocess.d.ts +106 -61
- package/build/lib/subprocess.d.ts.map +1 -1
- package/build/lib/subprocess.js +145 -143
- package/build/lib/subprocess.js.map +1 -1
- package/build/lib/types.d.ts +79 -0
- package/build/lib/types.d.ts.map +1 -0
- package/build/lib/types.js +3 -0
- package/build/lib/types.js.map +1 -0
- package/build/test/circular-buffer-specs.d.ts +2 -0
- package/build/test/circular-buffer-specs.d.ts.map +1 -0
- package/build/test/circular-buffer-specs.js +40 -0
- package/build/test/circular-buffer-specs.js.map +1 -0
- package/build/test/exec-specs.d.ts +2 -0
- package/build/test/exec-specs.d.ts.map +1 -0
- package/build/test/exec-specs.js +167 -0
- package/build/test/exec-specs.js.map +1 -0
- package/build/test/fixtures/bigbuffer.d.ts +3 -0
- package/build/test/fixtures/bigbuffer.d.ts.map +1 -0
- package/build/test/fixtures/bigbuffer.js +13 -0
- package/build/test/fixtures/bigbuffer.js.map +1 -0
- package/build/test/helpers.d.ts +3 -0
- package/build/test/helpers.d.ts.map +1 -0
- package/build/test/helpers.js +15 -0
- package/build/test/helpers.js.map +1 -0
- package/build/test/subproc-specs.d.ts +2 -0
- package/build/test/subproc-specs.d.ts.map +1 -0
- package/build/test/subproc-specs.js +414 -0
- package/build/test/subproc-specs.js.map +1 -0
- package/build/tsconfig.tsbuildinfo +1 -0
- package/lib/circular-buffer.ts +1 -1
- package/lib/exec.ts +158 -0
- package/lib/helpers.ts +44 -0
- package/lib/index.ts +8 -0
- package/lib/subprocess.ts +353 -0
- package/lib/types.ts +95 -0
- package/package.json +5 -5
- package/index.js +0 -1
- package/lib/exec.js +0 -191
- package/lib/helpers.js +0 -38
- package/lib/index.js +0 -9
- package/lib/subprocess.js +0 -329
package/lib/exec.js
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
|
-
import { quote } from 'shell-quote';
|
|
3
|
-
import B from 'bluebird';
|
|
4
|
-
import _ from 'lodash';
|
|
5
|
-
import { formatEnoent } from './helpers';
|
|
6
|
-
import { CircularBuffer, MAX_BUFFER_SIZE } from './circular-buffer';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Spawns a process
|
|
10
|
-
* @template {TeenProcessExecOptions} T
|
|
11
|
-
* @param {string} cmd - Program to execute
|
|
12
|
-
* @param {string[]} [args] - Arguments to pass to the program
|
|
13
|
-
* @param {T} [originalOpts] - Options
|
|
14
|
-
* @returns {Promise<BufferProp<T> extends true ? TeenProcessExecBufferResult : TeenProcessExecStringResult>}
|
|
15
|
-
*/
|
|
16
|
-
async function exec (cmd, args = [], originalOpts = /** @type {T} */({})) {
|
|
17
|
-
// get a quoted representation of the command for error strings
|
|
18
|
-
const rep = quote([cmd, ...args]);
|
|
19
|
-
|
|
20
|
-
// extend default options; we're basically re-implementing exec's options
|
|
21
|
-
// for use here with spawn under the hood
|
|
22
|
-
const opts = /** @type {T} */(_.defaults(originalOpts, {
|
|
23
|
-
timeout: null,
|
|
24
|
-
encoding: 'utf8',
|
|
25
|
-
killSignal: 'SIGTERM',
|
|
26
|
-
cwd: undefined,
|
|
27
|
-
env: process.env,
|
|
28
|
-
ignoreOutput: false,
|
|
29
|
-
stdio: 'inherit',
|
|
30
|
-
isBuffer: false,
|
|
31
|
-
shell: undefined,
|
|
32
|
-
logger: undefined,
|
|
33
|
-
maxStdoutBufferSize: MAX_BUFFER_SIZE,
|
|
34
|
-
maxStderrBufferSize: MAX_BUFFER_SIZE,
|
|
35
|
-
}));
|
|
36
|
-
|
|
37
|
-
const isBuffer = Boolean(opts.isBuffer);
|
|
38
|
-
|
|
39
|
-
// this is an async function, so return a promise
|
|
40
|
-
return await new B((resolve, reject) => {
|
|
41
|
-
// spawn the child process with options; we don't currently expose any of
|
|
42
|
-
// the other 'spawn' options through the API
|
|
43
|
-
const proc = spawn(cmd, args, {cwd: opts.cwd, env: opts.env, shell: opts.shell});
|
|
44
|
-
const stdoutBuffer = new CircularBuffer(opts.maxStdoutBufferSize);
|
|
45
|
-
const stderrBuffer = new CircularBuffer(opts.maxStderrBufferSize);
|
|
46
|
-
let timer = null;
|
|
47
|
-
|
|
48
|
-
// if the process errors out, reject the promise
|
|
49
|
-
proc.on('error', /** @param {NodeJS.ErrnoException} err */ async (err) => {
|
|
50
|
-
if (err.code === 'ENOENT') {
|
|
51
|
-
err = await formatEnoent(err, cmd, opts.cwd?.toString());
|
|
52
|
-
}
|
|
53
|
-
reject(err);
|
|
54
|
-
});
|
|
55
|
-
if (proc.stdin) {
|
|
56
|
-
proc.stdin.on('error', /** @param {NodeJS.ErrnoException} err */(err) => {
|
|
57
|
-
reject(new Error(`Standard input '${err.syscall}' error: ${err.stack}`));
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
const handleStream = (/** @type {string} */ streamType, /** @type {CircularBuffer} */ buffer) => {
|
|
61
|
-
if (!proc[streamType]) {
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
proc[streamType].on('error', (err) => {
|
|
66
|
-
reject(new Error(`${_.capitalize(streamType)} '${err.syscall}' error: ${err.stack}`));
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
if (opts.ignoreOutput) {
|
|
70
|
-
// https://github.com/nodejs/node/issues/4236
|
|
71
|
-
proc[streamType].on('data', () => {});
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// keep track of the stream if we don't want to ignore it
|
|
76
|
-
proc[streamType].on('data', (/** @type {Buffer} */ chunk) => {
|
|
77
|
-
buffer.add(chunk);
|
|
78
|
-
if (opts.logger && _.isFunction(opts.logger.debug)) {
|
|
79
|
-
opts.logger.debug(chunk.toString());
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
};
|
|
83
|
-
handleStream('stdout', stdoutBuffer);
|
|
84
|
-
handleStream('stderr', stderrBuffer);
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* @template {boolean} U
|
|
88
|
-
* @param {U} isBuffer
|
|
89
|
-
* @returns {U extends true ? {stdout: Buffer, stderr: Buffer} : {stdout: string, stderr: string}}
|
|
90
|
-
*/
|
|
91
|
-
function getStdio (isBuffer) {
|
|
92
|
-
const stdout = isBuffer ? stdoutBuffer.value() : stdoutBuffer.value().toString(opts.encoding);
|
|
93
|
-
const stderr = isBuffer ? stderrBuffer.value() : stderrBuffer.value().toString(opts.encoding);
|
|
94
|
-
return /** @type {U extends true ? {stdout: Buffer, stderr: Buffer} : {stdout: string, stderr: string}} */(
|
|
95
|
-
{stdout, stderr}
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// if the process ends, either resolve or reject the promise based on the
|
|
100
|
-
// exit code of the process. either way, attach stdout, stderr, and code.
|
|
101
|
-
// Also clean up the timer if it exists
|
|
102
|
-
proc.on('close', (code) => {
|
|
103
|
-
if (timer) {
|
|
104
|
-
clearTimeout(timer);
|
|
105
|
-
}
|
|
106
|
-
const {stdout, stderr} = getStdio(isBuffer);
|
|
107
|
-
if (code === 0) {
|
|
108
|
-
resolve(/** @type {BufferProp<T> extends true ? TeenProcessExecBufferResult : TeenProcessExecStringResult} */({stdout, stderr, code}));
|
|
109
|
-
} else {
|
|
110
|
-
let err = new Error(`Command '${rep}' exited with code ${code}`);
|
|
111
|
-
err = Object.assign(err, {stdout, stderr, code});
|
|
112
|
-
reject(err);
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// if we set a timeout on the child process, cut into the execution and
|
|
117
|
-
// reject if the timeout is reached. Attach the stdout/stderr we currently
|
|
118
|
-
// have in case it's helpful in debugging
|
|
119
|
-
if (opts.timeout) {
|
|
120
|
-
timer = setTimeout(() => {
|
|
121
|
-
const {stdout, stderr} = getStdio(isBuffer);
|
|
122
|
-
let err = new Error(`Command '${rep}' timed out after ${opts.timeout}ms`);
|
|
123
|
-
err = Object.assign(err, {stdout, stderr, code: null});
|
|
124
|
-
reject(err);
|
|
125
|
-
// reject and THEN kill to avoid race conditions with the handlers
|
|
126
|
-
// above
|
|
127
|
-
proc.kill(opts.killSignal);
|
|
128
|
-
}, opts.timeout);
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export { exec };
|
|
134
|
-
export default exec;
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Options on top of `SpawnOptions`, unique to `teen_process.`
|
|
138
|
-
* @typedef {Object} TeenProcessProps
|
|
139
|
-
* @property {boolean} [ignoreOutput] - Ignore & discard all output
|
|
140
|
-
* @property {boolean} [isBuffer] - Return output as a Buffer
|
|
141
|
-
* @property {TeenProcessLogger} [logger] - Logger to use for debugging
|
|
142
|
-
* @property {number} [maxStdoutBufferSize] - Maximum size of `stdout` buffer
|
|
143
|
-
* @property {number} [maxStderrBufferSize] - Maximum size of `stderr` buffer
|
|
144
|
-
* @property {BufferEncoding} [encoding='utf8'] - Encoding to use for output
|
|
145
|
-
*/
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* A logger object understood by {@link exec teen_process.exec}.
|
|
149
|
-
* @typedef {Object} TeenProcessLogger
|
|
150
|
-
* @property {(...args: any[]) => void} debug
|
|
151
|
-
*/
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Options for {@link exec teen_process.exec}.
|
|
155
|
-
* @typedef {import('child_process').SpawnOptions & TeenProcessProps} TeenProcessExecOptions
|
|
156
|
-
*/
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* The value {@link exec teen_process.exec} resolves to when `isBuffer` is `false`
|
|
160
|
-
* @typedef {Object} TeenProcessExecStringResult
|
|
161
|
-
* @property {string} stdout - Stdout
|
|
162
|
-
* @property {string} stderr - Stderr
|
|
163
|
-
* @property {number?} code - Exit code
|
|
164
|
-
*/
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* The value {@link exec teen_process.exec} resolves to when `isBuffer` is `true`
|
|
168
|
-
* @typedef {Object} TeenProcessExecBufferResult
|
|
169
|
-
* @property {Buffer} stdout - Stdout
|
|
170
|
-
* @property {Buffer} stderr - Stderr
|
|
171
|
-
* @property {number?} code - Exit code
|
|
172
|
-
*/
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Extra props {@link exec teen_process.exec} adds to its error objects
|
|
176
|
-
* @typedef {Object} TeenProcessExecErrorProps
|
|
177
|
-
* @property {string} stdout - STDOUT
|
|
178
|
-
* @property {string} stderr - STDERR
|
|
179
|
-
* @property {number?} code - Exit code
|
|
180
|
-
*/
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Error thrown by {@link exec teen_process.exec}
|
|
184
|
-
* @typedef {Error & TeenProcessExecErrorProps} TeenProcessExecError
|
|
185
|
-
*/
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* @template {{isBuffer?: boolean}} MaybeBuffer
|
|
189
|
-
* @typedef {MaybeBuffer['isBuffer']} BufferProp
|
|
190
|
-
* @private
|
|
191
|
-
*/
|
package/lib/helpers.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
import fs from 'fs/promises';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Decorates ENOENT error received from a spawn system call
|
|
6
|
-
* with a more descriptive message, so it could be properly handled by a user.
|
|
7
|
-
*
|
|
8
|
-
* @param {NodeJS.ErrnoException} error Original error instance. !!! The instance is mutated after
|
|
9
|
-
* this helper function invocation
|
|
10
|
-
* @param {string} cmd Original command to execute
|
|
11
|
-
* @param {string?} [cwd] Optional path to the current working dir
|
|
12
|
-
* @returns {Promise<NodeJS.ErrnoException>} Mutated error instance with an improved description or an
|
|
13
|
-
* unchanged error instance
|
|
14
|
-
*/
|
|
15
|
-
async function formatEnoent (error, cmd, cwd = null) {
|
|
16
|
-
if (cwd) {
|
|
17
|
-
try {
|
|
18
|
-
const stat = await fs.stat(cwd);
|
|
19
|
-
if (!stat.isDirectory()) {
|
|
20
|
-
error.message = `The working directory '${cwd}' of '${cmd}' is not a valid folder path`;
|
|
21
|
-
return error;
|
|
22
|
-
}
|
|
23
|
-
} catch (e) {
|
|
24
|
-
if (e.code === 'ENOENT') {
|
|
25
|
-
error.message = `The working directory '${cwd}' of '${cmd}' does not exist`;
|
|
26
|
-
return error;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const curDir = path.resolve(cwd ?? process.cwd());
|
|
32
|
-
const pathMsg = process.env.PATH ?? 'which is not defined for the process';
|
|
33
|
-
error.message = `'${cmd}' executable is not found neither in the process working folder (${curDir}) ` +
|
|
34
|
-
`nor in any folders specified in the PATH environment variable (${pathMsg})`;
|
|
35
|
-
return error;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export { formatEnoent };
|
package/lib/index.js
DELETED
package/lib/subprocess.js
DELETED
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
|
-
import { EventEmitter } from 'events';
|
|
3
|
-
import B from 'bluebird';
|
|
4
|
-
import { quote } from 'shell-quote';
|
|
5
|
-
import _ from 'lodash';
|
|
6
|
-
import { formatEnoent } from './helpers';
|
|
7
|
-
import { createInterface } from 'node:readline';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* @template {SubProcessOptions} TSubProcessOptions
|
|
11
|
-
*/
|
|
12
|
-
export class SubProcess extends EventEmitter {
|
|
13
|
-
/**
|
|
14
|
-
* @callback StartDetector
|
|
15
|
-
* @param {TSubProcessOptions extends TIsBufferOpts ? Buffer : string} stdout
|
|
16
|
-
* @param {TSubProcessOptions extends TIsBufferOpts ? Buffer : string} [stderr]
|
|
17
|
-
* @returns {any}
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
/** @type {import('child_process').ChildProcess | null} */
|
|
21
|
-
proc;
|
|
22
|
-
/** @type {string[]} */
|
|
23
|
-
args;
|
|
24
|
-
/**
|
|
25
|
-
* @type {string}
|
|
26
|
-
*/
|
|
27
|
-
cmd;
|
|
28
|
-
/**
|
|
29
|
-
* @type {SubProcessOptions}
|
|
30
|
-
*/
|
|
31
|
-
opts;
|
|
32
|
-
/**
|
|
33
|
-
* @type {boolean}
|
|
34
|
-
*/
|
|
35
|
-
expectingExit;
|
|
36
|
-
/**
|
|
37
|
-
* @type {string}
|
|
38
|
-
*/
|
|
39
|
-
rep;
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* @param {string} cmd
|
|
43
|
-
* @param {string[]} [args=[]]
|
|
44
|
-
* @param {TSubProcessOptions} [opts]
|
|
45
|
-
*/
|
|
46
|
-
constructor (cmd, args = [], opts) {
|
|
47
|
-
super();
|
|
48
|
-
if (!cmd) throw new Error('Command is required'); // eslint-disable-line curly
|
|
49
|
-
if (!_.isString(cmd)) throw new Error('Command must be a string'); // eslint-disable-line curly
|
|
50
|
-
if (!_.isArray(args)) throw new Error('Args must be an array'); // eslint-disable-line curly
|
|
51
|
-
|
|
52
|
-
this.cmd = cmd;
|
|
53
|
-
this.args = args;
|
|
54
|
-
this.proc = null;
|
|
55
|
-
this.opts = opts ?? {};
|
|
56
|
-
this.expectingExit = false;
|
|
57
|
-
|
|
58
|
-
// get a quoted representation of the command for error strings
|
|
59
|
-
this.rep = quote([cmd, ...args]);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
get isRunning () {
|
|
63
|
-
// presence of `proc` means we have connected and started
|
|
64
|
-
return !!this.proc;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
*
|
|
69
|
-
* @param {string} streamName
|
|
70
|
-
* @param {Iterable<string>|string} lines
|
|
71
|
-
*/
|
|
72
|
-
emitLines (streamName, lines) {
|
|
73
|
-
const doEmit = (/** @type {string} */ line) => this.emit('stream-line', `[${streamName.toUpperCase()}] ${line}`);
|
|
74
|
-
|
|
75
|
-
if (_.isString(lines)) {
|
|
76
|
-
doEmit(lines);
|
|
77
|
-
} else {
|
|
78
|
-
for (const line of lines) {
|
|
79
|
-
doEmit(line);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* spawn the subprocess and return control whenever we deem that it has fully
|
|
86
|
-
* "started"
|
|
87
|
-
*
|
|
88
|
-
* @param {StartDetector|number?} startDetector
|
|
89
|
-
* @param {number?} timeoutMs
|
|
90
|
-
* @param {boolean} detach
|
|
91
|
-
* @returns {Promise<void>}
|
|
92
|
-
*/
|
|
93
|
-
async start (startDetector = null, timeoutMs = null, detach = false) {
|
|
94
|
-
let startDelay = 10;
|
|
95
|
-
|
|
96
|
-
const genericStartDetector = /** @type {StartDetector} */(function genericStartDetector (stdout, stderr) {
|
|
97
|
-
return stdout || stderr;
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
// the default start detector simply returns true when we get any output
|
|
101
|
-
if (startDetector === null) {
|
|
102
|
-
startDetector = genericStartDetector;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// if the user passes a number, then we simply delay a certain amount of
|
|
106
|
-
// time before returning control, rather than waiting for a condition
|
|
107
|
-
if (_.isNumber(startDetector)) {
|
|
108
|
-
startDelay = startDetector;
|
|
109
|
-
startDetector = null;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// if the user passes in a boolean as one of the arguments, use it for `detach`
|
|
113
|
-
if (_.isBoolean(startDetector) && startDetector) {
|
|
114
|
-
if (!this.opts.detached) {
|
|
115
|
-
throw new Error(`Unable to detach process that is not started with 'detached' option`);
|
|
116
|
-
}
|
|
117
|
-
detach = true;
|
|
118
|
-
startDetector = genericStartDetector;
|
|
119
|
-
} else if (_.isBoolean(timeoutMs) && timeoutMs) {
|
|
120
|
-
if (!this.opts.detached) {
|
|
121
|
-
throw new Error(`Unable to detach process that is not started with 'detached' option`);
|
|
122
|
-
}
|
|
123
|
-
detach = true;
|
|
124
|
-
timeoutMs = null;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// return a promise so we can wrap the async behavior
|
|
128
|
-
return await new B((resolve, reject) => {
|
|
129
|
-
// actually spawn the subproc
|
|
130
|
-
this.proc = spawn(this.cmd, this.args, this.opts);
|
|
131
|
-
|
|
132
|
-
// this function handles output that we collect from the subproc
|
|
133
|
-
/**
|
|
134
|
-
* @param { {
|
|
135
|
-
* stdout: TSubProcessOptions extends TIsBufferOpts ? Buffer : string,
|
|
136
|
-
* stderr: TSubProcessOptions extends TIsBufferOpts ? Buffer : string
|
|
137
|
-
* } } streams
|
|
138
|
-
*/
|
|
139
|
-
const handleOutput = (streams) => {
|
|
140
|
-
const {stdout, stderr} = streams;
|
|
141
|
-
// if we have a startDetector, run it on the output so we can resolve/
|
|
142
|
-
// reject and move on from start
|
|
143
|
-
try {
|
|
144
|
-
if (_.isFunction(startDetector) && startDetector(stdout, stderr)) {
|
|
145
|
-
startDetector = null;
|
|
146
|
-
resolve();
|
|
147
|
-
}
|
|
148
|
-
} catch (e) {
|
|
149
|
-
reject(e);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// emit the actual output for whomever's listening
|
|
153
|
-
this.emit('output', stdout, stderr);
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
// if we get an error spawning the proc, reject and clean up the proc
|
|
157
|
-
this.proc.on('error', /** @param {NodeJS.ErrnoException} err */ async (err) => {
|
|
158
|
-
this.proc?.removeAllListeners('exit');
|
|
159
|
-
this.proc?.kill('SIGINT');
|
|
160
|
-
|
|
161
|
-
if (err.code === 'ENOENT') {
|
|
162
|
-
err = await formatEnoent(err, this.cmd, this.opts?.cwd?.toString());
|
|
163
|
-
}
|
|
164
|
-
reject(err);
|
|
165
|
-
|
|
166
|
-
this.proc?.unref();
|
|
167
|
-
this.proc = null;
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
const handleStreamLines = (/** @type {string} */ streamName, /** @type {import('stream').Readable} */ input) => {
|
|
171
|
-
const rl = createInterface({input});
|
|
172
|
-
rl.on('line', (line) => {
|
|
173
|
-
// This event is a legacy one
|
|
174
|
-
// It always produces a single-item array
|
|
175
|
-
if (this.listenerCount(`lines-${streamName}`)) {
|
|
176
|
-
this.emit(`lines-${streamName}`, [line]);
|
|
177
|
-
}
|
|
178
|
-
this.emit(`line-${streamName}`, line);
|
|
179
|
-
if (this.listenerCount('stream-line')) {
|
|
180
|
-
this.emitLines(streamName, line);
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
const isBuffer = Boolean(this.opts.isBuffer);
|
|
186
|
-
const encoding = this.opts.encoding || 'utf8';
|
|
187
|
-
|
|
188
|
-
if (this.proc.stdout) {
|
|
189
|
-
this.proc.stdout.on('data', (chunk) =>
|
|
190
|
-
handleOutput({
|
|
191
|
-
stdout: isBuffer ? chunk : chunk.toString(encoding),
|
|
192
|
-
// @ts-ignore This is OK
|
|
193
|
-
stderr: isBuffer ? Buffer.alloc(0) : '',
|
|
194
|
-
}),
|
|
195
|
-
);
|
|
196
|
-
handleStreamLines('stdout', this.proc.stdout);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (this.proc.stderr) {
|
|
200
|
-
this.proc.stderr.on('data', (chunk) =>
|
|
201
|
-
handleOutput({
|
|
202
|
-
// @ts-ignore This is OK
|
|
203
|
-
stdout: isBuffer ? Buffer.alloc(0) : '',
|
|
204
|
-
stderr: isBuffer ? chunk : chunk.toString(encoding)
|
|
205
|
-
}),
|
|
206
|
-
);
|
|
207
|
-
handleStreamLines('stderr', this.proc.stderr);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// when the proc exits, we might still have a buffer of lines we were
|
|
211
|
-
// waiting on more chunks to complete. Go ahead and emit those, then
|
|
212
|
-
// re-emit the exit so a listener can handle the possibly-unexpected exit
|
|
213
|
-
this.proc.on('exit', (code, signal) => {
|
|
214
|
-
this.emit('exit', code, signal);
|
|
215
|
-
|
|
216
|
-
// in addition to the bare exit event, also emit one of three other
|
|
217
|
-
// events that contain more helpful information:
|
|
218
|
-
// 'stop': we stopped this
|
|
219
|
-
// 'die': the process ended out of our control with a non-zero exit
|
|
220
|
-
// 'end': the process ended out of our control with a zero exit
|
|
221
|
-
let event = this.expectingExit ? 'stop' : 'die';
|
|
222
|
-
if (!this.expectingExit && code === 0) {
|
|
223
|
-
event = 'end';
|
|
224
|
-
}
|
|
225
|
-
this.emit(event, code, signal);
|
|
226
|
-
|
|
227
|
-
// finally clean up the proc and make sure to reset our exit
|
|
228
|
-
// expectations
|
|
229
|
-
this.proc = null;
|
|
230
|
-
this.expectingExit = false;
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
// if the user hasn't given us a startDetector, instead just resolve
|
|
234
|
-
// when startDelay ms have passed
|
|
235
|
-
if (!startDetector) {
|
|
236
|
-
setTimeout(() => { resolve(); }, startDelay);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// if the user has given us a timeout, start the clock for rejecting
|
|
240
|
-
// the promise if we take too long to start
|
|
241
|
-
if (_.isNumber(timeoutMs)) {
|
|
242
|
-
setTimeout(() => {
|
|
243
|
-
reject(new Error(`The process did not start within ${timeoutMs}ms ` +
|
|
244
|
-
`(cmd: '${this.rep}')`));
|
|
245
|
-
}, timeoutMs);
|
|
246
|
-
}
|
|
247
|
-
}).finally(() => {
|
|
248
|
-
if (detach && this.proc) {
|
|
249
|
-
this.proc.unref();
|
|
250
|
-
}
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* @deprecated This method is deprecated and will be removed
|
|
256
|
-
*/
|
|
257
|
-
handleLastLines () {
|
|
258
|
-
// TODO: THis is a noop left for backward compatibility.
|
|
259
|
-
// TODO: Remove it after the major version bump
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
*
|
|
264
|
-
* @param {NodeJS.Signals} signal
|
|
265
|
-
* @param {number} timeout
|
|
266
|
-
* @returns {Promise<void>}
|
|
267
|
-
*/
|
|
268
|
-
async stop (signal = 'SIGTERM', timeout = 10000) {
|
|
269
|
-
if (!this.isRunning) {
|
|
270
|
-
throw new Error(`Can't stop process; it's not currently running (cmd: '${this.rep}')`);
|
|
271
|
-
}
|
|
272
|
-
return await new B((resolve, reject) => {
|
|
273
|
-
this.proc?.on('close', resolve);
|
|
274
|
-
this.expectingExit = true;
|
|
275
|
-
this.proc?.kill(signal);
|
|
276
|
-
// this timeout needs unref() or node will wait for the timeout to fire before
|
|
277
|
-
// exiting the process.
|
|
278
|
-
setTimeout(() => {
|
|
279
|
-
reject(new Error(`Process didn't end after ${timeout}ms (cmd: '${this.rep}')`));
|
|
280
|
-
}, timeout).unref();
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
async join (allowedExitCodes = [0]) {
|
|
285
|
-
if (!this.isRunning) {
|
|
286
|
-
throw new Error(`Cannot join process; it is not currently running (cmd: '${this.rep}')`);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
return await new B((resolve, reject) => {
|
|
290
|
-
this.proc?.on('exit', (code) => {
|
|
291
|
-
if (code !== null && allowedExitCodes.indexOf(code) === -1) {
|
|
292
|
-
reject(new Error(`Process ended with exitcode ${code} (cmd: '${this.rep}')`));
|
|
293
|
-
} else {
|
|
294
|
-
resolve(code);
|
|
295
|
-
}
|
|
296
|
-
});
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/*
|
|
301
|
-
* This will only work if the process is created with the `detached` option
|
|
302
|
-
*/
|
|
303
|
-
detachProcess () {
|
|
304
|
-
if (!this.opts.detached) {
|
|
305
|
-
// this means that there is a misconfiguration in the calling code
|
|
306
|
-
throw new Error(`Unable to detach process that is not started with 'detached' option`);
|
|
307
|
-
}
|
|
308
|
-
if (this.proc) {
|
|
309
|
-
this.proc.unref();
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
get pid () {
|
|
314
|
-
return this.proc ? this.proc.pid : null;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
export default SubProcess;
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* @typedef {Object} SubProcessCustomOptions
|
|
322
|
-
* @property {boolean} [isBuffer]
|
|
323
|
-
* @property {string} [encoding]
|
|
324
|
-
*/
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* @typedef {SubProcessCustomOptions & import('child_process').SpawnOptionsWithoutStdio} SubProcessOptions
|
|
328
|
-
* @typedef {{isBuffer: true}} TIsBufferOpts
|
|
329
|
-
*/
|