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:
@@ -141,12 +141,15 @@ class SessionManager:
|
|||||||
)
|
)
|
||||||
log.info("Created app session %s", session_id)
|
log.info("Created app session %s", session_id)
|
||||||
|
|
||||||
self.sessions[session_id] = session_process
|
# Open the session BEFORE registering it, so it's fully initialized
|
||||||
self.routes[route_key] = session_id
|
# when other code can access it via sessions/routes dicts
|
||||||
|
|
||||||
await session_process.open(*size)
|
await session_process.open(*size)
|
||||||
log.debug("Session %s opened and ready", session_id)
|
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
|
return session_process
|
||||||
|
|
||||||
async def close_session(self, session_id: SessionID) -> None:
|
async def close_session(self, session_id: SessionID) -> None:
|
||||||
|
|||||||
@@ -64,9 +64,10 @@ class TerminalSession(Session):
|
|||||||
|
|
||||||
async def open(self, width: int = 80, height: int = 24) -> None:
|
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)
|
log.info("Opening terminal session %s with command: %s", self.session_id, self.command)
|
||||||
# Initialize pyte screen with the requested size
|
# Initialize pyte screen with the requested size (under lock to prevent races)
|
||||||
self._screen = pyte.Screen(width, height)
|
async with self._screen_lock:
|
||||||
self._stream = pyte.Stream(self._screen)
|
self._screen = pyte.Screen(width, height)
|
||||||
|
self._stream = pyte.Stream(self._screen)
|
||||||
|
|
||||||
pid, master_fd = pty.fork()
|
pid, master_fd = pty.fork()
|
||||||
self.pid = pid
|
self.pid = pid
|
||||||
@@ -99,9 +100,10 @@ class TerminalSession(Session):
|
|||||||
fcntl.ioctl(self.master_fd, termios.TIOCSWINSZ, buf)
|
fcntl.ioctl(self.master_fd, termios.TIOCSWINSZ, buf)
|
||||||
|
|
||||||
async def set_terminal_size(self, width: int, height: int) -> None:
|
async def set_terminal_size(self, width: int, height: int) -> None:
|
||||||
|
# First resize the PTY (blocking call in executor)
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
await loop.run_in_executor(None, self._set_terminal_size, width, height)
|
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:
|
async with self._screen_lock:
|
||||||
self._screen.resize(height, width)
|
self._screen.resize(height, width)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user