@@ -125,6 +125,26 @@ def _should_use_color() -> bool:
125125 return False
126126
127127
128+ def _safe_stream_write (stream , text : str , * , flush : bool = True ) -> None :
129+ """Write *text* to *stream*, falling back to UTF-8 bytes on encode errors."""
130+ try :
131+ stream .write (text )
132+ if flush :
133+ stream .flush ()
134+ except UnicodeEncodeError :
135+ buffer = getattr (stream , "buffer" , None )
136+ if buffer is None :
137+ return
138+ try :
139+ buffer .write (text .encode ("utf-8" , errors = "replace" ))
140+ if flush :
141+ buffer .flush ()
142+ except (BrokenPipeError , OSError ):
143+ pass
144+ except (BrokenPipeError , OSError ):
145+ pass
146+
147+
128148# ---------------------------------------------------------------------------
129149# Time formatting
130150# ---------------------------------------------------------------------------
@@ -282,15 +302,13 @@ def update(self, current: int) -> None:
282302 return # throttle to 1 Hz
283303 self ._last_draw_time = now
284304 line = self ._render_bar (current )
285- self .stream .write (f"\r { line } " )
286- self .stream .flush ()
305+ _safe_stream_write (self .stream , f"\r { line } " )
287306 else :
288307 # CI mode: emit at every 10 % boundary
289308 bucket = min (current * 100 // self .total , 100 ) // 10
290309 if bucket > self ._last_pct_bucket :
291310 self ._last_pct_bucket = bucket
292- self .stream .write (self ._render_bar_plain (current ) + "\n " )
293- self .stream .flush ()
311+ _safe_stream_write (self .stream , self ._render_bar_plain (current ) + "\n " )
294312 except (BrokenPipeError , OSError ):
295313 pass
296314
@@ -307,8 +325,7 @@ def finish(self, summary: str | None = None) -> None: # noqa: ARG002
307325 # Clear the in-place line
308326 try :
309327 width = shutil .get_terminal_size ((80 , 24 )).columns
310- self .stream .write ("\r " + " " * width + "\r " )
311- self .stream .flush ()
328+ _safe_stream_write (self .stream , "\r " + " " * width + "\r " )
312329 except (BrokenPipeError , OSError ):
313330 pass
314331
@@ -391,8 +408,7 @@ def update(self, idx: int, current: int) -> None:
391408 if bucket > self ._ci_buckets [idx ]:
392409 self ._ci_buckets [idx ] = bucket
393410 pct = bucket * 10
394- self .stream .write (f" [..] { self ._labels [idx ]} { pct :>3d} %\n " )
395- self .stream .flush ()
411+ _safe_stream_write (self .stream , f" [..] { self ._labels [idx ]} { pct :>3d} %\n " )
396412
397413 def finish (self ) -> None :
398414 """Clear all progress lines. Safe to call more than once."""
@@ -403,12 +419,11 @@ def finish(self) -> None:
403419 try :
404420 width = shutil .get_terminal_size ((80 , 24 )).columns
405421 # Move up N lines and clear each
406- self .stream . write ( f"\033 [{ self ._n } A" )
422+ _safe_stream_write ( self .stream , f"\033 [{ self ._n } A" , flush = False )
407423 for _ in range (self ._n ):
408- self .stream . write ( " " * width + "\n " )
424+ _safe_stream_write ( self .stream , " " * width + "\n " , flush = False )
409425 # Move back up to start
410- self .stream .write (f"\033 [{ self ._n } A\r " )
411- self .stream .flush ()
426+ _safe_stream_write (self .stream , f"\033 [{ self ._n } A\r " )
412427 except (BrokenPipeError , OSError ):
413428 pass
414429
@@ -417,7 +432,7 @@ def _redraw(self) -> None:
417432 try :
418433 if self ._drawn :
419434 # Move cursor up to first slot line
420- self .stream . write ( f"\033 [{ self ._n } A" )
435+ _safe_stream_write ( self .stream , f"\033 [{ self ._n } A" , flush = False )
421436
422437 lines = []
423438 width = shutil .get_terminal_size ((80 , 24 )).columns
@@ -427,8 +442,7 @@ def _redraw(self) -> None:
427442 padding = max (0 , width - _display_width (line ))
428443 lines .append (line + " " * padding )
429444
430- self .stream .write ("\n " .join (lines ) + "\n " )
431- self .stream .flush ()
445+ _safe_stream_write (self .stream , "\n " .join (lines ) + "\n " )
432446 self ._drawn = True
433447 except (BrokenPipeError , OSError ):
434448 pass
@@ -505,11 +519,7 @@ def __init__(
505519 # -- internal helpers ---------------------------------------------------
506520
507521 def _write (self , text : str ) -> None :
508- try :
509- self .stream .write (text + "\n " )
510- self .stream .flush ()
511- except (BrokenPipeError , OSError ):
512- pass # never crash the build for a logging failure
522+ _safe_stream_write (self .stream , text + "\n " )
513523
514524 def _pad_rail (self , inner : str , inner_plain_len : int ) -> str :
515525 """Pad a step header *inner* string with ``━`` to fill the width."""
0 commit comments