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