TradingAgents/.claude/scripts/pipeline_controller.py

238 lines
6.7 KiB
Python

#!/usr/bin/env python3
"""
Pipeline Controller - Manages progress display subprocess lifecycle
Handles starting, stopping, and monitoring the progress display process.
Ensures clean shutdown and prevents multiple concurrent displays.
Features:
- Start display subprocess in background
- Stop display gracefully or forcefully
- Track PID for process management
- Handle signals (SIGTERM, SIGINT)
- Automatic cleanup on exit
- Prevent multiple concurrent displays
Usage:
# Start display
controller = PipelineController(session_file=Path("session.json"))
controller.start_display()
# ... pipeline runs ...
# Stop display
controller.stop_display()
"""
import atexit
import os
import signal
import subprocess
import sys
from pathlib import Path
from typing import Dict, Optional, Any
class PipelineController:
"""Controller for managing progress display subprocess."""
def __init__(self, session_file: Path, pid_dir: Optional[Path] = None):
"""Initialize pipeline controller.
Args:
session_file: Path to JSON session file to display
pid_dir: Directory for PID file (default: temp dir)
"""
self.session_file = session_file
self.display_process: Optional[subprocess.Popen] = None
# Create PID file path
if pid_dir is None:
pid_dir = Path("/tmp")
self.pid_file = pid_dir / f"progress_display_{os.getpid()}.pid"
# Register cleanup on exit
atexit.register(self.cleanup)
def start_display(self, refresh_interval: float = 0.5) -> bool:
"""Start the progress display subprocess.
Args:
refresh_interval: Display refresh interval in seconds
Returns:
True if started successfully, False otherwise
"""
# Check if already running
if self.display_process and self.is_display_running():
return False
try:
# Get path to progress_display.py
script_dir = Path(__file__).parent
display_script = script_dir / "progress_display.py"
if not display_script.exists():
raise FileNotFoundError(f"progress_display.py not found at {display_script}")
# Start subprocess
self.display_process = subprocess.Popen(
[
sys.executable,
str(display_script),
str(self.session_file),
"--refresh",
str(refresh_interval)
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
start_new_session=True # Create new process group
)
# Write PID file
self.pid_file.write_text(str(self.display_process.pid))
return True
except (FileNotFoundError, PermissionError):
# Re-raise so caller can handle
raise
except Exception as e:
# Other errors - log but don't crash
print(f"Error starting display: {e}", file=sys.stderr)
return False
def stop_display(self, timeout: int = 5) -> bool:
"""Stop the progress display subprocess.
Args:
timeout: Seconds to wait for graceful shutdown
Returns:
True if stopped successfully, False otherwise
"""
if not self.display_process:
return True
try:
# Try graceful termination first
self.display_process.terminate()
try:
self.display_process.wait(timeout=timeout)
except subprocess.TimeoutExpired:
# Force kill if graceful shutdown failed
self.display_process.kill()
self.display_process.wait()
self.display_process = None
# Clean up PID file
if self.pid_file.exists():
self.pid_file.unlink()
return True
except ProcessLookupError:
# Process already gone
self.display_process = None
if self.pid_file.exists():
self.pid_file.unlink()
return True
except Exception as e:
print(f"Error stopping display: {e}", file=sys.stderr)
return False
def is_display_running(self) -> bool:
"""Check if display process is still running.
Returns:
True if running, False otherwise
"""
if not self.display_process:
return False
# Check if process has exited
return_code = self.display_process.poll()
return return_code is None
def get_status(self) -> Dict[str, Any]:
"""Get display process status information.
Returns:
Dictionary with status info
"""
if not self.display_process:
return {
"running": False,
"pid": None,
"session_file": str(self.session_file)
}
return {
"running": self.is_display_running(),
"pid": self.display_process.pid,
"session_file": str(self.session_file),
"pid_file": str(self.pid_file)
}
def cleanup(self):
"""Cleanup on exit - stop display process."""
if self.display_process and self.is_display_running():
self.stop_display()
def handle_signal(self, signum, frame):
"""Handle termination signals.
Args:
signum: Signal number
frame: Current stack frame
"""
self.cleanup()
sys.exit(0)
def main():
"""Main entry point for CLI usage."""
if len(sys.argv) < 2:
print("Usage: pipeline_controller.py <session_file.json>")
print("\nExample:")
print(" pipeline_controller.py docs/sessions/20251104-120000-pipeline.json")
sys.exit(1)
session_file = Path(sys.argv[1])
if not session_file.exists():
print(f"Error: Session file not found: {session_file}")
sys.exit(1)
controller = PipelineController(session_file=session_file)
# Register signal handlers
signal.signal(signal.SIGTERM, controller.handle_signal)
signal.signal(signal.SIGINT, controller.handle_signal)
# Start display
print(f"Starting progress display for {session_file.name}...")
if controller.start_display():
print(f"Display started (PID: {controller.display_process.pid})")
print("Press Ctrl+C to stop")
# Keep running until interrupted
try:
controller.display_process.wait()
except KeyboardInterrupt:
print("\nStopping display...")
controller.stop_display()
else:
print("Failed to start display")
sys.exit(1)
if __name__ == "__main__":
main()