spawn-rx 5.1.1 → 6.0.0
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 +11 -23
- package/src/index.ts +289 -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 -384
- 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,33 +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
|
|
54
|
-
|
|
118
|
+
d(`Found executable in current directory: ${target}`);
|
|
119
|
+
|
|
120
|
+
// XXX: Some very Odd programs decide to use args[0] as a parameter
|
|
121
|
+
// to determine what to do, and also symlink themselves, so we can't
|
|
122
|
+
// use realpathSync here like we used to
|
|
123
|
+
pathCache.set(exe, target);
|
|
124
|
+
return target;
|
|
55
125
|
}
|
|
56
126
|
|
|
57
|
-
const haystack = process.env.PATH
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
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
|
+
}
|
|
62
136
|
}
|
|
63
137
|
}
|
|
64
138
|
|
|
65
139
|
d("Failed to find executable anywhere in path");
|
|
140
|
+
pathCache.set(exe, exe);
|
|
66
141
|
return exe;
|
|
67
142
|
}
|
|
68
143
|
|
|
144
|
+
export type CmdWithArgs = {
|
|
145
|
+
cmd: string;
|
|
146
|
+
args: string[];
|
|
147
|
+
};
|
|
148
|
+
|
|
69
149
|
/**
|
|
70
150
|
* Finds the actual executable and parameters to run on Windows. This method
|
|
71
151
|
* mimics the POSIX behavior of being able to run scripts as executables by
|
|
@@ -82,13 +162,7 @@ function runDownPath(exe: string): string {
|
|
|
82
162
|
* @property {string} cmd The command to pass to spawn
|
|
83
163
|
* @property {string[]} args The arguments to pass to spawn
|
|
84
164
|
*/
|
|
85
|
-
export function findActualExecutable(
|
|
86
|
-
exe: string,
|
|
87
|
-
args: string[],
|
|
88
|
-
): {
|
|
89
|
-
cmd: string;
|
|
90
|
-
args: string[];
|
|
91
|
-
} {
|
|
165
|
+
export function findActualExecutable(exe: string, args: string[]): CmdWithArgs {
|
|
92
166
|
// POSIX can just execute scripts directly, no need for silly goosery
|
|
93
167
|
if (process.platform !== "win32") {
|
|
94
168
|
return { cmd: runDownPath(exe), args: args };
|
|
@@ -109,21 +183,8 @@ export function findActualExecutable(
|
|
|
109
183
|
}
|
|
110
184
|
|
|
111
185
|
if (exe.match(/\.ps1$/i)) {
|
|
112
|
-
const cmd = path.join(
|
|
113
|
-
|
|
114
|
-
"System32",
|
|
115
|
-
"WindowsPowerShell",
|
|
116
|
-
"v1.0",
|
|
117
|
-
"PowerShell.exe",
|
|
118
|
-
);
|
|
119
|
-
const psargs = [
|
|
120
|
-
"-ExecutionPolicy",
|
|
121
|
-
"Unrestricted",
|
|
122
|
-
"-NoLogo",
|
|
123
|
-
"-NonInteractive",
|
|
124
|
-
"-File",
|
|
125
|
-
exe,
|
|
126
|
-
];
|
|
186
|
+
const cmd = path.join(process.env.SYSTEMROOT!, "System32", "WindowsPowerShell", "v1.0", "PowerShell.exe");
|
|
187
|
+
const psargs = ["-ExecutionPolicy", "Unrestricted", "-NoLogo", "-NonInteractive", "-File", exe];
|
|
127
188
|
|
|
128
189
|
return { cmd: cmd, args: psargs.concat(args) };
|
|
129
190
|
}
|
|
@@ -150,8 +211,21 @@ export type SpawnRxExtras = {
|
|
|
150
211
|
stdin?: Observable<string>;
|
|
151
212
|
echoOutput?: boolean;
|
|
152
213
|
split?: boolean;
|
|
153
|
-
jobber?: boolean;
|
|
154
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;
|
|
155
229
|
};
|
|
156
230
|
|
|
157
231
|
export type OutputLine = {
|
|
@@ -160,102 +234,20 @@ export type OutputLine = {
|
|
|
160
234
|
};
|
|
161
235
|
|
|
162
236
|
/**
|
|
163
|
-
*
|
|
164
|
-
* into its own Process Group that can be killed by unsubscribing from the
|
|
165
|
-
* return Observable.
|
|
166
|
-
*
|
|
167
|
-
* @param {string} exe The executable to run
|
|
168
|
-
* @param {string[]} params The parameters to pass to the child
|
|
169
|
-
* @param {SpawnOptions & SpawnRxExtras} opts Options to pass to spawn.
|
|
170
|
-
*
|
|
171
|
-
* @return {Observable<OutputLine>} Returns an Observable that when subscribed
|
|
172
|
-
* to, will create a detached process. The
|
|
173
|
-
* process output will be streamed to this
|
|
174
|
-
* Observable, and if unsubscribed from, the
|
|
175
|
-
* process will be terminated early. If the
|
|
176
|
-
* process terminates with a non-zero value,
|
|
177
|
-
* the Observable will terminate with onError.
|
|
237
|
+
* Utility type to extract the return type based on split option
|
|
178
238
|
*/
|
|
179
|
-
export
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
opts: SpawnOptions & SpawnRxExtras & { split: true },
|
|
183
|
-
): Observable<OutputLine>;
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Spawns a process but detached from the current process. The process is put
|
|
187
|
-
* into its own Process Group that can be killed by unsubscribing from the
|
|
188
|
-
* return Observable.
|
|
189
|
-
*
|
|
190
|
-
* @param {string} exe The executable to run
|
|
191
|
-
* @param {string[]} params The parameters to pass to the child
|
|
192
|
-
* @param {SpawnOptions & SpawnRxExtras} opts Options to pass to spawn.
|
|
193
|
-
*
|
|
194
|
-
* @return {Observable<string>} Returns an Observable that when subscribed
|
|
195
|
-
* to, will create a detached process. The
|
|
196
|
-
* process output will be streamed to this
|
|
197
|
-
* Observable, and if unsubscribed from, the
|
|
198
|
-
* process will be terminated early. If the
|
|
199
|
-
* process terminates with a non-zero value,
|
|
200
|
-
* the Observable will terminate with onError.
|
|
201
|
-
*/
|
|
202
|
-
export function spawnDetached(
|
|
203
|
-
exe: string,
|
|
204
|
-
params: string[],
|
|
205
|
-
opts?: SpawnOptions & SpawnRxExtras & { split: false | undefined },
|
|
206
|
-
): Observable<string>;
|
|
239
|
+
export type SpawnResult<T extends SpawnRxExtras> = T extends { split: true }
|
|
240
|
+
? Observable<OutputLine>
|
|
241
|
+
: Observable<string>;
|
|
207
242
|
|
|
208
243
|
/**
|
|
209
|
-
*
|
|
210
|
-
* into its own Process Group that can be killed by unsubscribing from the
|
|
211
|
-
* return Observable.
|
|
212
|
-
*
|
|
213
|
-
* @param {string} exe The executable to run
|
|
214
|
-
* @param {string[]} params The parameters to pass to the child
|
|
215
|
-
* @param {SpawnOptions & SpawnRxExtras} opts Options to pass to spawn.
|
|
216
|
-
*
|
|
217
|
-
* @return {Observable<string>} Returns an Observable that when subscribed
|
|
218
|
-
* to, will create a detached process. The
|
|
219
|
-
* process output will be streamed to this
|
|
220
|
-
* Observable, and if unsubscribed from, the
|
|
221
|
-
* process will be terminated early. If the
|
|
222
|
-
* process terminates with a non-zero value,
|
|
223
|
-
* the Observable will terminate with onError.
|
|
244
|
+
* Utility type to extract the promise return type based on split option
|
|
224
245
|
*/
|
|
225
|
-
export
|
|
226
|
-
|
|
227
|
-
params: string[],
|
|
228
|
-
opts?: SpawnOptions & SpawnRxExtras,
|
|
229
|
-
): Observable<string> | Observable<OutputLine> {
|
|
230
|
-
const { cmd, args } = findActualExecutable(exe, params ?? []);
|
|
231
|
-
|
|
232
|
-
if (!isWindows) {
|
|
233
|
-
return spawn(
|
|
234
|
-
cmd,
|
|
235
|
-
args,
|
|
236
|
-
Object.assign({}, opts || {}, { detached: true }) as any,
|
|
237
|
-
);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const newParams = [cmd].concat(args);
|
|
241
|
-
|
|
242
|
-
const target = path.join(
|
|
243
|
-
__dirname,
|
|
244
|
-
"..",
|
|
245
|
-
"..",
|
|
246
|
-
"vendor",
|
|
247
|
-
"jobber",
|
|
248
|
-
"Jobber.exe",
|
|
249
|
-
);
|
|
250
|
-
const options = {
|
|
251
|
-
...(opts ?? {}),
|
|
252
|
-
detached: true,
|
|
253
|
-
jobber: true,
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
d(`spawnDetached: ${target}, ${newParams}`);
|
|
257
|
-
return spawn(target, newParams, options as any);
|
|
246
|
+
export type SpawnPromiseResult<T extends SpawnRxExtras> = T extends {
|
|
247
|
+
split: true;
|
|
258
248
|
}
|
|
249
|
+
? Promise<[string, string]>
|
|
250
|
+
: Promise<string>;
|
|
259
251
|
|
|
260
252
|
/**
|
|
261
253
|
* Spawns a process attached as a child of the current process.
|
|
@@ -320,133 +312,167 @@ export function spawn(
|
|
|
320
312
|
opts?: SpawnOptions & SpawnRxExtras,
|
|
321
313
|
): Observable<string> | Observable<OutputLine> {
|
|
322
314
|
opts = opts ?? {};
|
|
323
|
-
const spawnObs: Observable<OutputLine> = new Observable(
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
} catch {
|
|
352
|
-
chunk = `<< Lost chunk of process output for ${exe} - length was ${b.length}>>`;
|
|
353
|
-
}
|
|
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
|
+
}
|
|
354
343
|
|
|
355
|
-
|
|
356
|
-
|
|
344
|
+
const bufHandler = (source: "stdout" | "stderr") => (b: string | Buffer) => {
|
|
345
|
+
if (b.length < 1) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
357
348
|
|
|
358
|
-
|
|
349
|
+
if (opts.echoOutput) {
|
|
350
|
+
(source === "stdout" ? process.stdout : process.stderr).write(b);
|
|
351
|
+
}
|
|
359
352
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
next: (x: any) => proc.stdin.write(x),
|
|
365
|
-
error: subj.error.bind(subj),
|
|
366
|
-
complete: () => proc.stdin.end(),
|
|
367
|
-
}),
|
|
368
|
-
);
|
|
353
|
+
let chunk = "<< String sent back was too long >>";
|
|
354
|
+
try {
|
|
355
|
+
if (typeof b === "string") {
|
|
356
|
+
chunk = b.toString();
|
|
369
357
|
} else {
|
|
370
|
-
|
|
371
|
-
new Error(
|
|
372
|
-
`opts.stdio conflicts with provided spawn opts.stdin observable, 'pipe' is required`,
|
|
373
|
-
),
|
|
374
|
-
);
|
|
358
|
+
chunk = b.toString(encoding || "utf8");
|
|
375
359
|
}
|
|
360
|
+
} catch {
|
|
361
|
+
chunk = `<< Lost chunk of process output for ${exe} - length was ${b.length}>>`;
|
|
376
362
|
}
|
|
377
363
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
let noClose = false;
|
|
364
|
+
subj.next({ source: source, text: chunk });
|
|
365
|
+
};
|
|
381
366
|
|
|
382
|
-
|
|
383
|
-
stdoutCompleted = new AsyncSubject<boolean>();
|
|
384
|
-
proc.stdout.on("data", bufHandler("stdout"));
|
|
385
|
-
proc.stdout.on("close", () => {
|
|
386
|
-
(stdoutCompleted! as Subject<boolean>).next(true);
|
|
387
|
-
(stdoutCompleted! as Subject<boolean>).complete();
|
|
388
|
-
});
|
|
389
|
-
} else {
|
|
390
|
-
stdoutCompleted = of(true);
|
|
391
|
-
}
|
|
367
|
+
const ret = new Subscription();
|
|
392
368
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
proc.
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
+
);
|
|
400
379
|
} else {
|
|
401
|
-
|
|
380
|
+
subj.error(new Error(`opts.stdio conflicts with provided spawn opts.stdin observable, 'pipe' is required`));
|
|
402
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;
|
|
403
387
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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();
|
|
407
394
|
});
|
|
395
|
+
} else {
|
|
396
|
+
stdoutCompleted = of(true);
|
|
397
|
+
}
|
|
408
398
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
);
|
|
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
|
+
}
|
|
414
409
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
+
});
|
|
422
434
|
|
|
423
|
-
|
|
424
|
-
|
|
435
|
+
ret.add(
|
|
436
|
+
new Subscription(() => {
|
|
437
|
+
if (noClose) {
|
|
438
|
+
return;
|
|
425
439
|
}
|
|
426
|
-
});
|
|
427
440
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
441
|
+
if (timeoutHandle) {
|
|
442
|
+
clearTimeout(timeoutHandle);
|
|
443
|
+
}
|
|
433
444
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
setTimeout(() => proc.kill(), 5 * 1000);
|
|
439
|
-
} else {
|
|
440
|
-
proc.kill();
|
|
441
|
-
}
|
|
442
|
-
}),
|
|
443
|
-
);
|
|
445
|
+
d(`Killing process: ${cmd} ${args.join()}`);
|
|
446
|
+
proc.kill();
|
|
447
|
+
}),
|
|
448
|
+
);
|
|
444
449
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
+
}
|
|
448
474
|
|
|
449
|
-
return opts.split ?
|
|
475
|
+
return opts.split ? resultObs : resultObs.pipe(map((x: OutputLine) => x?.text));
|
|
450
476
|
}
|
|
451
477
|
|
|
452
478
|
function wrapObservableInPromise(obs: Observable<string>) {
|
|
@@ -454,14 +480,17 @@ function wrapObservableInPromise(obs: Observable<string>) {
|
|
|
454
480
|
let out = "";
|
|
455
481
|
|
|
456
482
|
obs.subscribe({
|
|
457
|
-
next: (x) =>
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
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);
|
|
463
493
|
}
|
|
464
|
-
rej(err);
|
|
465
494
|
},
|
|
466
495
|
complete: () => res(out),
|
|
467
496
|
});
|
|
@@ -474,93 +503,27 @@ function wrapObservableInSplitPromise(obs: Observable<OutputLine>) {
|
|
|
474
503
|
let err = "";
|
|
475
504
|
|
|
476
505
|
obs.subscribe({
|
|
477
|
-
next: (x) =>
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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);
|
|
486
520
|
}
|
|
487
|
-
rej(error);
|
|
488
521
|
},
|
|
489
522
|
complete: () => res([out, err]),
|
|
490
523
|
});
|
|
491
524
|
});
|
|
492
525
|
}
|
|
493
526
|
|
|
494
|
-
/**
|
|
495
|
-
* Spawns a process but detached from the current process. The process is put
|
|
496
|
-
* into its own Process Group.
|
|
497
|
-
*
|
|
498
|
-
* @param {string} exe The executable to run
|
|
499
|
-
* @param {string[]} params The parameters to pass to the child
|
|
500
|
-
* @param {SpawnOptions & SpawnRxExtras} opts Options to pass to spawn.
|
|
501
|
-
*
|
|
502
|
-
* @return {Promise<[string, string]>} Returns an Promise that represents a detached
|
|
503
|
-
* process. The value returned is the process
|
|
504
|
-
* output. If the process terminates with a
|
|
505
|
-
* non-zero value, the Promise will resolve with
|
|
506
|
-
* an Error.
|
|
507
|
-
*/
|
|
508
|
-
export function spawnDetachedPromise(
|
|
509
|
-
exe: string,
|
|
510
|
-
params: string[],
|
|
511
|
-
opts: SpawnOptions & SpawnRxExtras & { split: true },
|
|
512
|
-
): Promise<[string, string]>;
|
|
513
|
-
|
|
514
|
-
/**
|
|
515
|
-
* Spawns a process but detached from the current process. The process is put
|
|
516
|
-
* into its own Process Group.
|
|
517
|
-
*
|
|
518
|
-
* @param {string} exe The executable to run
|
|
519
|
-
* @param {string[]} params The parameters to pass to the child
|
|
520
|
-
* @param {SpawnOptions & SpawnRxExtras} opts Options to pass to spawn.
|
|
521
|
-
*
|
|
522
|
-
* @return {Promise<string>} Returns an Promise that represents a detached
|
|
523
|
-
* process. The value returned is the process
|
|
524
|
-
* output. If the process terminates with a
|
|
525
|
-
* non-zero value, the Promise will resolve with
|
|
526
|
-
* an Error.
|
|
527
|
-
*/
|
|
528
|
-
export function spawnDetachedPromise(
|
|
529
|
-
exe: string,
|
|
530
|
-
params: string[],
|
|
531
|
-
opts?: SpawnOptions & SpawnRxExtras & { split: false | undefined },
|
|
532
|
-
): Promise<string>;
|
|
533
|
-
|
|
534
|
-
/**
|
|
535
|
-
* Spawns a process but detached from the current process. The process is put
|
|
536
|
-
* into its own Process Group.
|
|
537
|
-
*
|
|
538
|
-
* @param {string} exe The executable to run
|
|
539
|
-
* @param {string[]} params The parameters to pass to the child
|
|
540
|
-
* @param {Object} opts Options to pass to spawn.
|
|
541
|
-
*
|
|
542
|
-
* @return {Promise<string>} Returns an Promise that represents a detached
|
|
543
|
-
* process. The value returned is the process
|
|
544
|
-
* output. If the process terminates with a
|
|
545
|
-
* non-zero value, the Promise will resolve with
|
|
546
|
-
* an Error.
|
|
547
|
-
*/
|
|
548
|
-
export function spawnDetachedPromise(
|
|
549
|
-
exe: string,
|
|
550
|
-
params: string[],
|
|
551
|
-
opts?: SpawnOptions & SpawnRxExtras,
|
|
552
|
-
): Promise<string> | Promise<[string, string]> {
|
|
553
|
-
if (opts?.split) {
|
|
554
|
-
return wrapObservableInSplitPromise(
|
|
555
|
-
spawnDetached(exe, params, { ...(opts ?? {}), split: true }),
|
|
556
|
-
);
|
|
557
|
-
} else {
|
|
558
|
-
return wrapObservableInPromise(
|
|
559
|
-
spawnDetached(exe, params, { ...(opts ?? {}), split: false }),
|
|
560
|
-
);
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
527
|
/**
|
|
565
528
|
* Spawns a process as a child process.
|
|
566
529
|
*
|
|
@@ -593,11 +556,7 @@ export function spawnPromise(
|
|
|
593
556
|
* non-zero value, the Promise will resolve with
|
|
594
557
|
* an Error.
|
|
595
558
|
*/
|
|
596
|
-
export function spawnPromise(
|
|
597
|
-
exe: string,
|
|
598
|
-
params: string[],
|
|
599
|
-
opts?: SpawnOptions & SpawnRxExtras,
|
|
600
|
-
): Promise<string>;
|
|
559
|
+
export function spawnPromise(exe: string, params: string[], opts?: SpawnOptions & SpawnRxExtras): Promise<string>;
|
|
601
560
|
|
|
602
561
|
/**
|
|
603
562
|
* Spawns a process as a child process.
|
|
@@ -618,12 +577,7 @@ export function spawnPromise(
|
|
|
618
577
|
opts?: SpawnOptions & SpawnRxExtras,
|
|
619
578
|
): Promise<string> | Promise<[string, string]> {
|
|
620
579
|
if (opts?.split) {
|
|
621
|
-
return wrapObservableInSplitPromise(
|
|
622
|
-
spawn(exe, params, { ...(opts ?? {}), split: true }),
|
|
623
|
-
);
|
|
624
|
-
} else {
|
|
625
|
-
return wrapObservableInPromise(
|
|
626
|
-
spawn(exe, params, { ...(opts ?? {}), split: false }),
|
|
627
|
-
);
|
|
580
|
+
return wrapObservableInSplitPromise(spawn(exe, params, { ...(opts ?? {}), split: true }));
|
|
628
581
|
}
|
|
582
|
+
return wrapObservableInPromise(spawn(exe, params, { ...(opts ?? {}), split: false }));
|
|
629
583
|
}
|