383 lines
12 KiB
Python
Raw Normal View History

2025-11-04 05:17:27 +01:00
import queue
2025-11-04 07:52:36 +01:00
import subprocess
2025-11-04 08:09:12 +01:00
import sys
import threading
import time
from pr.tools.process_handlers import detect_process_type, get_handler_for_process
2025-11-04 07:52:36 +01:00
from pr.tools.prompt_detection import get_global_detector
2025-11-04 08:09:12 +01:00
from pr.ui import Colors
2025-11-04 05:17:27 +01:00
class TerminalMultiplexer:
def __init__(self, name, show_output=True):
self.name = name
self.show_output = show_output
self.stdout_buffer = []
self.stderr_buffer = []
self.stdout_queue = queue.Queue()
self.stderr_queue = queue.Queue()
self.active = True
self.lock = threading.Lock()
2025-11-04 07:52:36 +01:00
self.metadata = {
2025-11-04 08:09:12 +01:00
"start_time": time.time(),
"last_activity": time.time(),
"interaction_count": 0,
"process_type": "unknown",
"state": "active",
2025-11-04 07:52:36 +01:00
}
self.handler = None
self.prompt_detector = get_global_detector()
2025-11-04 05:17:27 +01:00
if self.show_output:
2025-11-04 08:09:12 +01:00
self.display_thread = threading.Thread(
target=self._display_worker, daemon=True
)
2025-11-04 05:17:27 +01:00
self.display_thread.start()
def _display_worker(self):
while self.active:
try:
line = self.stdout_queue.get(timeout=0.1)
if line:
sys.stdout.write(f"{Colors.GRAY}[{self.name}]{Colors.RESET} {line}")
sys.stdout.flush()
except queue.Empty:
pass
try:
line = self.stderr_queue.get(timeout=0.1)
if line:
2025-11-04 08:09:12 +01:00
sys.stderr.write(
f"{Colors.YELLOW}[{self.name} err]{Colors.RESET} {line}"
)
2025-11-04 05:17:27 +01:00
sys.stderr.flush()
except queue.Empty:
pass
def write_stdout(self, data):
with self.lock:
self.stdout_buffer.append(data)
2025-11-04 08:09:12 +01:00
self.metadata["last_activity"] = time.time()
2025-11-04 07:52:36 +01:00
# Update handler state if available
if self.handler:
self.handler.update_state(data)
# Update prompt detector
2025-11-04 08:09:12 +01:00
self.prompt_detector.update_session_state(
self.name, data, self.metadata["process_type"]
)
2025-11-04 05:17:27 +01:00
if self.show_output:
self.stdout_queue.put(data)
def write_stderr(self, data):
with self.lock:
self.stderr_buffer.append(data)
2025-11-04 08:09:12 +01:00
self.metadata["last_activity"] = time.time()
2025-11-04 07:52:36 +01:00
# Update handler state if available
if self.handler:
self.handler.update_state(data)
# Update prompt detector
2025-11-04 08:09:12 +01:00
self.prompt_detector.update_session_state(
self.name, data, self.metadata["process_type"]
)
2025-11-04 05:17:27 +01:00
if self.show_output:
self.stderr_queue.put(data)
def get_stdout(self):
with self.lock:
2025-11-04 08:09:12 +01:00
return "".join(self.stdout_buffer)
2025-11-04 05:17:27 +01:00
def get_stderr(self):
with self.lock:
2025-11-04 08:09:12 +01:00
return "".join(self.stderr_buffer)
2025-11-04 05:17:27 +01:00
def get_all_output(self):
with self.lock:
return {
2025-11-04 08:09:12 +01:00
"stdout": "".join(self.stdout_buffer),
"stderr": "".join(self.stderr_buffer),
2025-11-04 05:17:27 +01:00
}
2025-11-04 07:52:36 +01:00
def get_metadata(self):
with self.lock:
return self.metadata.copy()
def update_metadata(self, key, value):
with self.lock:
self.metadata[key] = value
def set_process_type(self, process_type):
"""Set the process type and initialize appropriate handler."""
with self.lock:
2025-11-04 08:09:12 +01:00
self.metadata["process_type"] = process_type
2025-11-04 07:52:36 +01:00
self.handler = get_handler_for_process(process_type, self)
def send_input(self, input_data):
2025-11-04 08:09:12 +01:00
if hasattr(self, "process") and self.process.poll() is None:
2025-11-04 07:52:36 +01:00
try:
2025-11-04 08:09:12 +01:00
self.process.stdin.write(input_data + "\n")
2025-11-04 07:52:36 +01:00
self.process.stdin.flush()
with self.lock:
2025-11-04 08:09:12 +01:00
self.metadata["last_activity"] = time.time()
self.metadata["interaction_count"] += 1
2025-11-04 07:52:36 +01:00
except Exception as e:
self.write_stderr(f"Error sending input: {e}")
else:
# This will be implemented when we have a process attached
# For now, just update activity
with self.lock:
2025-11-04 08:09:12 +01:00
self.metadata["last_activity"] = time.time()
self.metadata["interaction_count"] += 1
2025-11-04 07:52:36 +01:00
2025-11-04 05:17:27 +01:00
def close(self):
self.active = False
2025-11-04 08:09:12 +01:00
if hasattr(self, "display_thread"):
2025-11-04 05:17:27 +01:00
self.display_thread.join(timeout=1)
2025-11-04 08:09:12 +01:00
2025-11-04 05:17:27 +01:00
_multiplexers = {}
_mux_counter = 0
_mux_lock = threading.Lock()
2025-11-04 07:52:36 +01:00
_background_monitor = None
_monitor_active = False
_monitor_interval = 0.2 # 200ms
2025-11-04 05:17:27 +01:00
2025-11-04 08:09:12 +01:00
2025-11-04 05:17:27 +01:00
def create_multiplexer(name=None, show_output=True):
global _mux_counter
with _mux_lock:
if name is None:
_mux_counter += 1
name = f"process-{_mux_counter}"
mux = TerminalMultiplexer(name, show_output)
_multiplexers[name] = mux
return name, mux
2025-11-04 08:09:12 +01:00
2025-11-04 05:17:27 +01:00
def get_multiplexer(name):
return _multiplexers.get(name)
2025-11-04 08:09:12 +01:00
2025-11-04 05:17:27 +01:00
def close_multiplexer(name):
mux = _multiplexers.get(name)
if mux:
mux.close()
del _multiplexers[name]
2025-11-04 08:09:12 +01:00
2025-11-04 07:52:36 +01:00
def get_all_multiplexer_states():
with _mux_lock:
states = {}
for name, mux in _multiplexers.items():
states[name] = {
2025-11-04 08:09:12 +01:00
"metadata": mux.get_metadata(),
"output_summary": {
"stdout_lines": len(mux.stdout_buffer),
"stderr_lines": len(mux.stderr_buffer),
},
2025-11-04 07:52:36 +01:00
}
return states
2025-11-04 08:09:12 +01:00
2025-11-04 05:17:27 +01:00
def cleanup_all_multiplexers():
for mux in list(_multiplexers.values()):
mux.close()
_multiplexers.clear()
2025-11-04 07:52:36 +01:00
2025-11-04 08:09:12 +01:00
2025-11-04 07:52:36 +01:00
# Background process management
_background_processes = {}
_process_lock = threading.Lock()
2025-11-04 08:09:12 +01:00
2025-11-04 07:52:36 +01:00
class BackgroundProcess:
def __init__(self, name, command):
self.name = name
self.command = command
self.process = None
self.multiplexer = None
2025-11-04 08:09:12 +01:00
self.status = "starting"
2025-11-04 07:52:36 +01:00
self.start_time = time.time()
self.end_time = None
def start(self):
"""Start the background process."""
try:
# Create multiplexer for this process
mux_name, mux = create_multiplexer(self.name, show_output=False)
self.multiplexer = mux
# Detect process type
process_type = detect_process_type(self.command)
mux.set_process_type(process_type)
# Start the subprocess
self.process = subprocess.Popen(
self.command,
shell=True,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
2025-11-04 08:09:12 +01:00
universal_newlines=True,
2025-11-04 07:52:36 +01:00
)
2025-11-04 08:09:12 +01:00
self.status = "running"
2025-11-04 07:52:36 +01:00
# Start output monitoring threads
threading.Thread(target=self._monitor_stdout, daemon=True).start()
threading.Thread(target=self._monitor_stderr, daemon=True).start()
2025-11-04 08:09:12 +01:00
return {"status": "success", "pid": self.process.pid}
2025-11-04 07:52:36 +01:00
except Exception as e:
2025-11-04 08:09:12 +01:00
self.status = "error"
return {"status": "error", "error": str(e)}
2025-11-04 07:52:36 +01:00
def _monitor_stdout(self):
"""Monitor stdout from the process."""
try:
2025-11-04 08:09:12 +01:00
for line in iter(self.process.stdout.readline, ""):
2025-11-04 07:52:36 +01:00
if line:
2025-11-04 08:09:12 +01:00
self.multiplexer.write_stdout(line.rstrip("\n\r"))
2025-11-04 07:52:36 +01:00
except Exception as e:
self.write_stderr(f"Error reading stdout: {e}")
finally:
self._check_completion()
def _monitor_stderr(self):
"""Monitor stderr from the process."""
try:
2025-11-04 08:09:12 +01:00
for line in iter(self.process.stderr.readline, ""):
2025-11-04 07:52:36 +01:00
if line:
2025-11-04 08:09:12 +01:00
self.multiplexer.write_stderr(line.rstrip("\n\r"))
2025-11-04 07:52:36 +01:00
except Exception as e:
self.write_stderr(f"Error reading stderr: {e}")
def _check_completion(self):
"""Check if process has completed."""
if self.process and self.process.poll() is not None:
2025-11-04 08:09:12 +01:00
self.status = "completed"
2025-11-04 07:52:36 +01:00
self.end_time = time.time()
def get_info(self):
"""Get process information."""
self._check_completion()
return {
2025-11-04 08:09:12 +01:00
"name": self.name,
"command": self.command,
"status": self.status,
"pid": self.process.pid if self.process else None,
"start_time": self.start_time,
"end_time": self.end_time,
"runtime": (
time.time() - self.start_time
if not self.end_time
else self.end_time - self.start_time
),
2025-11-04 07:52:36 +01:00
}
def get_output(self, lines=None):
"""Get process output."""
if not self.multiplexer:
return []
all_output = self.multiplexer.get_all_output()
2025-11-04 08:09:12 +01:00
stdout_lines = all_output["stdout"].split("\n") if all_output["stdout"] else []
stderr_lines = all_output["stderr"].split("\n") if all_output["stderr"] else []
2025-11-04 07:52:36 +01:00
combined = stdout_lines + stderr_lines
if lines:
combined = combined[-lines:]
return [line for line in combined if line.strip()]
def send_input(self, input_text):
"""Send input to the process."""
2025-11-04 08:09:12 +01:00
if self.process and self.status == "running":
2025-11-04 07:52:36 +01:00
try:
2025-11-04 08:09:12 +01:00
self.process.stdin.write(input_text + "\n")
2025-11-04 07:52:36 +01:00
self.process.stdin.flush()
2025-11-04 08:09:12 +01:00
return {"status": "success"}
2025-11-04 07:52:36 +01:00
except Exception as e:
2025-11-04 08:09:12 +01:00
return {"status": "error", "error": str(e)}
return {"status": "error", "error": "Process not running or no stdin"}
2025-11-04 07:52:36 +01:00
def kill(self):
"""Kill the process."""
2025-11-04 08:09:12 +01:00
if self.process and self.status == "running":
2025-11-04 07:52:36 +01:00
try:
self.process.terminate()
# Wait a bit for graceful termination
time.sleep(0.1)
if self.process.poll() is None:
self.process.kill()
2025-11-04 08:09:12 +01:00
self.status = "killed"
2025-11-04 07:52:36 +01:00
self.end_time = time.time()
2025-11-04 08:09:12 +01:00
return {"status": "success"}
2025-11-04 07:52:36 +01:00
except Exception as e:
2025-11-04 08:09:12 +01:00
return {"status": "error", "error": str(e)}
return {"status": "error", "error": "Process not running"}
2025-11-04 07:52:36 +01:00
def start_background_process(name, command):
"""Start a background process."""
with _process_lock:
if name in _background_processes:
2025-11-04 08:09:12 +01:00
return {"status": "error", "error": f"Process {name} already exists"}
2025-11-04 07:52:36 +01:00
process = BackgroundProcess(name, command)
result = process.start()
2025-11-04 08:09:12 +01:00
if result["status"] == "success":
2025-11-04 07:52:36 +01:00
_background_processes[name] = process
return result
2025-11-04 08:09:12 +01:00
2025-11-04 07:52:36 +01:00
def get_all_sessions():
"""Get all background process sessions."""
with _process_lock:
sessions = {}
for name, process in _background_processes.items():
sessions[name] = process.get_info()
return sessions
2025-11-04 08:09:12 +01:00
2025-11-04 07:52:36 +01:00
def get_session_info(name):
"""Get information about a specific session."""
with _process_lock:
process = _background_processes.get(name)
return process.get_info() if process else None
2025-11-04 08:09:12 +01:00
2025-11-04 07:52:36 +01:00
def get_session_output(name, lines=None):
"""Get output from a specific session."""
with _process_lock:
process = _background_processes.get(name)
return process.get_output(lines) if process else None
2025-11-04 08:09:12 +01:00
2025-11-04 07:52:36 +01:00
def send_input_to_session(name, input_text):
"""Send input to a background session."""
with _process_lock:
process = _background_processes.get(name)
2025-11-04 08:09:12 +01:00
return (
process.send_input(input_text)
if process
else {"status": "error", "error": "Session not found"}
)
2025-11-04 07:52:36 +01:00
def kill_session(name):
"""Kill a background session."""
with _process_lock:
process = _background_processes.get(name)
if process:
result = process.kill()
2025-11-04 08:09:12 +01:00
if result["status"] == "success":
2025-11-04 07:52:36 +01:00
del _background_processes[name]
return result
2025-11-04 08:09:12 +01:00
return {"status": "error", "error": "Session not found"}