@@ -76,25 +76,118 @@ async function defaultExecutor(
7676
7777 const childProcess = spawn ( executable , args , spawnOpts ) ;
7878
79- if ( verbose ) {
80- childProcess . stdout ?. pipe ( process . stderr , { end : false } ) ;
81- childProcess . stderr ?. pipe ( process . stderr , { end : false } ) ;
82- }
83-
8479 let stdout = '' ;
8580 let stderr = '' ;
8681
87- childProcess . stdout ?. on ( 'data' , ( data : Buffer ) => {
88- const chunk = data . toString ( ) ;
89- stdout += chunk ;
90- opts ?. onStdout ?.( chunk ) ;
91- } ) ;
82+ const streamClosers : Array < ( ) => void > = [ ] ;
83+ const streamDetachers : Array < ( ) => void > = [ ] ;
84+ let openStreamCount = 0 ;
85+ let settled = false ;
86+ let exitObserved = false ;
87+ let exitCode : number | null = null ;
88+ let exitSettleTimer : NodeJS . Timeout | null = null ;
89+
90+ const clearExitSettleTimer = ( ) : void => {
91+ if ( exitSettleTimer ) {
92+ clearTimeout ( exitSettleTimer ) ;
93+ exitSettleTimer = null ;
94+ }
95+ } ;
9296
93- childProcess . stderr ?. on ( 'data' , ( data : Buffer ) => {
94- const chunk = data . toString ( ) ;
95- stderr += chunk ;
96- opts ?. onStderr ?.( chunk ) ;
97- } ) ;
97+ const detachStreamListeners = ( ) : void => {
98+ for ( const detachStream of streamDetachers ) {
99+ detachStream ( ) ;
100+ }
101+ streamDetachers . length = 0 ;
102+ } ;
103+
104+ const handleError = ( err : Error ) : void => {
105+ if ( settled ) {
106+ return ;
107+ }
108+ settled = true ;
109+ clearExitSettleTimer ( ) ;
110+ detachStreamListeners ( ) ;
111+ logSpawnError ( err ) ;
112+ reject ( err ) ;
113+ } ;
114+
115+ const settle = ( code : number | null ) : void => {
116+ if ( settled ) {
117+ return ;
118+ }
119+ settled = true ;
120+ clearExitSettleTimer ( ) ;
121+ detachStreamListeners ( ) ;
122+
123+ const success = code === 0 ;
124+ const response : CommandResponse = {
125+ success,
126+ output : stdout ,
127+ error : success ? undefined : stderr ,
128+ process : childProcess ,
129+ exitCode : code ?? undefined ,
130+ } ;
131+
132+ resolve ( response ) ;
133+ } ;
134+
135+ const maybeSettleAfterExit = ( ) : void => {
136+ if ( ! exitObserved || settled || openStreamCount > 0 ) {
137+ return ;
138+ }
139+ settle ( exitCode ) ;
140+ } ;
141+
142+ const scheduleExitSettle = ( ) : void => {
143+ if ( settled || exitSettleTimer ) {
144+ return ;
145+ }
146+ exitSettleTimer = setTimeout ( ( ) => {
147+ settle ( exitCode ) ;
148+ } , 100 ) ;
149+ } ;
150+
151+ const attachStream = (
152+ stream : NodeJS . ReadableStream | null | undefined ,
153+ onChunk : ( chunk : string ) => void ,
154+ mirrorToStderr : boolean ,
155+ ) : void => {
156+ if ( ! stream ) {
157+ return ;
158+ }
159+
160+ openStreamCount += 1 ;
161+ let streamClosed = false ;
162+
163+ const markClosed = ( ) : void => {
164+ if ( streamClosed ) {
165+ return ;
166+ }
167+ streamClosed = true ;
168+ openStreamCount = Math . max ( 0 , openStreamCount - 1 ) ;
169+ maybeSettleAfterExit ( ) ;
170+ } ;
171+
172+ const handleData = ( data : Buffer | string ) : void => {
173+ if ( settled ) {
174+ return ;
175+ }
176+ const chunk = data . toString ( ) ;
177+ onChunk ( chunk ) ;
178+ if ( mirrorToStderr ) {
179+ process . stderr . write ( chunk ) ;
180+ }
181+ } ;
182+
183+ stream . on ( 'data' , handleData ) ;
184+ stream . once ( 'end' , markClosed ) ;
185+ stream . once ( 'close' , markClosed ) ;
186+ streamClosers . push ( markClosed ) ;
187+ streamDetachers . push ( ( ) => {
188+ stream . off ( 'data' , handleData ) ;
189+ } ) ;
190+ } ;
98191
99192 if ( detached ) {
100193 let resolved = false ;
@@ -126,25 +219,41 @@ async function defaultExecutor(
126219 }
127220 }
128221 } , 100 ) ;
129- } else {
130- childProcess . on ( 'close' , ( code ) => {
131- const success = code === 0 ;
132- const response : CommandResponse = {
133- success,
134- output : stdout ,
135- error : success ? undefined : stderr ,
136- process : childProcess ,
137- exitCode : code ?? undefined ,
138- } ;
139-
140- resolve ( response ) ;
141- } ) ;
142-
143- childProcess . on ( 'error' , ( err ) => {
144- logSpawnError ( err ) ;
145- reject ( err ) ;
146- } ) ;
222+ return ;
147223 }
224+
225+ attachStream (
226+ childProcess . stdout ,
227+ ( chunk ) => {
228+ stdout += chunk ;
229+ opts ?. onStdout ?.( chunk ) ;
230+ } ,
231+ verbose ,
232+ ) ;
233+
234+ attachStream (
235+ childProcess . stderr ,
236+ ( chunk ) => {
237+ stderr += chunk ;
238+ opts ?. onStderr ?.( chunk ) ;
239+ } ,
240+ verbose ,
241+ ) ;
242+
243+ childProcess . once ( 'error' , handleError ) ;
244+ childProcess . once ( 'exit' , ( code ) => {
245+ exitObserved = true ;
246+ exitCode = code ;
247+ maybeSettleAfterExit ( ) ;
248+ scheduleExitSettle ( ) ;
249+ } ) ;
250+ childProcess . once ( 'close' , ( code ) => {
251+ clearExitSettleTimer ( ) ;
252+ for ( const closeStream of streamClosers ) {
253+ closeStream ( ) ;
254+ }
255+ settle ( code ?? exitCode ) ;
256+ } ) ;
148257 } ) ;
149258}
150259
0 commit comments