import threading
import time
import queue
from pr.multiplexer import get_all_multiplexer_states, get_multiplexer
from pr.tools.interactive_control import get_session_status
class BackgroundMonitor:
def __init__(self, check_interval=5.0):
self.check_interval = check_interval
self.active = False
self.monitor_thread = None
self.event_queue = queue.Queue()
self.last_states = {}
self.event_callbacks = []
def start(self):
"""Start the background monitoring thread."""
if self.monitor_thread is None:
self.active = True
self.monitor_thread = threading.Thread(target=self._monitor_loop, daemon=True)
self.monitor_thread.start()
def stop(self):
"""Stop the background monitoring thread."""
self.active = False
if self.monitor_thread:
self.monitor_thread.join(timeout=2)
def add_event_callback(self, callback):
"""Add a callback function to be called when events are detected."""
self.event_callbacks.append(callback)
def remove_event_callback(self, callback):
"""Remove an event callback."""
if callback in self.event_callbacks:
self.event_callbacks.remove(callback)
def get_pending_events(self):
"""Get all pending events from the queue."""
events = []
while not self.event_queue.empty():
try:
events.append(self.event_queue.get_nowait())
except queue.Empty:
break
return events
def _monitor_loop(self):
"""Main monitoring loop that checks for multiplexer activity."""
while self.active:
try:
current_states = get_all_multiplexer_states()
# Detect changes and events
events = self._detect_events(self.last_states, current_states)
# Queue events for processing
for event in events:
self.event_queue.put(event)
# Also call callbacks immediately
for callback in self.event_callbacks:
try:
callback(event)
except Exception as e:
print(f"Error in event callback: {e}")
self.last_states = current_states.copy()
time.sleep(self.check_interval)
except Exception as e:
print(f"Error in background monitor loop: {e}")
time.sleep(self.check_interval)
def _detect_events(self, old_states, new_states):
"""Detect significant events in multiplexer states."""
events = []
# Check for new sessions
for session_name in new_states:
if session_name not in old_states:
events.append({
'type': 'session_started',
'session_name': session_name,
'metadata': new_states[session_name]['metadata']
})
# Check for ended sessions
for session_name in old_states:
if session_name not in new_states:
events.append({
'type': 'session_ended',
'session_name': session_name
})
# Check for activity in existing sessions
for session_name, new_state in new_states.items():
if session_name in old_states:
old_state = old_states[session_name]
# Check for output changes
old_stdout_lines = old_state['output_summary']['stdout_lines']
new_stdout_lines = new_state['output_summary']['stdout_lines']
old_stderr_lines = old_state['output_summary']['stderr_lines']
new_stderr_lines = new_state['output_summary']['stderr_lines']
if new_stdout_lines > old_stdout_lines or new_stderr_lines > old_stderr_lines:
# Get the new output
mux = get_multiplexer(session_name)
if mux:
all_output = mux.get_all_output()
new_output = {
'stdout': all_output['stdout'].split('\n')[old_stdout_lines:],
'stderr': all_output['stderr'].split('\n')[old_stderr_lines:]
}
events.append({
'type': 'output_received',
'session_name': session_name,
'new_output': new_output,
'total_lines': {
'stdout': new_stdout_lines,
'stderr': new_stderr_lines
}
})
# Check for state changes
old_metadata = old_state['metadata']
new_metadata = new_state['metadata']
if old_metadata.get('state') != new_metadata.get('state'):
events.append({
'type': 'state_changed',
'session_name': session_name,
'old_state': old_metadata.get('state'),
'new_state': new_metadata.get('state')
})
# Check for process type identification
if (old_metadata.get('process_type') == 'unknown' and
new_metadata.get('process_type') != 'unknown'):
events.append({
'type': 'process_identified',
'session_name': session_name,
'process_type': new_metadata.get('process_type')
})
# Check for sessions needing attention (based on heuristics)
for session_name, state in new_states.items():
metadata = state['metadata']
output_summary = state['output_summary']
# Heuristic: High output volume might indicate completion or error
total_lines = output_summary['stdout_lines'] + output_summary['stderr_lines']
if total_lines > 100: # Arbitrary threshold
events.append({
'type': 'high_output_volume',
'session_name': session_name,
'total_lines': total_lines
})
# Heuristic: Long-running session without recent activity
time_since_activity = time.time() - metadata.get('last_activity', 0)
if time_since_activity > 300: # 5 minutes
events.append({
'type': 'inactive_session',
'session_name': session_name,
'inactive_seconds': time_since_activity
})
# Heuristic: Sessions that might be waiting for input
# This would be enhanced with prompt detection in later phases
if self._might_be_waiting_for_input(session_name, state):
events.append({
'type': 'possible_input_needed',
'session_name': session_name
})
return events
def _might_be_waiting_for_input(self, session_name, state):
"""Heuristic to detect if a session might be waiting for input."""
metadata = state['metadata']
process_type = metadata.get('process_type', 'unknown')
# Simple heuristics based on process type and recent activity
time_since_activity = time.time() - metadata.get('last_activity', 0)
# If it's been more than 10 seconds since last activity, might be waiting
if time_since_activity > 10:
return True
return False
# Global monitor instance
_global_monitor = None
def get_global_monitor():
"""Get the global background monitor instance."""
global _global_monitor
if _global_monitor is None:
_global_monitor = BackgroundMonitor()
return _global_monitor
def start_global_monitor():
"""Start the global background monitor."""
monitor = get_global_monitor()
monitor.start()
def stop_global_monitor():
"""Stop the global background monitor."""
global _global_monitor
if _global_monitor:
_global_monitor.stop()
# Global monitor instance
_global_monitor = None
def start_global_monitor():
"""Start the global background monitor."""
global _global_monitor
if _global_monitor is None:
_global_monitor = BackgroundMonitor()
_global_monitor.start()
return _global_monitor
def stop_global_monitor():
"""Stop the global background monitor."""
global _global_monitor
if _global_monitor:
_global_monitor.stop()
_global_monitor = None
def get_global_monitor():
"""Get the global background monitor instance."""
global _global_monitor
return _global_monitor