Skip to content

Commit 6e4d341

Browse files
committed
Fix pty/termios crash on Windows: fall back to pipe on non-Unix
1 parent fd1dfc9 commit 6e4d341

1 file changed

Lines changed: 88 additions & 56 deletions

File tree

gui.py

Lines changed: 88 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -471,23 +471,11 @@ def _append_line(text: str, color: str | None = None) -> None:
471471
def _worker() -> None:
472472
rc = -1
473473
try:
474-
import pty as _pty, io as _io
475-
env = {**os.environ, "PYTHONUNBUFFERED": "1",
476-
"TERM": "xterm-256color", "COLUMNS": "100"}
477-
master_fd, slave_fd = _pty.openpty()
478-
state.proc = subprocess.Popen(
479-
cmd,
480-
stdout=slave_fd, stderr=slave_fd,
481-
close_fds=True,
482-
cwd=str(_get_output_dir()),
483-
env=env,
484-
)
485-
os.close(slave_fd)
486-
487-
_MAX_LINES = 500
488-
_ANSI_RE = re.compile(
474+
import io as _io
475+
_ANSI_RE = re.compile(
489476
r"\x1b\[[0-9;]*[mABCDEFGHJKST]|\x1b\][^\x07]*\x07"
490477
)
478+
_MAX_LINES = 500
491479
_last_update = time.monotonic()
492480
buf = "" # current line being assembled
493481

@@ -511,47 +499,91 @@ def _new_line() -> None:
511499
if excess > 0:
512500
del self._lines.controls[:excess]
513501

514-
try:
515-
with _io.open(master_fd, "rb", closefd=True) as master:
516-
while True:
517-
try:
518-
chunk = master.read(4096)
519-
except OSError:
520-
break
521-
if not chunk:
522-
break
523-
text = _ANSI_RE.sub(
524-
"", chunk.decode("utf-8", errors="replace")
525-
)
526-
for ch in text:
527-
if ch == "\r":
528-
# Carriage return: next chars overwrite
529-
# the current line — discard old buffer.
530-
buf = ""
531-
elif ch == "\n":
532-
if not buf.startswith("<frozen importlib"):
533-
item = _cur()
534-
item.value = buf
535-
low = buf.lower().lstrip()
536-
if low.startswith(
537-
("error", "traceback",
538-
"exception")):
539-
item.color = C_ERROR
540-
elif low.startswith("warning"):
541-
item.color = C_WARN
542-
_new_line()
543-
buf = ""
544-
else:
545-
buf += ch
546-
547-
now = time.monotonic()
548-
if now - _last_update >= _UPDATE_INTERVAL:
549-
if buf:
550-
_cur().value = buf
551-
self.page.update()
552-
_last_update = now
553-
except OSError:
554-
pass
502+
def _process_chunk(raw: bytes) -> None:
503+
nonlocal buf
504+
text = _ANSI_RE.sub(
505+
"", raw.decode("utf-8", errors="replace")
506+
)
507+
for ch in text:
508+
if ch == "\r":
509+
buf = ""
510+
elif ch == "\n":
511+
if not buf.startswith("<frozen importlib"):
512+
item = _cur()
513+
item.value = buf
514+
low = buf.lower().lstrip()
515+
if low.startswith(
516+
("error", "traceback", "exception")):
517+
item.color = C_ERROR
518+
elif low.startswith("warning"):
519+
item.color = C_WARN
520+
_new_line()
521+
buf = ""
522+
else:
523+
buf += ch
524+
525+
# ── Launch subprocess ─────────────────────────────────────
526+
# On Unix use a pty so tqdm/rich see a real terminal and use
527+
# \r for in-place refresh. On Windows pty is unavailable so
528+
# fall back to a plain pipe (progress bars produce extra lines
529+
# but work correctly).
530+
env = {**os.environ, "PYTHONUNBUFFERED": "1"}
531+
_use_pty = False
532+
if sys.platform != "win32":
533+
try:
534+
import pty as _pty
535+
master_fd, slave_fd = _pty.openpty()
536+
env.update({"TERM": "xterm-256color", "COLUMNS": "100"})
537+
state.proc = subprocess.Popen(
538+
cmd,
539+
stdout=slave_fd, stderr=slave_fd,
540+
close_fds=True,
541+
cwd=str(_get_output_dir()),
542+
env=env,
543+
)
544+
os.close(slave_fd)
545+
_use_pty = True
546+
except Exception:
547+
_use_pty = False
548+
549+
if not _use_pty:
550+
state.proc = subprocess.Popen(
551+
cmd,
552+
stdout=subprocess.PIPE,
553+
stderr=subprocess.STDOUT,
554+
cwd=str(_get_output_dir()),
555+
env=env,
556+
)
557+
558+
# ── Stream output ─────────────────────────────────────────
559+
if _use_pty:
560+
try:
561+
with _io.open(master_fd, "rb", closefd=True) as master:
562+
while True:
563+
try:
564+
chunk = master.read(4096)
565+
except OSError:
566+
break
567+
if not chunk:
568+
break
569+
_process_chunk(chunk)
570+
now = time.monotonic()
571+
if now - _last_update >= _UPDATE_INTERVAL:
572+
if buf:
573+
_cur().value = buf
574+
self.page.update()
575+
_last_update = now
576+
except OSError:
577+
pass
578+
else:
579+
for line in state.proc.stdout:
580+
_process_chunk(line.encode())
581+
now = time.monotonic()
582+
if now - _last_update >= _UPDATE_INTERVAL:
583+
if buf:
584+
_cur().value = buf
585+
self.page.update()
586+
_last_update = now
555587

556588
if buf:
557589
_cur().value = buf

0 commit comments

Comments
 (0)