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