Skip to main content
Glama
session.py5.53 kB
""" Terminal session management using terminado for cross-platform PTY support. """ import logging import os import uuid from datetime import datetime from typing import Optional from terminado import NamedTermManager logger = logging.getLogger(__name__) class TerminalSession: """ Represents a single terminal session with PTY support. Uses terminado for cross-platform compatibility (Windows/Linux/macOS). Handles encoding in UTF-8 for special characters. """ def __init__( self, rows: int = 24, cols: int = 80, shell_command: Optional[list] = None, ): """ Initialize a new terminal session. Args: rows: Number of rows (height) of the terminal cols: Number of columns (width) of the terminal shell_command: Custom shell command (defaults to system shell) """ self.id = str(uuid.uuid4()) self.rows = rows self.cols = cols self.created_at = datetime.utcnow() self.is_alive = False # Terminal manager for PTY self.term_manager = NamedTermManager( shell_command=shell_command or self._get_default_shell(), max_terminals=1, ) # Terminal process self.terminal = None # Output buffer self._output_buffer = [] logger.info(f"Terminal session created: {self.id}") def _get_default_shell(self) -> list: """ Get default shell command for the current OS. Returns: Shell command as list """ if os.name == "nt": # Windows return ["cmd.exe"] else: # Unix/Linux/macOS return [os.environ.get("SHELL", "/bin/bash")] async def start(self): """Start the terminal session.""" if self.is_alive: logger.warning(f"Terminal {self.id} already started") return try: # Create new terminal self.terminal = self.term_manager.new_terminal() self.terminal.ptyproc.setwinsize(self.rows, self.cols) self.is_alive = True logger.info(f"Terminal {self.id} started") except Exception as e: logger.error(f"Failed to start terminal {self.id}: {e}") raise async def write(self, data: str): """ Write data to the terminal. Args: data: String data to write (commands, keystrokes, etc.) """ if not self.is_alive or not self.terminal: raise RuntimeError(f"Terminal {self.id} is not running") try: import asyncio # Encode to UTF-8 bytes encoded_data = data.encode("utf-8") # Run blocking write in executor to avoid blocking event loop loop = asyncio.get_event_loop() await loop.run_in_executor( None, self.terminal.ptyproc.write, encoded_data ) except Exception as e: logger.error(f"Failed to write to terminal {self.id}: {e}") raise async def read(self, timeout: float = 0.1) -> str: """ Read available output from the terminal. Args: timeout: Timeout in seconds Returns: Available output as UTF-8 string """ if not self.is_alive or not self.terminal: raise RuntimeError(f"Terminal {self.id} is not running") try: # Try to read available data with non-blocking read import asyncio # Small delay to let data accumulate await asyncio.sleep(timeout) # Try to read from ptyproc try: output = self.terminal.ptyproc.read(1024) # Safely handle both bytes and string if output: try: # Try to decode if it's bytes return output.decode("utf-8", errors="replace") except AttributeError: # Already a string return str(output) except (OSError, IOError): # No data available pass return "" except Exception as e: logger.error(f"Failed to read from terminal {self.id}: {e}") return "" async def resize(self, rows: int, cols: int): """ Resize the terminal. Args: rows: New number of rows cols: New number of columns """ if not self.is_alive or not self.terminal: raise RuntimeError(f"Terminal {self.id} is not running") try: self.rows = rows self.cols = cols self.terminal.ptyproc.setwinsize(rows, cols) logger.info(f"Terminal {self.id} resized to {rows}x{cols}") except Exception as e: logger.error(f"Failed to resize terminal {self.id}: {e}") raise async def close(self): """Close the terminal session and cleanup resources.""" if not self.is_alive: logger.warning(f"Terminal {self.id} already closed") return try: if self.terminal: self.terminal.kill() self.is_alive = False logger.info(f"Terminal {self.id} closed") except Exception as e: logger.error(f"Failed to close terminal {self.id}: {e}") raise

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/alejoair/mcp-terminal'

If you have feedback or need assistance with the MCP directory API, please join our Discord server