import queue import threading import time from pr.multiplexer import get_all_multiplexer_states, get_multiplexer 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"] 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