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/src/index.ts CHANGED
@@ -1,18 +1,58 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import * as path from "path";
3
- import * as net from "net";
4
- import * as sfs from "fs";
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 currect directory: ${target}`);
54
- return sfs.realpathSync(target);
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!.split(isWindows ? ";" : ":");
58
- for (const p of haystack) {
59
- const needle = path.join(p, exe);
60
- if (statSyncNoException(needle)) {
61
- return sfs.realpathSync(needle);
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
- process.env.SYSTEMROOT!,
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
- * Spawns a process but detached from the current process. The process is put
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 function spawnDetached(
180
- exe: string,
181
- params: string[],
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
- * Spawns a process but detached from the current process. The process is put
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 function spawnDetached(
226
- exe: string,
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
- (subj: Observer<OutputLine>) => {
325
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
326
- const { stdin, jobber, split, encoding, ...spawnOpts } = opts;
327
- const { cmd, args } = findActualExecutable(exe, params);
328
- d(
329
- `spawning process: ${cmd} ${args.join()}, ${JSON.stringify(spawnOpts)}`,
330
- );
331
-
332
- const proc = spawnOg(cmd, args, spawnOpts);
333
-
334
- const bufHandler =
335
- (source: "stdout" | "stderr") => (b: string | Buffer) => {
336
- if (b.length < 1) {
337
- return;
338
- }
339
-
340
- if (opts.echoOutput) {
341
- (source === "stdout" ? process.stdout : process.stderr).write(b);
342
- }
343
-
344
- let chunk = "<< String sent back was too long >>";
345
- try {
346
- if (typeof b === "string") {
347
- chunk = b.toString();
348
- } else {
349
- chunk = b.toString(encoding || "utf8");
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
- subj.next({ source: source, text: chunk });
356
- };
344
+ const bufHandler = (source: "stdout" | "stderr") => (b: string | Buffer) => {
345
+ if (b.length < 1) {
346
+ return;
347
+ }
357
348
 
358
- const ret = new Subscription();
349
+ if (opts.echoOutput) {
350
+ (source === "stdout" ? process.stdout : process.stderr).write(b);
351
+ }
359
352
 
360
- if (opts.stdin) {
361
- if (proc.stdin) {
362
- ret.add(
363
- opts.stdin.subscribe({
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
- subj.error(
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
- let stderrCompleted: Subject<boolean> | Observable<boolean> | null = null;
379
- let stdoutCompleted: Subject<boolean> | Observable<boolean> | null = null;
380
- let noClose = false;
364
+ subj.next({ source: source, text: chunk });
365
+ };
381
366
 
382
- if (proc.stdout) {
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
- if (proc.stderr) {
394
- stderrCompleted = new AsyncSubject<boolean>();
395
- proc.stderr.on("data", bufHandler("stderr"));
396
- proc.stderr.on("close", () => {
397
- (stderrCompleted! as Subject<boolean>).next(true);
398
- (stderrCompleted! as Subject<boolean>).complete();
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
- stderrCompleted = of(true);
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
- proc.on("error", (e: Error) => {
405
- noClose = true;
406
- subj.error(e);
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
- proc.on("close", (code: number) => {
410
- noClose = true;
411
- const pipesClosed = merge(stdoutCompleted!, stderrCompleted!).pipe(
412
- reduce((acc) => acc, true),
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
- if (code === 0) {
416
- pipesClosed.subscribe(() => subj.complete());
417
- } else {
418
- pipesClosed.subscribe(() => {
419
- const e: any = new Error(`Failed with exit code: ${code}`);
420
- e.exitCode = code;
421
- e.code = code;
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
- subj.error(e);
424
- });
435
+ ret.add(
436
+ new Subscription(() => {
437
+ if (noClose) {
438
+ return;
425
439
  }
426
- });
427
440
 
428
- ret.add(
429
- new Subscription(() => {
430
- if (noClose) {
431
- return;
432
- }
441
+ if (timeoutHandle) {
442
+ clearTimeout(timeoutHandle);
443
+ }
433
444
 
434
- d(`Killing process: ${cmd} ${args.join()}`);
435
- if (opts.jobber) {
436
- // NB: Connecting to Jobber's named pipe will kill it
437
- net.connect(`\\\\.\\pipe\\jobber-${proc.pid}`);
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
- return ret;
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 ? spawnObs : spawnObs.pipe(map((x: any) => x?.text));
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) => (out += x),
458
- error: (e) => {
459
- const err: any = new Error(`${out}\n${e.message}`);
460
- if ("exitCode" in e) {
461
- err.exitCode = e.exitCode;
462
- err.code = e.exitCode;
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) => (x.source === "stdout" ? (out += x.text) : (err += x.text)),
478
- error: (e) => {
479
- const error: any = new Error(`${out}\n${e.message}`);
480
-
481
- if ("exitCode" in e) {
482
- error.exitCode = e.exitCode;
483
- error.code = e.exitCode;
484
- error.stdout = out;
485
- error.stderr = err;
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
  }