@@ -2,6 +2,8 @@ import { constants } from 'node:fs';
22import { access , stat } from 'node:fs/promises' ;
33import path from 'node:path' ;
44import { spawn , spawnSync , type ChildProcess , type StdioOptions } from 'node:child_process' ;
5+ import { Readable } from 'node:stream' ;
6+ import { pipeline } from 'node:stream/promises' ;
57import { AppError } from './errors.ts' ;
68
79export type ExecResult = {
@@ -19,6 +21,7 @@ type ExecOptions = {
1921 stdin ?: string | Buffer ;
2022 timeoutMs ?: number ;
2123 detached ?: boolean ;
24+ signal ?: AbortSignal ;
2225} ;
2326
2427type ExecStreamOptions = ExecOptions & {
@@ -75,27 +78,32 @@ function runSpawnedCommand(
7578 const stdoutChunks : Buffer [ ] | undefined = options . binaryStdout ? [ ] : undefined ;
7679 let stderr = '' ;
7780 let didTimeout = false ;
81+ let didAbort = false ;
7882 const timeoutMs = normalizeTimeoutMs ( options . timeoutMs ) ;
7983 const timeoutHandle = timeoutMs
8084 ? setTimeout ( ( ) => {
8185 didTimeout = true ;
8286 killProcessTree ( child , options . detached ) ;
8387 } , timeoutMs )
8488 : null ;
89+ const onAbort = ( ) => {
90+ didAbort = true ;
91+ killProcessTree ( child , options . detached ) ;
92+ } ;
93+ if ( options . signal ?. aborted ) {
94+ onAbort ( ) ;
95+ } else {
96+ options . signal ?. addEventListener ( 'abort' , onAbort , { once : true } ) ;
97+ }
8598
8699 if ( ! options . binaryStdout ) child . stdout . setEncoding ( 'utf8' ) ;
87100 child . stderr . setEncoding ( 'utf8' ) ;
88101
89- child . stdin . on ( 'error' , ( err : NodeJS . ErrnoException ) => {
90- if ( err . code !== 'EPIPE' ) {
91- child . emit ( 'error' , err ) ;
92- }
102+ void writeChildStdin ( child , options . stdin ) . catch ( ( err : unknown ) => {
103+ if ( isEpipeError ( err ) ) return ;
104+ reject ( createStdinError ( executable , cmd , args , err ) ) ;
105+ killProcessTree ( child , options . detached ) ;
93106 } ) ;
94- if ( options . stdin !== undefined ) {
95- child . stdin . end ( options . stdin ) ;
96- } else {
97- child . stdin . end ( ) ;
98- }
99107
100108 child . stdout . on ( 'data' , ( chunk ) => {
101109 if ( options . binaryStdout ) {
@@ -115,12 +123,22 @@ function runSpawnedCommand(
115123
116124 child . on ( 'error' , ( err ) => {
117125 if ( timeoutHandle ) clearTimeout ( timeoutHandle ) ;
118- reject ( createSpawnError ( executable , cmd , args , err ) ) ;
126+ options . signal ?. removeEventListener ( 'abort' , onAbort ) ;
127+ reject (
128+ didAbort
129+ ? createCommandCanceledError ( executable , cmd , args )
130+ : createSpawnError ( executable , cmd , args , err ) ,
131+ ) ;
119132 } ) ;
120133
121134 child . on ( 'close' , ( code ) => {
122135 if ( timeoutHandle ) clearTimeout ( timeoutHandle ) ;
136+ options . signal ?. removeEventListener ( 'abort' , onAbort ) ;
123137 const exitCode = code ?? 1 ;
138+ if ( didAbort ) {
139+ reject ( createCommandCanceledError ( executable , cmd , args ) ) ;
140+ return ;
141+ }
124142 if ( didTimeout && timeoutMs ) {
125143 reject ( createTimeoutError ( executable , cmd , args , timeoutMs , exitCode , stdout , stderr ) ) ;
126144 return ;
@@ -341,6 +359,29 @@ function createCommandFailedError(
341359 return new AppError ( 'COMMAND_FAILED' , `Failed to run ${ executable } ` , { cmd, args } , cause ) ;
342360}
343361
362+ function createStdinError (
363+ executable : string ,
364+ cmd : string ,
365+ args : string [ ] ,
366+ cause : unknown ,
367+ ) : AppError {
368+ return new AppError (
369+ 'COMMAND_FAILED' ,
370+ `Failed to write stdin for ${ executable } ` ,
371+ { cmd, args } ,
372+ cause instanceof Error ? cause : undefined ,
373+ ) ;
374+ }
375+
376+ function createCommandCanceledError ( executable : string , cmd : string , args : string [ ] ) : AppError {
377+ return new AppError ( 'COMMAND_FAILED' , 'request canceled' , {
378+ cmd,
379+ args,
380+ executable,
381+ reason : 'request_canceled' ,
382+ } ) ;
383+ }
384+
344385function createTimeoutError (
345386 executable : string ,
346387 cmd : string ,
@@ -460,3 +501,21 @@ function killProcessTree(child: ChildProcess, detached: boolean | undefined): vo
460501 }
461502 child . kill ( 'SIGKILL' ) ;
462503}
504+
505+ async function writeChildStdin (
506+ child : ChildProcess ,
507+ stdin : string | Buffer | undefined ,
508+ ) : Promise < void > {
509+ if ( ! child . stdin ) return ;
510+ if ( stdin === undefined ) {
511+ child . stdin ?. end ( ) ;
512+ return ;
513+ }
514+ await pipeline ( Readable . from ( [ stdin ] ) , child . stdin ) ;
515+ }
516+
517+ function isEpipeError ( error : unknown ) : boolean {
518+ return (
519+ error instanceof Error && 'code' in error && ( error as NodeJS . ErrnoException ) . code === 'EPIPE'
520+ ) ;
521+ }
0 commit comments