1919TIMEOUT_ERROR_EXIT_CODE = 124
2020POLL_INTERVAL_SECONDS = 0.2
2121SIGTERM_GRACE_PERIOD_SECONDS = 0.2
22- STDOUT_READ_TIMEOUT_SECONDS = 5
22+ STDOUT_READ_TIMEOUT_SECONDS = 2
2323F_SETPIPE_SIZE = 1031 # Linux-only constant
2424PIPE_SIZE_KB = 1024 # 1MB
2525
@@ -120,11 +120,27 @@ def execute_script( # noqa: C901
120120 # Set the pipe size to 1MB to avoid buffer overflows
121121 fcntl .fcntl (proc .stdout .fileno (), F_SETPIPE_SIZE , PIPE_SIZE_KB * 1024 ) # 1MB
122122
123+ # Drain stdout in a background thread to prevent pipe buffer deadlock.
124+ # macOS has a 64KB pipe buffer; without continuous draining, scripts that produce
125+ # more output than that block on write and never exit, causing spurious timeouts.
126+ output_chunks : list [str ] = []
127+
128+ def _drain_stdout () -> None :
129+ try :
130+ for chunk in iter (lambda : proc .stdout .read (8192 ), "" ):
131+ output_chunks .append (chunk )
132+ except (OSError , ValueError ):
133+ pass
134+
135+ reader = threading .Thread (target = _drain_stdout , daemon = True )
136+ reader .start ()
137+
123138 try :
124139 while proc .poll () is None :
125140 if time .time () - start_time >= script_timeout :
126141 _kill_process (proc )
127- partial_stdout = proc .stdout .read ()
142+ reader .join (timeout = 2 )
143+ partial_stdout = "" .join (output_chunks )
128144 exc = subprocess .TimeoutExpired (cmd , script_timeout )
129145 exc .stdout = partial_stdout
130146 raise exc
@@ -136,12 +152,13 @@ def execute_script( # noqa: C901
136152 else :
137153 time .sleep (POLL_INTERVAL_SECONDS )
138154
139- # Use communicate() with a timeout because child processes may hold the pipe open after the main process exits .
140- try :
141- stdout , _ = proc . communicate (timeout = STDOUT_READ_TIMEOUT_SECONDS )
142- except subprocess . TimeoutExpired :
155+ # Wait for the reader to finish draining remaining output .
156+ # Close stdout if child processes keep the pipe open beyond the grace period.
157+ reader . join (timeout = STDOUT_READ_TIMEOUT_SECONDS )
158+ if reader . is_alive () :
143159 proc .stdout .close ()
144- stdout = ""
160+ reader .join (timeout = 1 )
161+ stdout = "" .join (output_chunks )
145162 elapsed_time = time .time () - start_time
146163
147164 sanitized_script_output = _sanitize_script_output (stdout )
0 commit comments