From 4d3a13f6ef772c712caac9dc918c9e02c24002e7 Mon Sep 17 00:00:00 2001 From: GitHub Copilot Date: Sat, 24 Jan 2026 11:17:18 +0000 Subject: [PATCH] fix: resolve terminal lifecycle race conditions 1. Lock pyte screen initialization in open() to prevent races with concurrent _update_screen() calls 2. Reorder session registration: call open() BEFORE adding to sessions/routes dicts, so sessions are fully initialized before other code can access them 3. Add clarifying comment that PTY resize completes before pyte resize These fixes prevent dimension mismatches between PTY and pyte screen that could cause content wrapping in screenshots. --- src/textual_webterm/session_manager.py | 9 ++++++--- src/textual_webterm/terminal_session.py | 10 ++++++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/textual_webterm/session_manager.py b/src/textual_webterm/session_manager.py index 0f243d6..ad1fe78 100644 --- a/src/textual_webterm/session_manager.py +++ b/src/textual_webterm/session_manager.py @@ -141,12 +141,15 @@ class SessionManager: ) log.info("Created app session %s", session_id) - self.sessions[session_id] = session_process - self.routes[route_key] = session_id - + # Open the session BEFORE registering it, so it's fully initialized + # when other code can access it via sessions/routes dicts await session_process.open(*size) log.debug("Session %s opened and ready", session_id) + # Now register the fully initialized session + self.sessions[session_id] = session_process + self.routes[route_key] = session_id + return session_process async def close_session(self, session_id: SessionID) -> None: diff --git a/src/textual_webterm/terminal_session.py b/src/textual_webterm/terminal_session.py index 8795670..46aac48 100644 --- a/src/textual_webterm/terminal_session.py +++ b/src/textual_webterm/terminal_session.py @@ -64,9 +64,10 @@ class TerminalSession(Session): async def open(self, width: int = 80, height: int = 24) -> None: log.info("Opening terminal session %s with command: %s", self.session_id, self.command) - # Initialize pyte screen with the requested size - self._screen = pyte.Screen(width, height) - self._stream = pyte.Stream(self._screen) + # Initialize pyte screen with the requested size (under lock to prevent races) + async with self._screen_lock: + self._screen = pyte.Screen(width, height) + self._stream = pyte.Stream(self._screen) pid, master_fd = pty.fork() self.pid = pid @@ -99,9 +100,10 @@ class TerminalSession(Session): fcntl.ioctl(self.master_fd, termios.TIOCSWINSZ, buf) async def set_terminal_size(self, width: int, height: int) -> None: + # First resize the PTY (blocking call in executor) loop = asyncio.get_running_loop() await loop.run_in_executor(None, self._set_terminal_size, width, height) - # Resize pyte screen to match + # Then resize pyte screen to match (after PTY resize completes) async with self._screen_lock: self._screen.resize(height, width)