@@ -269,7 +269,15 @@ public class LocalProcess {
269269
270270 // Mark as running and set up I/O for reading from master fd first
271271 running = true
272- io = DispatchIO ( type: . stream, fileDescriptor: master, queue: dispatchQueue, cleanupHandler: { _ in } )
272+ // Capture FD values for cleanup handler to close them safely after DispatchIO is done
273+ let masterToClose = master
274+ let slaveToClose = slave
275+ io = DispatchIO ( type: . stream, fileDescriptor: master, queue: dispatchQueue, cleanupHandler: { _ in
276+ // Close file descriptors after DispatchIO has finished with them
277+ // This prevents EV_VANISHED crash by ensuring proper cleanup order
278+ close ( masterToClose)
279+ close ( slaveToClose)
280+ } )
273281 guard let io else {
274282 return
275283 }
@@ -362,7 +370,13 @@ public class LocalProcess {
362370 running = true
363371 self . childfd = childfd
364372 self . shellPid = shellPid
365- io = DispatchIO ( type: . stream, fileDescriptor: childfd, queue: dispatchQueue, cleanupHandler: { x in } )
373+ // Capture FD value for cleanup handler to close it safely after DispatchIO is done
374+ let fdToClose = childfd
375+ io = DispatchIO ( type: . stream, fileDescriptor: childfd, queue: dispatchQueue, cleanupHandler: { _ in
376+ // Close file descriptor after DispatchIO has finished with it
377+ // This prevents EV_VANISHED crash by ensuring proper cleanup order
378+ close ( fdToClose)
379+ } )
366380 guard let io else {
367381 return
368382 }
@@ -379,22 +393,24 @@ public class LocalProcess {
379393 task. cancel ( )
380394 subprocessTask = nil
381395 }
382-
383- // Close file descriptors
384- if masterFd != - 1 {
385- close ( masterFd)
386- masterFd = - 1
387- }
388- if slaveFd != - 1 {
389- close ( slaveFd)
390- slaveFd = - 1
391- }
396+
397+ // Set FD markers to -1 (actual FDs are closed by DispatchIO cleanup handler)
398+ masterFd = - 1
399+ slaveFd = - 1
392400 #endif
393-
401+
402+ // Close DispatchIO - this will trigger the cleanup handler which closes file descriptors
403+ // The cleanup handler ensures FDs are closed AFTER DispatchIO is done with them,
404+ // preventing "BUG IN CLIENT OF LIBDISPATCH: Unexpected EV_VANISHED" crash
405+ // This applies to both Subprocess and forkpty paths
406+ io? . close ( )
407+ io = nil
408+ childfd = - 1
409+
394410 if shellPid != 0 {
395411 kill ( shellPid, SIGTERM)
396412 }
397-
413+
398414 running = false
399415 }
400416
0 commit comments