spawn-rx 5.1.2 → 6.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/biome.json +15 -0
- package/bun.lock +417 -0
- package/esdoc.json +1 -4
- package/lib/{src/index.d.ts → index.d.ts} +67 -75
- package/lib/index.js +451 -0
- package/lib/index.js.map +1 -0
- package/package.json +14 -26
- package/src/index.ts +284 -335
- package/test/{asserttest.ts → asserttest.test.ts} +4 -5
- package/test/{spawn.ts → spawn.test.ts} +47 -75
- package/tsconfig.json +1 -1
- package/eslint.config.mjs +0 -88
- package/lib/src/index.js +0 -388
- package/lib/src/index.js.map +0 -1
- package/src/ambient.d.ts +0 -1
- package/test/support.ts +0 -15
- package/vendor/jobber/Jobber.exe +0 -0
package/src/index.ts
CHANGED
|
@@ -1,18 +1,58 @@
|
|
|
1
|
-
|
|
2
|
-
import * as
|
|
3
|
-
import * as
|
|
4
|
-
import * as
|
|
1
|
+
import { type SpawnOptions, spawn as spawnOg } from "node:child_process";
|
|
2
|
+
import * as sfs from "node:fs";
|
|
3
|
+
import * as fs from "node:fs/promises";
|
|
4
|
+
import * as path from "node:path";
|
|
5
5
|
|
|
6
|
-
import type { Observer, Subject } from "rxjs";
|
|
7
|
-
import { Observable, Subscription, AsyncSubject, of, merge } from "rxjs";
|
|
8
|
-
import { map, reduce } from "rxjs/operators";
|
|
9
|
-
import { spawn as spawnOg, SpawnOptions } from "child_process";
|
|
10
6
|
import Debug from "debug";
|
|
7
|
+
import { LRUCache } from "lru-cache";
|
|
8
|
+
import type { Observer, Subject } from "rxjs";
|
|
9
|
+
import { AsyncSubject, merge, Observable, of, Subscription, timer } from "rxjs";
|
|
10
|
+
import { map, reduce, retry as rxRetry } from "rxjs/operators";
|
|
11
11
|
|
|
12
12
|
const isWindows = process.platform === "win32";
|
|
13
13
|
|
|
14
14
|
const d = Debug("spawn-rx"); // tslint:disable-line:no-var-requires
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Custom error class for spawn operations with additional metadata
|
|
18
|
+
*/
|
|
19
|
+
export class SpawnError extends Error {
|
|
20
|
+
public readonly exitCode: number;
|
|
21
|
+
public readonly code: number;
|
|
22
|
+
public readonly stdout?: string;
|
|
23
|
+
public readonly stderr?: string;
|
|
24
|
+
public readonly command: string;
|
|
25
|
+
public readonly args: string[];
|
|
26
|
+
|
|
27
|
+
constructor(message: string, exitCode: number, command: string, args: string[], stdout?: string, stderr?: string) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = "SpawnError";
|
|
30
|
+
this.exitCode = exitCode;
|
|
31
|
+
this.code = exitCode;
|
|
32
|
+
this.stdout = stdout;
|
|
33
|
+
this.stderr = stderr;
|
|
34
|
+
this.command = command;
|
|
35
|
+
this.args = args;
|
|
36
|
+
|
|
37
|
+
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
+
if ((Error as any).captureStackTrace) {
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
41
|
+
(Error as any).captureStackTrace(this, SpawnError);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Process metadata tracked during execution
|
|
48
|
+
*/
|
|
49
|
+
export interface ProcessMetadata {
|
|
50
|
+
pid: number;
|
|
51
|
+
startTime: number;
|
|
52
|
+
command: string;
|
|
53
|
+
args: string[];
|
|
54
|
+
}
|
|
55
|
+
|
|
16
56
|
/**
|
|
17
57
|
* stat a file but don't throw if it doesn't exist
|
|
18
58
|
*
|
|
@@ -21,7 +61,7 @@ const d = Debug("spawn-rx"); // tslint:disable-line:no-var-requires
|
|
|
21
61
|
*
|
|
22
62
|
* @private
|
|
23
63
|
*/
|
|
24
|
-
function statSyncNoException(file: string): sfs.Stats | null {
|
|
64
|
+
export function statSyncNoException(file: string): sfs.Stats | null {
|
|
25
65
|
try {
|
|
26
66
|
return sfs.statSync(file);
|
|
27
67
|
} catch {
|
|
@@ -29,6 +69,23 @@ function statSyncNoException(file: string): sfs.Stats | null {
|
|
|
29
69
|
}
|
|
30
70
|
}
|
|
31
71
|
|
|
72
|
+
/**
|
|
73
|
+
* stat a file but don't throw if it doesn't exist
|
|
74
|
+
*
|
|
75
|
+
* @param {string} file The path to a file
|
|
76
|
+
* @return {Stats} The stats structure
|
|
77
|
+
*
|
|
78
|
+
* @private
|
|
79
|
+
*/
|
|
80
|
+
export function statNoException(file: string): Promise<sfs.Stats | null> {
|
|
81
|
+
return fs.stat(file).catch(() => null);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Cache for resolved executable paths
|
|
86
|
+
*/
|
|
87
|
+
const pathCache = new LRUCache<string, string>({ max: 512 });
|
|
88
|
+
|
|
32
89
|
/**
|
|
33
90
|
* Search PATH to see if a file exists in any of the path folders.
|
|
34
91
|
*
|
|
@@ -39,38 +96,56 @@ function statSyncNoException(file: string): sfs.Stats | null {
|
|
|
39
96
|
* @private
|
|
40
97
|
*/
|
|
41
98
|
function runDownPath(exe: string): string {
|
|
99
|
+
// Check cache first
|
|
100
|
+
const cached = pathCache.get(exe);
|
|
101
|
+
if (cached !== undefined) {
|
|
102
|
+
d(`Cache hit for executable: ${exe} -> ${cached}`);
|
|
103
|
+
return cached;
|
|
104
|
+
}
|
|
105
|
+
|
|
42
106
|
// NB: Windows won't search PATH looking for executables in spawn like
|
|
43
107
|
// Posix does
|
|
44
108
|
|
|
45
109
|
// Files with any directory path don't get this applied
|
|
46
110
|
if (exe.match(/[\\/]/)) {
|
|
47
111
|
d("Path has slash in directory, bailing");
|
|
112
|
+
pathCache.set(exe, exe);
|
|
48
113
|
return exe;
|
|
49
114
|
}
|
|
50
115
|
|
|
51
116
|
const target = path.join(".", exe);
|
|
52
117
|
if (statSyncNoException(target)) {
|
|
53
|
-
d(`Found executable in
|
|
118
|
+
d(`Found executable in current directory: ${target}`);
|
|
54
119
|
|
|
55
120
|
// XXX: Some very Odd programs decide to use args[0] as a parameter
|
|
56
121
|
// to determine what to do, and also symlink themselves, so we can't
|
|
57
122
|
// use realpathSync here like we used to
|
|
123
|
+
pathCache.set(exe, target);
|
|
58
124
|
return target;
|
|
59
125
|
}
|
|
60
126
|
|
|
61
|
-
const haystack = process.env.PATH
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
127
|
+
const haystack = process.env.PATH?.split(isWindows ? ";" : ":");
|
|
128
|
+
if (haystack) {
|
|
129
|
+
for (const p of haystack) {
|
|
130
|
+
const needle = path.join(p, exe);
|
|
131
|
+
if (statSyncNoException(needle)) {
|
|
132
|
+
// NB: Same deal as above
|
|
133
|
+
pathCache.set(exe, needle);
|
|
134
|
+
return needle;
|
|
135
|
+
}
|
|
67
136
|
}
|
|
68
137
|
}
|
|
69
138
|
|
|
70
139
|
d("Failed to find executable anywhere in path");
|
|
140
|
+
pathCache.set(exe, exe);
|
|
71
141
|
return exe;
|
|
72
142
|
}
|
|
73
143
|
|
|
144
|
+
export type CmdWithArgs = {
|
|
145
|
+
cmd: string;
|
|
146
|
+
args: string[];
|
|
147
|
+
};
|
|
148
|
+
|
|
74
149
|
/**
|
|
75
150
|
* Finds the actual executable and parameters to run on Windows. This method
|
|
76
151
|
* mimics the POSIX behavior of being able to run scripts as executables by
|
|
@@ -87,13 +162,7 @@ function runDownPath(exe: string): string {
|
|
|
87
162
|
* @property {string} cmd The command to pass to spawn
|
|
88
163
|
* @property {string[]} args The arguments to pass to spawn
|
|
89
164
|
*/
|
|
90
|
-
export function findActualExecutable(
|
|
91
|
-
exe: string,
|
|
92
|
-
args: string[],
|
|
93
|
-
): {
|
|
94
|
-
cmd: string;
|
|
95
|
-
args: string[];
|
|
96
|
-
} {
|
|
165
|
+
export function findActualExecutable(exe: string, args: string[]): CmdWithArgs {
|
|
97
166
|
// POSIX can just execute scripts directly, no need for silly goosery
|
|
98
167
|
if (process.platform !== "win32") {
|
|
99
168
|
return { cmd: runDownPath(exe), args: args };
|
|
@@ -114,21 +183,8 @@ export function findActualExecutable(
|
|
|
114
183
|
}
|
|
115
184
|
|
|
116
185
|
if (exe.match(/\.ps1$/i)) {
|
|
117
|
-
const cmd = path.join(
|
|
118
|
-
|
|
119
|
-
"System32",
|
|
120
|
-
"WindowsPowerShell",
|
|
121
|
-
"v1.0",
|
|
122
|
-
"PowerShell.exe",
|
|
123
|
-
);
|
|
124
|
-
const psargs = [
|
|
125
|
-
"-ExecutionPolicy",
|
|
126
|
-
"Unrestricted",
|
|
127
|
-
"-NoLogo",
|
|
128
|
-
"-NonInteractive",
|
|
129
|
-
"-File",
|
|
130
|
-
exe,
|
|
131
|
-
];
|
|
186
|
+
const cmd = path.join(process.env.SYSTEMROOT!, "System32", "WindowsPowerShell", "v1.0", "PowerShell.exe");
|
|
187
|
+
const psargs = ["-ExecutionPolicy", "Unrestricted", "-NoLogo", "-NonInteractive", "-File", exe];
|
|
132
188
|
|
|
133
189
|
return { cmd: cmd, args: psargs.concat(args) };
|
|
134
190
|
}
|
|
@@ -155,8 +211,21 @@ export type SpawnRxExtras = {
|
|
|
155
211
|
stdin?: Observable<string>;
|
|
156
212
|
echoOutput?: boolean;
|
|
157
213
|
split?: boolean;
|
|
158
|
-
jobber?: boolean;
|
|
159
214
|
encoding?: BufferEncoding;
|
|
215
|
+
/**
|
|
216
|
+
* Timeout in milliseconds. If the process doesn't complete within this time,
|
|
217
|
+
* it will be killed and the observable will error with a TimeoutError.
|
|
218
|
+
*/
|
|
219
|
+
timeout?: number;
|
|
220
|
+
/**
|
|
221
|
+
* Number of retry attempts if the process fails (non-zero exit code).
|
|
222
|
+
* Defaults to 0 (no retries).
|
|
223
|
+
*/
|
|
224
|
+
retries?: number;
|
|
225
|
+
/**
|
|
226
|
+
* Delay in milliseconds between retry attempts. Defaults to 1000ms.
|
|
227
|
+
*/
|
|
228
|
+
retryDelay?: number;
|
|
160
229
|
};
|
|
161
230
|
|
|
162
231
|
export type OutputLine = {
|
|
@@ -165,102 +234,20 @@ export type OutputLine = {
|
|
|
165
234
|
};
|
|
166
235
|
|
|
167
236
|
/**
|
|
168
|
-
*
|
|
169
|
-
* into its own Process Group that can be killed by unsubscribing from the
|
|
170
|
-
* return Observable.
|
|
171
|
-
*
|
|
172
|
-
* @param {string} exe The executable to run
|
|
173
|
-
* @param {string[]} params The parameters to pass to the child
|
|
174
|
-
* @param {SpawnOptions & SpawnRxExtras} opts Options to pass to spawn.
|
|
175
|
-
*
|
|
176
|
-
* @return {Observable<OutputLine>} Returns an Observable that when subscribed
|
|
177
|
-
* to, will create a detached process. The
|
|
178
|
-
* process output will be streamed to this
|
|
179
|
-
* Observable, and if unsubscribed from, the
|
|
180
|
-
* process will be terminated early. If the
|
|
181
|
-
* process terminates with a non-zero value,
|
|
182
|
-
* the Observable will terminate with onError.
|
|
237
|
+
* Utility type to extract the return type based on split option
|
|
183
238
|
*/
|
|
184
|
-
export
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
opts: SpawnOptions & SpawnRxExtras & { split: true },
|
|
188
|
-
): Observable<OutputLine>;
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Spawns a process but detached from the current process. The process is put
|
|
192
|
-
* into its own Process Group that can be killed by unsubscribing from the
|
|
193
|
-
* return Observable.
|
|
194
|
-
*
|
|
195
|
-
* @param {string} exe The executable to run
|
|
196
|
-
* @param {string[]} params The parameters to pass to the child
|
|
197
|
-
* @param {SpawnOptions & SpawnRxExtras} opts Options to pass to spawn.
|
|
198
|
-
*
|
|
199
|
-
* @return {Observable<string>} Returns an Observable that when subscribed
|
|
200
|
-
* to, will create a detached process. The
|
|
201
|
-
* process output will be streamed to this
|
|
202
|
-
* Observable, and if unsubscribed from, the
|
|
203
|
-
* process will be terminated early. If the
|
|
204
|
-
* process terminates with a non-zero value,
|
|
205
|
-
* the Observable will terminate with onError.
|
|
206
|
-
*/
|
|
207
|
-
export function spawnDetached(
|
|
208
|
-
exe: string,
|
|
209
|
-
params: string[],
|
|
210
|
-
opts?: SpawnOptions & SpawnRxExtras & { split: false | undefined },
|
|
211
|
-
): Observable<string>;
|
|
239
|
+
export type SpawnResult<T extends SpawnRxExtras> = T extends { split: true }
|
|
240
|
+
? Observable<OutputLine>
|
|
241
|
+
: Observable<string>;
|
|
212
242
|
|
|
213
243
|
/**
|
|
214
|
-
*
|
|
215
|
-
* into its own Process Group that can be killed by unsubscribing from the
|
|
216
|
-
* return Observable.
|
|
217
|
-
*
|
|
218
|
-
* @param {string} exe The executable to run
|
|
219
|
-
* @param {string[]} params The parameters to pass to the child
|
|
220
|
-
* @param {SpawnOptions & SpawnRxExtras} opts Options to pass to spawn.
|
|
221
|
-
*
|
|
222
|
-
* @return {Observable<string>} Returns an Observable that when subscribed
|
|
223
|
-
* to, will create a detached process. The
|
|
224
|
-
* process output will be streamed to this
|
|
225
|
-
* Observable, and if unsubscribed from, the
|
|
226
|
-
* process will be terminated early. If the
|
|
227
|
-
* process terminates with a non-zero value,
|
|
228
|
-
* the Observable will terminate with onError.
|
|
244
|
+
* Utility type to extract the promise return type based on split option
|
|
229
245
|
*/
|
|
230
|
-
export
|
|
231
|
-
|
|
232
|
-
params: string[],
|
|
233
|
-
opts?: SpawnOptions & SpawnRxExtras,
|
|
234
|
-
): Observable<string> | Observable<OutputLine> {
|
|
235
|
-
const { cmd, args } = findActualExecutable(exe, params ?? []);
|
|
236
|
-
|
|
237
|
-
if (!isWindows) {
|
|
238
|
-
return spawn(
|
|
239
|
-
cmd,
|
|
240
|
-
args,
|
|
241
|
-
Object.assign({}, opts || {}, { detached: true }) as any,
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const newParams = [cmd].concat(args);
|
|
246
|
-
|
|
247
|
-
const target = path.join(
|
|
248
|
-
__dirname,
|
|
249
|
-
"..",
|
|
250
|
-
"..",
|
|
251
|
-
"vendor",
|
|
252
|
-
"jobber",
|
|
253
|
-
"Jobber.exe",
|
|
254
|
-
);
|
|
255
|
-
const options = {
|
|
256
|
-
...(opts ?? {}),
|
|
257
|
-
detached: true,
|
|
258
|
-
jobber: true,
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
d(`spawnDetached: ${target}, ${newParams}`);
|
|
262
|
-
return spawn(target, newParams, options as any);
|
|
246
|
+
export type SpawnPromiseResult<T extends SpawnRxExtras> = T extends {
|
|
247
|
+
split: true;
|
|
263
248
|
}
|
|
249
|
+
? Promise<[string, string]>
|
|
250
|
+
: Promise<string>;
|
|
264
251
|
|
|
265
252
|
/**
|
|
266
253
|
* Spawns a process attached as a child of the current process.
|
|
@@ -325,133 +312,167 @@ export function spawn(
|
|
|
325
312
|
opts?: SpawnOptions & SpawnRxExtras,
|
|
326
313
|
): Observable<string> | Observable<OutputLine> {
|
|
327
314
|
opts = opts ?? {};
|
|
328
|
-
const spawnObs: Observable<OutputLine> = new Observable(
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
} catch {
|
|
357
|
-
chunk = `<< Lost chunk of process output for ${exe} - length was ${b.length}>>`;
|
|
358
|
-
}
|
|
315
|
+
const spawnObs: Observable<OutputLine> = new Observable((subj: Observer<OutputLine>) => {
|
|
316
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
317
|
+
const { encoding, timeout, ...spawnOpts } = opts;
|
|
318
|
+
const { cmd, args } = findActualExecutable(exe, params);
|
|
319
|
+
d(`spawning process: ${cmd} ${args.join()}, ${JSON.stringify(spawnOpts)}`);
|
|
320
|
+
|
|
321
|
+
const proc = spawnOg(cmd, args, spawnOpts);
|
|
322
|
+
// Process metadata is tracked but not currently exposed
|
|
323
|
+
// Could be added to SpawnError or returned in a future enhancement
|
|
324
|
+
// const _processMetadata: ProcessMetadata = {
|
|
325
|
+
// pid: proc.pid ?? 0,
|
|
326
|
+
// startTime: Date.now(),
|
|
327
|
+
// command: cmd,
|
|
328
|
+
// args: args,
|
|
329
|
+
// };
|
|
330
|
+
|
|
331
|
+
// Set up timeout if specified
|
|
332
|
+
let timeoutHandle: NodeJS.Timeout | null = null;
|
|
333
|
+
if (timeout && timeout > 0) {
|
|
334
|
+
timeoutHandle = setTimeout(() => {
|
|
335
|
+
d(`Process timeout reached: ${cmd} ${args.join()}`);
|
|
336
|
+
if (!proc.killed) {
|
|
337
|
+
proc.kill();
|
|
338
|
+
}
|
|
339
|
+
const error = new SpawnError(`Process timed out after ${timeout}ms`, -1, cmd, args);
|
|
340
|
+
subj.error(error);
|
|
341
|
+
}, timeout);
|
|
342
|
+
}
|
|
359
343
|
|
|
360
|
-
|
|
361
|
-
|
|
344
|
+
const bufHandler = (source: "stdout" | "stderr") => (b: string | Buffer) => {
|
|
345
|
+
if (b.length < 1) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
362
348
|
|
|
363
|
-
|
|
349
|
+
if (opts.echoOutput) {
|
|
350
|
+
(source === "stdout" ? process.stdout : process.stderr).write(b);
|
|
351
|
+
}
|
|
364
352
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
next: (x: any) => proc.stdin.write(x),
|
|
370
|
-
error: subj.error.bind(subj),
|
|
371
|
-
complete: () => proc.stdin.end(),
|
|
372
|
-
}),
|
|
373
|
-
);
|
|
353
|
+
let chunk = "<< String sent back was too long >>";
|
|
354
|
+
try {
|
|
355
|
+
if (typeof b === "string") {
|
|
356
|
+
chunk = b.toString();
|
|
374
357
|
} else {
|
|
375
|
-
|
|
376
|
-
new Error(
|
|
377
|
-
`opts.stdio conflicts with provided spawn opts.stdin observable, 'pipe' is required`,
|
|
378
|
-
),
|
|
379
|
-
);
|
|
358
|
+
chunk = b.toString(encoding || "utf8");
|
|
380
359
|
}
|
|
360
|
+
} catch {
|
|
361
|
+
chunk = `<< Lost chunk of process output for ${exe} - length was ${b.length}>>`;
|
|
381
362
|
}
|
|
382
363
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
let noClose = false;
|
|
364
|
+
subj.next({ source: source, text: chunk });
|
|
365
|
+
};
|
|
386
366
|
|
|
387
|
-
|
|
388
|
-
stdoutCompleted = new AsyncSubject<boolean>();
|
|
389
|
-
proc.stdout.on("data", bufHandler("stdout"));
|
|
390
|
-
proc.stdout.on("close", () => {
|
|
391
|
-
(stdoutCompleted! as Subject<boolean>).next(true);
|
|
392
|
-
(stdoutCompleted! as Subject<boolean>).complete();
|
|
393
|
-
});
|
|
394
|
-
} else {
|
|
395
|
-
stdoutCompleted = of(true);
|
|
396
|
-
}
|
|
367
|
+
const ret = new Subscription();
|
|
397
368
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
proc.
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
369
|
+
if (opts.stdin) {
|
|
370
|
+
if (proc.stdin) {
|
|
371
|
+
const stdin = proc.stdin;
|
|
372
|
+
ret.add(
|
|
373
|
+
opts.stdin.subscribe({
|
|
374
|
+
next: (x) => stdin.write(x),
|
|
375
|
+
error: subj.error.bind(subj),
|
|
376
|
+
complete: () => stdin.end(),
|
|
377
|
+
}),
|
|
378
|
+
);
|
|
405
379
|
} else {
|
|
406
|
-
|
|
380
|
+
subj.error(new Error(`opts.stdio conflicts with provided spawn opts.stdin observable, 'pipe' is required`));
|
|
407
381
|
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
let stderrCompleted: Subject<boolean> | Observable<boolean> | null = null;
|
|
385
|
+
let stdoutCompleted: Subject<boolean> | Observable<boolean> | null = null;
|
|
386
|
+
let noClose = false;
|
|
408
387
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
388
|
+
if (proc.stdout) {
|
|
389
|
+
stdoutCompleted = new AsyncSubject<boolean>();
|
|
390
|
+
proc.stdout.on("data", bufHandler("stdout"));
|
|
391
|
+
proc.stdout.on("close", () => {
|
|
392
|
+
(stdoutCompleted as Subject<boolean>).next(true);
|
|
393
|
+
(stdoutCompleted as Subject<boolean>).complete();
|
|
412
394
|
});
|
|
395
|
+
} else {
|
|
396
|
+
stdoutCompleted = of(true);
|
|
397
|
+
}
|
|
413
398
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
);
|
|
399
|
+
if (proc.stderr) {
|
|
400
|
+
stderrCompleted = new AsyncSubject<boolean>();
|
|
401
|
+
proc.stderr.on("data", bufHandler("stderr"));
|
|
402
|
+
proc.stderr.on("close", () => {
|
|
403
|
+
(stderrCompleted as Subject<boolean>).next(true);
|
|
404
|
+
(stderrCompleted as Subject<boolean>).complete();
|
|
405
|
+
});
|
|
406
|
+
} else {
|
|
407
|
+
stderrCompleted = of(true);
|
|
408
|
+
}
|
|
419
409
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
410
|
+
proc.on("error", (e: Error) => {
|
|
411
|
+
noClose = true;
|
|
412
|
+
if (timeoutHandle) {
|
|
413
|
+
clearTimeout(timeoutHandle);
|
|
414
|
+
}
|
|
415
|
+
subj.error(e);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
proc.on("close", (code: number) => {
|
|
419
|
+
noClose = true;
|
|
420
|
+
if (timeoutHandle) {
|
|
421
|
+
clearTimeout(timeoutHandle);
|
|
422
|
+
}
|
|
423
|
+
const pipesClosed = merge(stdoutCompleted, stderrCompleted).pipe(reduce((_acc: boolean) => true, true));
|
|
424
|
+
|
|
425
|
+
if (code === 0) {
|
|
426
|
+
pipesClosed.subscribe(() => subj.complete());
|
|
427
|
+
} else {
|
|
428
|
+
pipesClosed.subscribe(() => {
|
|
429
|
+
const error = new SpawnError(`Process failed with exit code: ${code}`, code, cmd, args);
|
|
430
|
+
subj.error(error);
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
});
|
|
427
434
|
|
|
428
|
-
|
|
429
|
-
|
|
435
|
+
ret.add(
|
|
436
|
+
new Subscription(() => {
|
|
437
|
+
if (noClose) {
|
|
438
|
+
return;
|
|
430
439
|
}
|
|
431
|
-
});
|
|
432
440
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
441
|
+
if (timeoutHandle) {
|
|
442
|
+
clearTimeout(timeoutHandle);
|
|
443
|
+
}
|
|
438
444
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
setTimeout(() => proc.kill(), 5 * 1000);
|
|
444
|
-
} else {
|
|
445
|
-
proc.kill();
|
|
446
|
-
}
|
|
447
|
-
}),
|
|
448
|
-
);
|
|
445
|
+
d(`Killing process: ${cmd} ${args.join()}`);
|
|
446
|
+
proc.kill();
|
|
447
|
+
}),
|
|
448
|
+
);
|
|
449
449
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
450
|
+
return ret;
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
let resultObs: Observable<OutputLine> = spawnObs;
|
|
454
|
+
|
|
455
|
+
// Apply retry logic if specified
|
|
456
|
+
if (opts.retries && opts.retries > 0) {
|
|
457
|
+
const retryCount = opts.retries;
|
|
458
|
+
const delay = opts.retryDelay ?? 1000;
|
|
459
|
+
resultObs = resultObs.pipe(
|
|
460
|
+
rxRetry({
|
|
461
|
+
count: retryCount,
|
|
462
|
+
delay: (error: unknown, retryIndex: number) => {
|
|
463
|
+
// Only retry on SpawnError with non-zero exit codes
|
|
464
|
+
if (error instanceof SpawnError && error.exitCode !== 0) {
|
|
465
|
+
d(`Retrying process (attempt ${retryIndex + 1}/${retryCount}): ${exe}`);
|
|
466
|
+
return timer(delay);
|
|
467
|
+
}
|
|
468
|
+
// Don't retry on other errors
|
|
469
|
+
throw error;
|
|
470
|
+
},
|
|
471
|
+
}),
|
|
472
|
+
);
|
|
473
|
+
}
|
|
453
474
|
|
|
454
|
-
return opts.split ?
|
|
475
|
+
return opts.split ? resultObs : resultObs.pipe(map((x: OutputLine) => x?.text));
|
|
455
476
|
}
|
|
456
477
|
|
|
457
478
|
function wrapObservableInPromise(obs: Observable<string>) {
|
|
@@ -459,14 +480,17 @@ function wrapObservableInPromise(obs: Observable<string>) {
|
|
|
459
480
|
let out = "";
|
|
460
481
|
|
|
461
482
|
obs.subscribe({
|
|
462
|
-
next: (x) =>
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
err
|
|
483
|
+
next: (x: string) => {
|
|
484
|
+
out += x;
|
|
485
|
+
},
|
|
486
|
+
error: (e: unknown) => {
|
|
487
|
+
if (e instanceof SpawnError) {
|
|
488
|
+
const err = new SpawnError(`${out}\n${e.message}`, e.exitCode, e.command, e.args, out, e.stderr);
|
|
489
|
+
rej(err);
|
|
490
|
+
} else {
|
|
491
|
+
const err = new Error(`${out}\n${e instanceof Error ? e.message : String(e)}`);
|
|
492
|
+
rej(err);
|
|
468
493
|
}
|
|
469
|
-
rej(err);
|
|
470
494
|
},
|
|
471
495
|
complete: () => res(out),
|
|
472
496
|
});
|
|
@@ -479,93 +503,27 @@ function wrapObservableInSplitPromise(obs: Observable<OutputLine>) {
|
|
|
479
503
|
let err = "";
|
|
480
504
|
|
|
481
505
|
obs.subscribe({
|
|
482
|
-
next: (x) =>
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
506
|
+
next: (x: OutputLine) => {
|
|
507
|
+
if (x.source === "stdout") {
|
|
508
|
+
out += x.text;
|
|
509
|
+
} else {
|
|
510
|
+
err += x.text;
|
|
511
|
+
}
|
|
512
|
+
},
|
|
513
|
+
error: (e: unknown) => {
|
|
514
|
+
if (e instanceof SpawnError) {
|
|
515
|
+
const error = new SpawnError(`${out}\n${e.message}`, e.exitCode, e.command, e.args, out, err);
|
|
516
|
+
rej(error);
|
|
517
|
+
} else {
|
|
518
|
+
const error = new Error(`${out}\n${e instanceof Error ? e.message : String(e)}`);
|
|
519
|
+
rej(error);
|
|
491
520
|
}
|
|
492
|
-
rej(error);
|
|
493
521
|
},
|
|
494
522
|
complete: () => res([out, err]),
|
|
495
523
|
});
|
|
496
524
|
});
|
|
497
525
|
}
|
|
498
526
|
|
|
499
|
-
/**
|
|
500
|
-
* Spawns a process but detached from the current process. The process is put
|
|
501
|
-
* into its own Process Group.
|
|
502
|
-
*
|
|
503
|
-
* @param {string} exe The executable to run
|
|
504
|
-
* @param {string[]} params The parameters to pass to the child
|
|
505
|
-
* @param {SpawnOptions & SpawnRxExtras} opts Options to pass to spawn.
|
|
506
|
-
*
|
|
507
|
-
* @return {Promise<[string, string]>} Returns an Promise that represents a detached
|
|
508
|
-
* process. The value returned is the process
|
|
509
|
-
* output. If the process terminates with a
|
|
510
|
-
* non-zero value, the Promise will resolve with
|
|
511
|
-
* an Error.
|
|
512
|
-
*/
|
|
513
|
-
export function spawnDetachedPromise(
|
|
514
|
-
exe: string,
|
|
515
|
-
params: string[],
|
|
516
|
-
opts: SpawnOptions & SpawnRxExtras & { split: true },
|
|
517
|
-
): Promise<[string, string]>;
|
|
518
|
-
|
|
519
|
-
/**
|
|
520
|
-
* Spawns a process but detached from the current process. The process is put
|
|
521
|
-
* into its own Process Group.
|
|
522
|
-
*
|
|
523
|
-
* @param {string} exe The executable to run
|
|
524
|
-
* @param {string[]} params The parameters to pass to the child
|
|
525
|
-
* @param {SpawnOptions & SpawnRxExtras} opts Options to pass to spawn.
|
|
526
|
-
*
|
|
527
|
-
* @return {Promise<string>} Returns an Promise that represents a detached
|
|
528
|
-
* process. The value returned is the process
|
|
529
|
-
* output. If the process terminates with a
|
|
530
|
-
* non-zero value, the Promise will resolve with
|
|
531
|
-
* an Error.
|
|
532
|
-
*/
|
|
533
|
-
export function spawnDetachedPromise(
|
|
534
|
-
exe: string,
|
|
535
|
-
params: string[],
|
|
536
|
-
opts?: SpawnOptions & SpawnRxExtras & { split: false | undefined },
|
|
537
|
-
): Promise<string>;
|
|
538
|
-
|
|
539
|
-
/**
|
|
540
|
-
* Spawns a process but detached from the current process. The process is put
|
|
541
|
-
* into its own Process Group.
|
|
542
|
-
*
|
|
543
|
-
* @param {string} exe The executable to run
|
|
544
|
-
* @param {string[]} params The parameters to pass to the child
|
|
545
|
-
* @param {Object} opts Options to pass to spawn.
|
|
546
|
-
*
|
|
547
|
-
* @return {Promise<string>} Returns an Promise that represents a detached
|
|
548
|
-
* process. The value returned is the process
|
|
549
|
-
* output. If the process terminates with a
|
|
550
|
-
* non-zero value, the Promise will resolve with
|
|
551
|
-
* an Error.
|
|
552
|
-
*/
|
|
553
|
-
export function spawnDetachedPromise(
|
|
554
|
-
exe: string,
|
|
555
|
-
params: string[],
|
|
556
|
-
opts?: SpawnOptions & SpawnRxExtras,
|
|
557
|
-
): Promise<string> | Promise<[string, string]> {
|
|
558
|
-
if (opts?.split) {
|
|
559
|
-
return wrapObservableInSplitPromise(
|
|
560
|
-
spawnDetached(exe, params, { ...(opts ?? {}), split: true }),
|
|
561
|
-
);
|
|
562
|
-
} else {
|
|
563
|
-
return wrapObservableInPromise(
|
|
564
|
-
spawnDetached(exe, params, { ...(opts ?? {}), split: false }),
|
|
565
|
-
);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
|
|
569
527
|
/**
|
|
570
528
|
* Spawns a process as a child process.
|
|
571
529
|
*
|
|
@@ -598,11 +556,7 @@ export function spawnPromise(
|
|
|
598
556
|
* non-zero value, the Promise will resolve with
|
|
599
557
|
* an Error.
|
|
600
558
|
*/
|
|
601
|
-
export function spawnPromise(
|
|
602
|
-
exe: string,
|
|
603
|
-
params: string[],
|
|
604
|
-
opts?: SpawnOptions & SpawnRxExtras,
|
|
605
|
-
): Promise<string>;
|
|
559
|
+
export function spawnPromise(exe: string, params: string[], opts?: SpawnOptions & SpawnRxExtras): Promise<string>;
|
|
606
560
|
|
|
607
561
|
/**
|
|
608
562
|
* Spawns a process as a child process.
|
|
@@ -623,12 +577,7 @@ export function spawnPromise(
|
|
|
623
577
|
opts?: SpawnOptions & SpawnRxExtras,
|
|
624
578
|
): Promise<string> | Promise<[string, string]> {
|
|
625
579
|
if (opts?.split) {
|
|
626
|
-
return wrapObservableInSplitPromise(
|
|
627
|
-
spawn(exe, params, { ...(opts ?? {}), split: true }),
|
|
628
|
-
);
|
|
629
|
-
} else {
|
|
630
|
-
return wrapObservableInPromise(
|
|
631
|
-
spawn(exe, params, { ...(opts ?? {}), split: false }),
|
|
632
|
-
);
|
|
580
|
+
return wrapObservableInSplitPromise(spawn(exe, params, { ...(opts ?? {}), split: true }));
|
|
633
581
|
}
|
|
582
|
+
return wrapObservableInPromise(spawn(exe, params, { ...(opts ?? {}), split: false }));
|
|
634
583
|
}
|