# apps/api/src/services/terminal
dir: apps/api/src/services/terminal

index: apps/api/src/services/terminal/.pi-map.index.md

## role
Provides WebSocket-based terminal session management for containerized environments with PTY support, session persistence, and resource limits.
## files
- __init__.py | Exports the public API for the terminal services module by re-exporting TerminalManager, TerminalSession, and MaxSessionsExceededError. | dep: src.services.terminal.terminal_manager, src.services.terminal.terminal_session
- terminal_manager.py | Manages WebSocket-based terminal sessions with persistence, idle cleanup, and per-instance session limits for containerized environments. | exp: class:MaxSessionsExceededError, method:__init__(self, instance_id: str, max_sessions) → None, call:super().__init__, class:TerminalManager, method:__init__(self) → None, call:self._start_idle_check, method:_start_idle_check(self) → None, call:self._idle_check_task.done, call:asyncio.get_running_loop, call:loop.create_task, call:self._idle_check_loop, method:_idle_check_loop(self) → None, call:asyncio.sleep, call:self._cleanup_idle_sessions, call:logger.error, method:_cleanup_idle_sessions(self) → None, call:list, call:self._sessions.items, call:session.is_idle, call:idle_keys.append, call:logger.info, call:self._sessions.get, call:idle_session.close, call:asyncio.create_task, call:self._mark_closed_in_db, method:_insert_db_session_row(self, session_id: str, instance_id: uuid.UUID, name: str) → None, call:SessionLocal, call:pg_insert(TerminalSessionModel) .values( id=uuid.UUID(session_id), instance_id=instance_id, name=name, status="active", created_at=datetime.now(timezone.utc), last_activity_at=datetime.now(timezone.utc), ) .on_conflict_do_nothing, call:uuid.UUID, call:datetime.now, call:db_session.execute, call:db_session.commit, call:logger.debug, call:logger.error, method:_mark_closed_in_db(self, session_id: str) → None, call:SessionLocal, call:db_session.get, call:uuid.UUID, call:datetime.now, call:db_session.commit, call:logger.debug, call:logger.error, method:_count_sessions_for_instance(self, instance_id_str: str) → int, call:sum, method:create_session(self, instance_id: uuid.UUID, container_id: str, startup_command, name, session_id, container_user) → TerminalSession, call:str, call:self._count_sessions_for_instance, call:uuid.uuid4, call:TerminalSession, call:session.start, call:asyncio.create_task, call:self._insert_db_session_row, call:logger.info, raise:MaxSessionsExceededError, method:get_or_create_session(self, instance_id: uuid.UUID, container_id: str, startup_command, container_user) → TerminalSession, call:self._start_idle_check, call:str, call:session.is_alive, call:logger.debug, call:session.close, call:logger.info, call:uuid.uuid4, call:TerminalSession, call:session.start, call:asyncio.create_task, call:self._insert_db_session_row, method:get_session(self, instance_id: str, session_id: str) → TerminalSession | None, call:self._sessions.get, call:self._sessions.items, method:_find_key_by_internal_id(self, instance_id: str, internal_session_id: str) → tuple[str, str] | None, call:self._sessions.items, method:get_sessions_for_instance(self, instance_id: str) → list[TerminalSession], call:self._sessions.items, method:close_session(self, instance_id: str, session_id: str) → None, call:self._sessions.pop, call:session.close, call:asyncio.create_task, call:self._mark_closed_in_db, call:logger.info, method:attach_websocket(self, session: TerminalSession, websocket: WebSocket) → None, call:session.has_websockets, call:logger.debug, call:list, call:ws.close, call:session._websockets.clear, call:session.attach_websocket, call:session.get_buffer, call:websocket.send_bytes, method:detach_websocket(self, session: TerminalSession, websocket: WebSocket) → None, call:session.detach_websocket, method:reset_session(self, instance_id: uuid.UUID, container_id: str, startup_command, session_id, name, container_user) → TerminalSession, call:str, call:logger.debug, call:self._sessions.pop, call:old_session.close, call:asyncio.create_task, call:self._mark_closed_in_db, call:uuid.uuid4, call:TerminalSession, call:new_session.start, call:self._insert_db_session_row, method:close_all(self) → None, call:list, call:self._sessions.values, call:self._sessions.clear, call:session.close, call:self._idle_check_task.done, call:self._idle_check_task.cancel | dep: asyncio, logging, uuid, datetime, fastapi, sqlalchemy.dialects.postgresql, src.database, src.models, src.services.terminal.terminal_session, fastapi.WebSocket
- terminal_session.py | Manages an asyncio-native terminal session using PTY with docker exec, featuring event-driven I/O, output batching, and flow control for WebSocket clients. | exp: class:TerminalSession, method:__init__(self, session_id: str, instance_id: uuid.UUID, container_id: str, startup_command, name, container_user) → None, call:deque, call:set, call:time.time, call:self._generate_name, call:str, call:bytearray, call:asyncio.Lock, method:start(self, startup_command) → None, call:exec_cmd.extend, call:pty.openpty, call:self._set_terminal_size, call:logger.debug, call:asyncio.create_subprocess_exec, call:os.close, call:time.time, call:self._start_reading, method:_start_reading(self) → None, call:asyncio.get_event_loop, call:loop.add_reader, call:logger.debug, call:logger.error, method:_stop_reading(self) → None, call:asyncio.get_event_loop, call:loop.remove_reader, method:_on_fd_readable(self) → None, call:os.read, call:logger.debug, call:self._handle_eof, call:self._add_to_buffer, call:time.time, call:self._queue_output, method:_add_to_buffer(self, data: bytes) → None, call:self._output_buffer.append, call:len, call:self._output_buffer.popleft, method:_queue_output(self, data: bytes) → None, call:self._batch_buffer.extend, call:len, call:self._pause_output, call:asyncio.get_event_loop, call:loop.call_later, method:_flush_batch_sync(self) → None, call:self._batch_buffer.clear, call:bytes, call:set, call:list, call:asyncio.create_task, call:self._send_bytes, call:dead_sockets.add, method:_send_bytes(self, ws: Any, payload: bytes) → None, call:ws.send_bytes, call:self._websockets.discard, method:acknowledge_data(self, char_count: int) → None, call:max, call:self._resume_output, call:self._ack_timeout_handle.cancel, call:asyncio.get_event_loop, call:loop.call_later, method:_ack_timeout_fallback(self) → None, call:logger.warning, call:self._resume_output, method:_pause_output(self) → None, call:self._stop_reading, call:logger.debug, method:_resume_output(self) → None, call:self._start_reading, call:logger.debug, method:get_buffer(self) → bytes, call:b"".join, method:_handle_eof(self) → None, call:self._stop_reading, call:self.process._transport.close, call:set, call:self._websockets.clear, call:asyncio.create_task, call:ws.close, call:logger.info, method:write_input(self, data: bytes) → None, call:os.write, call:time.time, call:logger.debug, call:self._handle_eof, method:_set_terminal_size(self, cols: int, rows: int) → None, call:logger.warning, call:struct.pack, call:fcntl.ioctl, call:logger.debug, call:logger.error, method:resize(self, cols: int, rows: int) → None, call:logger.warning, call:logger.debug, call:self._set_terminal_size, call:os.kill, method:reset(self) → None, call:self.close, call:self._output_buffer.clear, call:self._websockets.clear, call:self._batch_buffer.clear, method:close(self) → None, call:self._stop_reading, call:self._batch_timer.cancel, call:self._ack_timeout_handle.cancel, call:os.close, call:self.process.kill, call:asyncio.wait_for, call:self.process.wait, method:is_alive(self) → bool, method:is_idle(self) → bool, call:time.time, method:attach_websocket(self, websocket: Any) → None, call:self._websockets.add, call:time.time, method:detach_websocket(self, websocket: Any) → None, call:self._websockets.discard, method:has_websockets(self) → bool, call:len, method:send_to_all(self, data: bytes) → None, call:set, call:ws.send_bytes, call:dead_sockets.add, call:self._websockets.discard, method:read_output(self) → bytes | dep: asyncio, logging, os, pty, signal, struct, fcntl, time, uuid, collections, typing, collections.deque, typing.Any
## arch
Asyncio-native event-driven architecture using PTY/docker exec with manager pattern (TerminalManager orchestrates TerminalSession instances), featuring output batching, flow control, idle cleanup, and per-instance session limiting.
## tags
call:self., session, call:logger.debug, idle, output, terminal, check, task
## symbols
- MaxSessionsExceededError
- TerminalManager
- TerminalSession
- __init__
- _start_idle_check
- _idle_check_loop
- _cleanup_idle_sessions
- _insert_db_session_row
## workflows
- change terminal behavior
  read: __init__.py, terminal_manager.py, terminal_session.py
## dirty
-
