|
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
|