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.
This commit is contained in:
GitHub Copilot
2026-01-24 11:17:18 +00:00
parent db22fd9357
commit 4d3a13f6ef
2 changed files with 12 additions and 7 deletions
+6 -3
View File
@@ -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:
+6 -4
View File
@@ -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)