"""Terminal session management for tool instances.""" import asyncio import uuid from typing import Any class TerminalSession: """Manages a single terminal session connected to a docker container.""" def __init__(self, session_id: str, instance_id: uuid.UUID, container_id: str) -> None: self.session_id = session_id self.instance_id = instance_id self.container_id = container_id self.process: asyncio.subprocess.Process | None = None self._closed = False async def start(self) -> None: """Start the docker exec process with a shell.""" self.process = await asyncio.create_subprocess_exec( "docker", "exec", "-i", self.container_id, "/bin/sh", "-c", "exec bash -l || exec sh -l", stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, ) async def read_output(self) -> bytes: """Read output from the process.""" if self.process is None or self.process.stdout is None: return b"" try: return await self.process.stdout.read(4096) except (asyncio.CancelledError, BrokenPipeError): return b"" async def write_input(self, data: bytes) -> None: """Write input to the process.""" if self.process is None or self.process.stdin is None or self._closed: return try: self.process.stdin.write(data) await self.process.stdin.drain() except (BrokenPipeError, ConnectionResetError): pass async def resize(self, cols: int, rows: int) -> None: """Resize the terminal.""" if self._closed: return try: proc = await asyncio.create_subprocess_exec( "docker", "exec", self.container_id, "stty", "cols", str(cols), "rows", str(rows), stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL, ) await proc.wait() except Exception: pass async def close(self) -> None: """Close the session and cleanup.""" if self._closed: return self._closed = True if self.process is not None: try: self.process.kill() await asyncio.wait_for(self.process.wait(), timeout=2.0) except (asyncio.TimeoutError, ProcessLookupError): pass def is_alive(self) -> bool: """Check if the session process is still running.""" if self.process is None: return False return self.process.returncode is None