@@ -991,13 +991,17 @@ def fatal_error(msg, error=None):
991991 class StreamRedirector(io.TextIOBase):
992992 def __init__(self, stream_name):
993993 self.stream_name = stream_name
994+ self._buf = []
994995 def write(self, s):
995996 if isinstance(s, bytes):
996997 s = s.decode('utf-8', 'replace')
998+ self._buf.append(s)
997999 msg = {'stream': self.stream_name, 'data': s}
9981000 _original_stdout.write(json.dumps(msg) + '\\ n')
9991001 _original_stdout.flush()
10001002 return len(s)
1003+ def getvalue(self):
1004+ return ''.join(self._buf)
10011005 def isatty(self):
10021006 return setup_data.get('isatty', False)
10031007 def flush(self):
@@ -1006,7 +1010,7 @@ def flush(self):
10061010 def buffer(self):
10071011 class DummyBuffer:
10081012 def write(self_buf, b):
1009- return self.write(b)
1013+ return self.write(b.decode('utf-8', 'replace') if isinstance(b, bytes) else b )
10101014 def flush(self_buf):
10111015 pass
10121016 return DummyBuffer()
@@ -1061,6 +1065,8 @@ def flush(self_buf):
10611065 sys.stderr.write(traceback.format_exc())
10621066 exit_code = 1
10631067
1068+ _captured_stdout = sys.stdout.getvalue() if hasattr(sys.stdout, 'getvalue') else ''
1069+ _captured_stderr = sys.stderr.getvalue() if hasattr(sys.stderr, 'getvalue') else ''
10641070 sys.stdout = old_stdout
10651071 sys.stderr = old_stderr
10661072
@@ -1092,7 +1098,7 @@ def flush(self_buf):
10921098 _t_done = _wtime.perf_counter()
10931099 sys.stderr.write(f'[WORKER-TIMING] worker total req\u2192 COMPLETED: {(_t_done-_t_req)*1000:.2f}ms\\ n')
10941100 sys.stderr.flush()
1095- _original_stdout.write(json.dumps({'status': 'COMPLETED', 'exit_code': exit_code}) + '\\ n')
1101+ _original_stdout.write(json.dumps({'status': 'COMPLETED', 'exit_code': exit_code, 'stdout': _captured_stdout, 'stderr': _captured_stderr }) + '\\ n')
10961102 _original_stdout.flush()
10971103
10981104 elif _req_type == 'run_uv':
@@ -2815,32 +2821,26 @@ def _start_worker(self):
28152821 self .force_shutdown ()
28162822 raise RuntimeError (_ ('Failed to send setup: {}' ).format (e ))
28172823
2818- # Wait for READY with timeout
2824+ # Wait for READY with timeout — read from stdout_queue (fed by _reader_thread)
2825+ import queue as _q
28192826 try :
2820- # Windows-compatible waiting for READY
2827+ deadline = time . time () + 30.0
28212828 ready_line = None
2822- if IS_WINDOWS :
2823- result = [None ]
2824- def try_read ():
2825- try : result [0 ] = self .process .stdout .readline ()
2826- except : pass
2827- t = threading .Thread (target = try_read , daemon = True )
2828- t .start ()
2829- t .join (timeout = 30.0 )
2830- ready_line = result [0 ]
2831- else :
2832- readable , unused , unused = select .select ([self .process .stdout ], [], [], 30.0 )
2833- if readable :
2834- ready_line = self .process .stdout .readline ()
2829+ while time .time () < deadline :
2830+ try :
2831+ ready_line = self .stdout_queue .get (timeout = max (0.1 , deadline - time .time ()))
2832+ break
2833+ except _q .Empty :
2834+ if self .process .poll () is not None :
2835+ raise RuntimeError (f"Worker crashed during startup (check { DAEMON_LOG_FILE } )" )
2836+ continue
28352837
28362838 if not ready_line :
2837- # Check if process died
28382839 if self .process .poll () is not None :
28392840 raise RuntimeError (f"Worker crashed during startup (check { DAEMON_LOG_FILE } )" )
28402841 raise RuntimeError ("Worker timeout waiting for READY" )
28412842
28422843 ready_line = ready_line .strip ()
2843-
28442844 if not ready_line :
28452845 raise RuntimeError ("Worker sent blank READY line" )
28462846
@@ -2852,7 +2852,6 @@ def try_read():
28522852 if ready_status .get ("status" ) != "READY" :
28532853 raise RuntimeError (_ ('Worker failed to initialize: {}' ).format (ready_status ))
28542854
2855- # Success!
28562855 self .last_health_check = time .time ()
28572856 self .health_check_failures = 0
28582857
@@ -4023,6 +4022,7 @@ def _run_cli(self, req, conn):
40234022 # fatal - breaking on parse failure was causing "Worker failed to
40244023 # send READY" and forcing a cold execv fallback on every call.
40254024 ready_parsed = None
4025+ _drain_count = 0
40264026 ready_line = None
40274027 _deadline = _rt .perf_counter () + 30.0
40284028 while _rt .perf_counter () < _deadline :
@@ -4052,7 +4052,7 @@ def _run_cli(self, req, conn):
40524052 _t_ready = _rt .perf_counter ()
40534053 if os .environ .get ('OMNIPKG_DEBUG' ) == '1' :
40544054 _elapsed_us = int ((_t_ready - locals ().get ('_t_post_flush' , _t_ready )) * 1000000 )
4055- safe_print (f"[RUN-CLI-T] queue.get READY={ _elapsed_us } us drained={ _drain_count } line={ repr (ready_line )} " , file = sys .stderr )
4055+ safe_print (f"[RUN-CLI-T] queue.get READY={ _elapsed_us } us drained=? line={ repr (ready_line )!r } " , file = sys .stderr )
40564056
40574057 _t_first_frame = None
40584058 _frame_count = 0
@@ -4143,25 +4143,14 @@ def _run_cli(self, req, conn):
41434143 worker .process .stdin .flush ()
41444144 except Exception :
41454145 break
4146- # COMPLETED / ERROR - forward and finish
4147- elif msg .get ("status" ) in ("COMPLETED" , "ERROR" ):
4148- _t_completed = _rt .perf_counter ()
4149- safe_print (
4150- f"[RUN-CLI] COMPLETED in { (_t_completed - _t0 )* 1000 :.2f} ms total "
4151- f"(worker->daemon->client relay)" ,
4152- file = sys .stderr
4153- )
4154- # Store last result so the recycle gate can read
4155- # _worker_abi_contaminated before putting this worker
4156- # back into idle_pools. _run_cli bypasses execute_shm_task
4157- # so we must set it here directly.
4158- worker ._last_result = msg
4146+ else :
4147+ # stream frame ({"stream":"stdout/stderr","data":"..."}) — forward to C dispatcher
4148+ _frame_count += 1
4149+ raw = line .encode ("utf-8" )
41594150 try :
4160- send_json ( conn , msg )
4151+ conn . sendall ( len ( raw ). to_bytes ( 8 , "big" ) + raw )
41614152 except Exception :
41624153 pass
4163- _completed_cleanly = True
4164- break
41654154
41664155 except Exception as e :
41674156 try :
0 commit comments