mirror of
https://github.com/hanzoai/mcp.git
synced 2026-10-29 02:55:49 +00:00
197 lines
6.4 KiB
Python
197 lines
6.4 KiB
Python
"""Session manager for coordinating bash sessions.
|
|
|
|
This module provides the SessionManager class which manages the lifecycle
|
|
of BashSession instances, handling creation, retrieval, and cleanup.
|
|
"""
|
|
|
|
import shutil
|
|
import threading
|
|
from typing import Self, final
|
|
|
|
from hanzo_mcp.tools.shell.bash_session import BashSession
|
|
from hanzo_mcp.tools.shell.session_storage import SessionStorage
|
|
|
|
|
|
@final
|
|
class SessionManager:
|
|
"""Manager for bash sessions with tmux support.
|
|
|
|
This class manages the creation, retrieval, and cleanup
|
|
of persistent bash sessions. By default, it uses a singleton pattern,
|
|
but can be instantiated directly for dependency injection scenarios.
|
|
"""
|
|
|
|
_instance: Self | None = None
|
|
_lock = threading.Lock()
|
|
|
|
def __new__(
|
|
cls, use_singleton: bool = True, session_storage: SessionStorage | None = None
|
|
) -> "SessionManager":
|
|
"""Create SessionManager instance.
|
|
|
|
Args:
|
|
use_singleton: If True, use singleton pattern. If False, create new instance.
|
|
"""
|
|
if not use_singleton:
|
|
# Create a new instance without singleton behavior
|
|
instance = super().__new__(cls)
|
|
instance._initialized = False
|
|
return instance
|
|
|
|
if cls._instance is None:
|
|
with cls._lock:
|
|
if cls._instance is None:
|
|
cls._instance = super().__new__(cls)
|
|
cls._instance._initialized = False
|
|
return cls._instance
|
|
|
|
def __init__(
|
|
self, use_singleton: bool = True, session_storage: SessionStorage | None = None
|
|
) -> None:
|
|
"""Initialize the session manager.
|
|
|
|
Args:
|
|
use_singleton: If True, use singleton pattern (for backward compatibility)
|
|
session_storage: Optional session storage instance for dependency injection
|
|
"""
|
|
if hasattr(self, "_initialized") and self._initialized:
|
|
return
|
|
self._initialized = True
|
|
self.default_timeout_seconds = 30
|
|
self.default_session_timeout = 1800 # 30 minutes
|
|
|
|
# Allow dependency injection of session storage for isolation
|
|
if session_storage is not None:
|
|
self._session_storage = session_storage
|
|
elif use_singleton:
|
|
# Use the default global SessionStorage for singleton instances
|
|
from hanzo_mcp.tools.shell.session_storage import SessionStorage
|
|
|
|
self._session_storage = SessionStorage
|
|
else:
|
|
# Use isolated instance storage for non-singleton instances
|
|
from hanzo_mcp.tools.shell.session_storage import (
|
|
SessionStorageInstance,
|
|
)
|
|
|
|
self._session_storage = SessionStorageInstance()
|
|
|
|
def is_tmux_available(self) -> bool:
|
|
"""Check if tmux is available on the system.
|
|
|
|
Returns:
|
|
True if tmux is available, False otherwise
|
|
"""
|
|
return shutil.which("tmux") is not None
|
|
|
|
def get_or_create_session(
|
|
self,
|
|
session_id: str,
|
|
work_dir: str,
|
|
username: str | None = None,
|
|
no_change_timeout_seconds: int | None = None,
|
|
max_memory_mb: int | None = None,
|
|
poll_interval: float | None = None,
|
|
) -> BashSession:
|
|
"""Get an existing session or create a new one.
|
|
|
|
Args:
|
|
session_id: Unique identifier for the session
|
|
work_dir: Working directory for the session
|
|
username: Username to run commands as
|
|
no_change_timeout_seconds: Timeout for commands with no output changes
|
|
max_memory_mb: Memory limit for the session
|
|
poll_interval: Polling interval in seconds (default 0.5, use 0.1 for tests)
|
|
|
|
Returns:
|
|
BashSession instance
|
|
|
|
Raises:
|
|
RuntimeError: If tmux is not available
|
|
"""
|
|
# Check if tmux is available
|
|
if not self.is_tmux_available():
|
|
raise RuntimeError(
|
|
"tmux is not available on this system. Please install tmux to use session-based command execution."
|
|
)
|
|
|
|
# Try to get existing session
|
|
session = self._session_storage.get_session(session_id)
|
|
if session is not None:
|
|
return session
|
|
|
|
# Create new session
|
|
timeout = no_change_timeout_seconds or self.default_timeout_seconds
|
|
interval = poll_interval if poll_interval is not None else 0.5
|
|
session = BashSession(
|
|
id=session_id,
|
|
work_dir=work_dir,
|
|
username=username,
|
|
no_change_timeout_seconds=timeout,
|
|
max_memory_mb=max_memory_mb,
|
|
poll_interval=interval,
|
|
)
|
|
|
|
# Store the session
|
|
self._session_storage.set_session(session_id, session)
|
|
|
|
return session
|
|
|
|
def get_session(self, session_id: str) -> BashSession | None:
|
|
"""Get an existing session.
|
|
|
|
Args:
|
|
session_id: Unique identifier for the session
|
|
|
|
Returns:
|
|
BashSession instance if found, None otherwise
|
|
"""
|
|
return self._session_storage.get_session(session_id)
|
|
|
|
def remove_session(self, session_id: str) -> bool:
|
|
"""Remove a session.
|
|
|
|
Args:
|
|
session_id: Unique identifier for the session
|
|
|
|
Returns:
|
|
True if session was removed, False if not found
|
|
"""
|
|
return self._session_storage.remove_session(session_id)
|
|
|
|
def cleanup_expired_sessions(self, max_age_seconds: int | None = None) -> int:
|
|
"""Clean up sessions that haven't been accessed recently.
|
|
|
|
Args:
|
|
max_age_seconds: Maximum age in seconds before cleanup
|
|
|
|
Returns:
|
|
Number of sessions cleaned up
|
|
"""
|
|
max_age = max_age_seconds or self.default_session_timeout
|
|
return self._session_storage.cleanup_expired_sessions(max_age)
|
|
|
|
def get_session_count(self) -> int:
|
|
"""Get the number of active sessions.
|
|
|
|
Returns:
|
|
Number of active sessions
|
|
"""
|
|
return self._session_storage.get_session_count()
|
|
|
|
def get_all_session_ids(self) -> list[str]:
|
|
"""Get all active session IDs.
|
|
|
|
Returns:
|
|
List of active session IDs
|
|
"""
|
|
return self._session_storage.get_all_session_ids()
|
|
|
|
def clear_all_sessions(self) -> int:
|
|
"""Clear all sessions.
|
|
|
|
Returns:
|
|
Number of sessions cleared
|
|
"""
|
|
return self._session_storage.clear_all_sessions()
|