mirror of
https://github.com/hanzoai/mcp.git
synced 2026-10-29 02:55:49 +00:00
326 lines
9.9 KiB
Python
326 lines
9.9 KiB
Python
"""Session storage module for shell command sessions.
|
|
|
|
This module provides storage functionality for managing persistent shell sessions.
|
|
It supports both global class-based storage (for backward compatibility) and
|
|
instance-based storage (for dependency injection scenarios).
|
|
"""
|
|
|
|
import time
|
|
from typing import TYPE_CHECKING, final
|
|
|
|
if TYPE_CHECKING:
|
|
from hanzo_mcp.tools.shell.bash_session import BashSession
|
|
|
|
|
|
@final
|
|
class SessionStorageInstance:
|
|
"""Instance-based storage for shell command sessions.
|
|
|
|
This class provides isolated storage for different SessionManager instances,
|
|
preventing shared state between different contexts. It includes LRU eviction
|
|
and TTL-based cleanup to prevent memory leaks.
|
|
"""
|
|
|
|
def __init__(self, max_sessions: int = 20, default_ttl_seconds: int = 300):
|
|
"""Initialize instance storage.
|
|
|
|
Args:
|
|
max_sessions: Maximum number of sessions to keep (LRU eviction after this)
|
|
default_ttl_seconds: Default TTL for sessions in seconds (5 minutes)
|
|
"""
|
|
self._sessions: dict[str, "BashSession"] = {}
|
|
self._last_access: dict[str, float] = {}
|
|
self._access_order: list[str] = [] # Track access order for LRU
|
|
self.max_sessions = max_sessions
|
|
self.default_ttl_seconds = default_ttl_seconds
|
|
|
|
def _update_access_order(self, session_id: str) -> None:
|
|
"""Update the access order for LRU tracking.
|
|
|
|
Args:
|
|
session_id: The session that was accessed
|
|
"""
|
|
# Remove from current position if exists
|
|
if session_id in self._access_order:
|
|
self._access_order.remove(session_id)
|
|
# Add to end (most recently used)
|
|
self._access_order.append(session_id)
|
|
|
|
def _evict_lru_if_needed(self) -> None:
|
|
"""Evict least recently used sessions if over capacity."""
|
|
# More aggressive eviction: start evicting when we reach 80% capacity
|
|
eviction_threshold = max(1, int(self.max_sessions * 0.8))
|
|
while len(self._sessions) >= eviction_threshold and self._access_order:
|
|
# Get least recently used session (first in list)
|
|
lru_session_id = self._access_order[0]
|
|
self.remove_session(lru_session_id)
|
|
|
|
def get_session(self, session_id: str) -> "BashSession | None":
|
|
"""Get a session by ID.
|
|
|
|
Args:
|
|
session_id: The session identifier
|
|
|
|
Returns:
|
|
The session if found, None otherwise
|
|
"""
|
|
session = self._sessions.get(session_id)
|
|
if session:
|
|
current_time = time.time()
|
|
self._last_access[session_id] = current_time
|
|
self._update_access_order(session_id)
|
|
|
|
# Check if session has expired
|
|
session_age = current_time - self._last_access.get(session_id, current_time)
|
|
if session_age > self.default_ttl_seconds:
|
|
self.remove_session(session_id)
|
|
return None
|
|
|
|
return session
|
|
|
|
def set_session(self, session_id: str, session: "BashSession") -> None:
|
|
"""Store a session.
|
|
|
|
Args:
|
|
session_id: The session identifier
|
|
session: The session to store
|
|
"""
|
|
current_time = time.time()
|
|
|
|
# If session already exists, update it
|
|
if session_id in self._sessions:
|
|
self._sessions[session_id] = session
|
|
self._last_access[session_id] = current_time
|
|
self._update_access_order(session_id)
|
|
else:
|
|
# New session - check if we need to evict first
|
|
self._evict_lru_if_needed()
|
|
|
|
# Add new session
|
|
self._sessions[session_id] = session
|
|
self._last_access[session_id] = current_time
|
|
self._update_access_order(session_id)
|
|
|
|
def remove_session(self, session_id: str) -> bool:
|
|
"""Remove a session from storage.
|
|
|
|
Args:
|
|
session_id: The session identifier
|
|
|
|
Returns:
|
|
True if session was removed, False if not found
|
|
"""
|
|
session = self._sessions.pop(session_id, None)
|
|
self._last_access.pop(session_id, None)
|
|
|
|
# Remove from access order tracking
|
|
if session_id in self._access_order:
|
|
self._access_order.remove(session_id)
|
|
|
|
if session:
|
|
# Clean up the session resources
|
|
try:
|
|
session.close()
|
|
except Exception:
|
|
pass # Ignore cleanup errors
|
|
return True
|
|
return False
|
|
|
|
def get_session_count(self) -> int:
|
|
"""Get the number of active sessions.
|
|
|
|
Returns:
|
|
Number of active sessions
|
|
"""
|
|
return len(self._sessions)
|
|
|
|
def get_all_session_ids(self) -> list[str]:
|
|
"""Get all active session IDs.
|
|
|
|
Returns:
|
|
List of active session IDs
|
|
"""
|
|
return list(self._sessions.keys())
|
|
|
|
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.
|
|
If None, uses instance default TTL.
|
|
|
|
Returns:
|
|
Number of sessions cleaned up
|
|
"""
|
|
max_age = (
|
|
max_age_seconds if max_age_seconds is not None else self.default_ttl_seconds
|
|
)
|
|
current_time = time.time()
|
|
expired_sessions: list[str] = []
|
|
|
|
for session_id, last_access in self._last_access.items():
|
|
if current_time - last_access > max_age:
|
|
expired_sessions.append(session_id)
|
|
|
|
cleaned_count = 0
|
|
for session_id in expired_sessions:
|
|
if self.remove_session(session_id):
|
|
cleaned_count += 1
|
|
|
|
return cleaned_count
|
|
|
|
def clear_all_sessions(self) -> int:
|
|
"""Clear all sessions.
|
|
|
|
Returns:
|
|
Number of sessions cleared
|
|
"""
|
|
session_ids = list(self._sessions.keys())
|
|
cleared_count = 0
|
|
|
|
for session_id in session_ids:
|
|
if self.remove_session(session_id):
|
|
cleared_count += 1
|
|
|
|
return cleared_count
|
|
|
|
def get_lru_session_ids(self) -> list[str]:
|
|
"""Get session IDs in LRU order (least recently used first).
|
|
|
|
Returns:
|
|
List of session IDs in LRU order
|
|
"""
|
|
return self._access_order.copy()
|
|
|
|
def get_session_stats(self) -> dict:
|
|
"""Get storage statistics.
|
|
|
|
Returns:
|
|
Dictionary with storage statistics
|
|
"""
|
|
return {
|
|
"total_sessions": len(self._sessions),
|
|
"max_sessions": self.max_sessions,
|
|
"utilization": len(self._sessions) / self.max_sessions
|
|
if self.max_sessions > 0
|
|
else 0,
|
|
"default_ttl_seconds": self.default_ttl_seconds,
|
|
}
|
|
|
|
|
|
class SessionStorage:
|
|
"""Global class-based storage for shell command sessions.
|
|
|
|
This class maintains backward compatibility while providing the same
|
|
interface as SessionStorageInstance for global session management.
|
|
"""
|
|
|
|
_sessions: dict[str, "BashSession"] = {}
|
|
_last_access: dict[str, float] = {}
|
|
|
|
@classmethod
|
|
def get_session(cls, session_id: str) -> "BashSession | None":
|
|
"""Get a session by ID.
|
|
|
|
Args:
|
|
session_id: The session identifier
|
|
|
|
Returns:
|
|
The session if found, None otherwise
|
|
"""
|
|
session = cls._sessions.get(session_id)
|
|
if session:
|
|
cls._last_access[session_id] = time.time()
|
|
return session
|
|
|
|
@classmethod
|
|
def set_session(cls, session_id: str, session: "BashSession") -> None:
|
|
"""Store a session.
|
|
|
|
Args:
|
|
session_id: The session identifier
|
|
session: The session to store
|
|
"""
|
|
cls._sessions[session_id] = session
|
|
cls._last_access[session_id] = time.time()
|
|
|
|
@classmethod
|
|
def remove_session(cls, session_id: str) -> bool:
|
|
"""Remove a session from storage.
|
|
|
|
Args:
|
|
session_id: The session identifier
|
|
|
|
Returns:
|
|
True if session was removed, False if not found
|
|
"""
|
|
session = cls._sessions.pop(session_id, None)
|
|
cls._last_access.pop(session_id, None)
|
|
|
|
if session:
|
|
# Clean up the session resources
|
|
try:
|
|
session.close()
|
|
except Exception:
|
|
pass # Ignore cleanup errors
|
|
return True
|
|
return False
|
|
|
|
@classmethod
|
|
def get_session_count(cls) -> int:
|
|
"""Get the number of active sessions.
|
|
|
|
Returns:
|
|
Number of active sessions
|
|
"""
|
|
return len(cls._sessions)
|
|
|
|
@classmethod
|
|
def get_all_session_ids(cls) -> list[str]:
|
|
"""Get all active session IDs.
|
|
|
|
Returns:
|
|
List of active session IDs
|
|
"""
|
|
return list(cls._sessions.keys())
|
|
|
|
@classmethod
|
|
def cleanup_expired_sessions(cls, max_age_seconds: int = 300) -> int:
|
|
"""Clean up sessions that haven't been accessed recently.
|
|
|
|
Args:
|
|
max_age_seconds: Maximum age in seconds before cleanup (default: 5 minutes)
|
|
|
|
Returns:
|
|
Number of sessions cleaned up
|
|
"""
|
|
current_time = time.time()
|
|
expired_sessions: list[str] = []
|
|
|
|
for session_id, last_access in cls._last_access.items():
|
|
if current_time - last_access > max_age_seconds:
|
|
expired_sessions.append(session_id)
|
|
|
|
cleaned_count = 0
|
|
for session_id in expired_sessions:
|
|
if cls.remove_session(session_id):
|
|
cleaned_count += 1
|
|
|
|
return cleaned_count
|
|
|
|
@classmethod
|
|
def clear_all_sessions(cls) -> int:
|
|
"""Clear all sessions.
|
|
|
|
Returns:
|
|
Number of sessions cleared
|
|
"""
|
|
session_ids = list(cls._sessions.keys())
|
|
cleared_count = 0
|
|
|
|
for session_id in session_ids:
|
|
if cls.remove_session(session_id):
|
|
cleared_count += 1
|
|
|
|
return cleared_count
|