@@ -2,10 +2,14 @@ import type { ChildProcess } from 'node:child_process'
22import type { Writable } from 'node:stream'
33import type { PoolOptions , PoolWorker , WorkerRequest } from '../types'
44import { fork } from 'node:child_process'
5+ import { EventEmitter } from 'node:events'
56import { resolve } from 'node:path'
67import { streamFlushed } from './utils'
78
89const SIGKILL_TIMEOUT = 500 /** jest does 500ms by default, let's follow it */
10+ // how long a failed pipe write may wait for the process's 'exit' event
11+ // before it is reported as the worker error itself
12+ const PIPE_ERROR_EXIT_GRACE = 1_000
913
1014/** @experimental */
1115export class ForksPoolWorker implements PoolWorker {
@@ -20,6 +24,9 @@ export class ForksPoolWorker implements PoolWorker {
2024 private stdout : NodeJS . WriteStream | Writable
2125 private stderr : NodeJS . WriteStream | Writable
2226
27+ private _errorEmitter = new EventEmitter < { error : [ Error ] } > ( )
28+ private _pipeErrorTimer : ReturnType < typeof setTimeout > | undefined
29+
2330 constructor ( options : PoolOptions ) {
2431 this . execArgv = options . execArgv
2532 this . env = options . env
@@ -31,11 +38,21 @@ export class ForksPoolWorker implements PoolWorker {
3138 }
3239
3340 on ( event : string , callback : ( ...args : any [ ] ) => void ) : void {
34- this . fork . on ( event , callback )
41+ if ( event === 'error' ) {
42+ this . _errorEmitter . on ( 'error' , callback )
43+ }
44+ else {
45+ this . fork . on ( event , callback )
46+ }
3547 }
3648
3749 off ( event : string , callback : ( ...args : any [ ] ) => void ) : void {
38- this . fork . off ( event , callback )
50+ if ( event === 'error' ) {
51+ this . _errorEmitter . off ( 'error' , callback )
52+ }
53+ else {
54+ this . fork . off ( event , callback )
55+ }
3956 }
4057
4158 send ( message : WorkerRequest ) : void {
@@ -50,6 +67,8 @@ export class ForksPoolWorker implements PoolWorker {
5067 serialization : 'advanced' ,
5168 } )
5269
70+ this . _fork . on ( 'error' , this . emitError )
71+
5372 // `end: false`: the logger streams are shared by every worker, so one
5473 // ending worker stream must not end them for everyone else
5574 if ( this . _fork . stdout ) {
@@ -108,6 +127,40 @@ export class ForksPoolWorker implements PoolWorker {
108127 return data
109128 }
110129
130+ private emitError = ( error : Error ) : void => {
131+ // A write into a dying child process fails with EPIPE (or a closed IPC
132+ // channel) and can be observed before the process's 'exit' event,
133+ // especially on macOS. The exit event knows the exit code, the signal and
134+ // the affected test files, so hold the write error and let the 'exit'
135+ // listeners report instead. The timer covers a broken channel whose
136+ // process never exits; a process that exited while the error was held was
137+ // already reported through the exit event, and a process whose listeners
138+ // were detached is being shut down deliberately — drop the error in both
139+ // cases.
140+ const code = ( error as NodeJS . ErrnoException ) . code
141+ if ( code === 'EPIPE' || code === 'ERR_IPC_CHANNEL_CLOSED' ) {
142+ if ( this . _pipeErrorTimer ) {
143+ return
144+ }
145+ this . _pipeErrorTimer = setTimeout ( ( ) => {
146+ this . _pipeErrorTimer = undefined
147+ const fork = this . _fork
148+ if (
149+ fork
150+ && fork . exitCode == null
151+ && fork . signalCode == null
152+ && this . _errorEmitter . listenerCount ( 'error' )
153+ ) {
154+ this . _errorEmitter . emit ( 'error' , error )
155+ }
156+ } , PIPE_ERROR_EXIT_GRACE )
157+ this . _pipeErrorTimer . unref ( )
158+ return
159+ }
160+
161+ this . _errorEmitter . emit ( 'error' , error )
162+ }
163+
111164 private get fork ( ) {
112165 if ( ! this . _fork ) {
113166 throw new Error ( `The child process was torn down or never initialized. This is a bug in Vitest.` )
0 commit comments