|
import threading
|
|
import queue
|
|
import time
|
|
import sys
|
|
import subprocess
|
|
import signal
|
|
import os
|
|
from pr.ui import Colors
|
|
from collections import defaultdict
|
|
from pr.tools.process_handlers import get_handler_for_process, detect_process_type
|
|
from pr.tools.prompt_detection import get_global_detector
|
|
|
|
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()
|
|
self.metadata = {
|
|
'start_time': time.time(),
|
|
'last_activity': time.time(),
|
|
'interaction_count': 0,
|
|
'process_type': 'unknown',
|
|
'state': 'active'
|
|
}
|
|
self.handler = None
|
|
self.prompt_detector = get_global_detector()
|
|
|
|
if self.show_output:
|
|
self.display_thread = threading.Thread(target=self._display_worker, daemon=True)
|
|
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:
|
|
sys.stderr.write(f"{Colors.YELLOW}[{self.name} err]{Colors.RESET} {line}")
|
|
sys.stderr.flush()
|
|
except queue.Empty:
|
|
pass
|
|
|
|
def write_stdout(self, data):
|
|
with self.lock:
|
|
self.stdout_buffer.append(data)
|
|
self.metadata['last_activity'] = time.time()
|
|
# Update handler state if available
|
|
if self.handler:
|
|
self.handler.update_state(data)
|
|
# Update prompt detector
|
|
self.prompt_detector.update_session_state(self.name, data, self.metadata['process_type'])
|
|
if self.show_output:
|
|
self.stdout_queue.put(data)
|
|
|
|
def write_stderr(self, data):
|
|
with self.lock:
|
|
self.stderr_buffer.append(data)
|
|
self.metadata['last_activity'] = time.time()
|
|
# Update handler state if available
|
|
if self.handler:
|
|
self.handler.update_state(data)
|
|
# Update prompt detector
|
|
self.prompt_detector.update_session_state(self.name, data, self.metadata['process_type'])
|
|
if self.show_output:
|
|
self.stderr_queue.put(data)
|
|
|
|
def get_stdout(self):
|
|
with self.lock:
|
|
return ''.join(self.stdout_buffer)
|
|
|
|
def get_stderr(self):
|
|
with self.lock:
|
|
return ''.join(self.stderr_buffer)
|
|
|
|
def get_all_output(self):
|
|
with self.lock:
|
|
return {
|
|
'stdout': ''.join(self.stdout_buffer),
|
|
'stderr': ''.join(self.stderr_buffer)
|
|
}
|
|
|
|
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:
|
|
self.metadata['process_type'] = process_type
|
|
self.handler = get_handler_for_process(process_type, self)
|
|
|
|
def send_input(self, input_data):
|
|
if hasattr(self, 'process') and self.process.poll() is None:
|
|
try:
|
|
self.process.stdin.write(input_data + '\n')
|
|
self.process.stdin.flush()
|
|
with self.lock:
|
|
self.metadata['last_activity'] = time.time()
|
|
self.metadata['interaction_count'] += 1
|
|
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:
|
|
self.metadata['last_activity'] = time.time()
|
|
self.metadata['interaction_count'] += 1
|
|
|
|
def close(self):
|
|
self.active = False
|
|
if hasattr(self, 'display_thread'):
|
|
self.display_thread.join(timeout=1)
|
|
|
|
_multiplexers = {}
|
|
_mux_counter = 0
|
|
_mux_lock = threading.Lock()
|
|
_background_monitor = None
|
|
_monitor_active = False
|
|
_monitor_interval = 0.2 # 200ms
|
|
|
|
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
|
|
|
|
def get_multiplexer(name):
|
|
return _multiplexers.get(name)
|
|
|
|
def close_multiplexer(name):
|
|
mux = _multiplexers.get(name)
|
|
if mux:
|
|
mux.close()
|
|
del _multiplexers[name]
|
|
|
|
def get_all_multiplexer_states():
|
|
with _mux_lock:
|
|
states = {}
|
|
for name, mux in _multiplexers.items():
|
|
states[name] = {
|
|
'metadata': mux.get_metadata(),
|
|
'output_summary': {
|
|
'stdout_lines': len(mux.stdout_buffer),
|
|
'stderr_lines': len(mux.stderr_buffer)
|
|
}
|
|
}
|
|
return states
|
|
|
|
def cleanup_all_multiplexers():
|
|
for mux in list(_multiplexers.values()):
|
|
mux.close()
|
|
_multiplexers.clear()
|
|
|
|
# Background process management
|
|
_background_processes = {}
|
|
_process_lock = threading.Lock()
|
|
|
|
class BackgroundProcess:
|
|
def __init__(self, name, command):
|
|
self.name = name
|
|
self.command = command
|
|
self.process = None
|
|
self.multiplexer = None
|
|
self.status = 'starting'
|
|
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,
|
|
universal_newlines=True
|
|
)
|
|
|
|
self.status = 'running'
|
|
|
|
# Start output monitoring threads
|
|
threading.Thread(target=self._monitor_stdout, daemon=True).start()
|
|
threading.Thread(target=self._monitor_stderr, daemon=True).start()
|
|
|
|
return {'status': 'success', 'pid': self.process.pid}
|
|
|
|
except Exception as e:
|
|
self.status = 'error'
|
|
return {'status': 'error', 'error': str(e)}
|
|
|
|
def _monitor_stdout(self):
|
|
"""Monitor stdout from the process."""
|
|
try:
|
|
for line in iter(self.process.stdout.readline, ''):
|
|
if line:
|
|
self.multiplexer.write_stdout(line.rstrip('\n\r'))
|
|
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:
|
|
for line in iter(self.process.stderr.readline, ''):
|
|
if line:
|
|
self.multiplexer.write_stderr(line.rstrip('\n\r'))
|
|
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:
|
|
self.status = 'completed'
|
|
self.end_time = time.time()
|
|
|
|
def get_info(self):
|
|
"""Get process information."""
|
|
self._check_completion()
|
|
return {
|
|
'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
|
|
}
|
|
|
|
def get_output(self, lines=None):
|
|
"""Get process output."""
|
|
if not self.multiplexer:
|
|
return []
|
|
|
|
all_output = self.multiplexer.get_all_output()
|
|
stdout_lines = all_output['stdout'].split('\n') if all_output['stdout'] else []
|
|
stderr_lines = all_output['stderr'].split('\n') if all_output['stderr'] else []
|
|
|
|
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."""
|
|
if self.process and self.status == 'running':
|
|
try:
|
|
self.process.stdin.write(input_text + '\n')
|
|
self.process.stdin.flush()
|
|
return {'status': 'success'}
|
|
except Exception as e:
|
|
return {'status': 'error', 'error': str(e)}
|
|
return {'status': 'error', 'error': 'Process not running or no stdin'}
|
|
|
|
def kill(self):
|
|
"""Kill the process."""
|
|
if self.process and self.status == 'running':
|
|
try:
|
|
self.process.terminate()
|
|
# Wait a bit for graceful termination
|
|
time.sleep(0.1)
|
|
if self.process.poll() is None:
|
|
self.process.kill()
|
|
self.status = 'killed'
|
|
self.end_time = time.time()
|
|
return {'status': 'success'}
|
|
except Exception as e:
|
|
return {'status': 'error', 'error': str(e)}
|
|
return {'status': 'error', 'error': 'Process not running'}
|
|
|
|
def start_background_process(name, command):
|
|
"""Start a background process."""
|
|
with _process_lock:
|
|
if name in _background_processes:
|
|
return {'status': 'error', 'error': f'Process {name} already exists'}
|
|
|
|
process = BackgroundProcess(name, command)
|
|
result = process.start()
|
|
|
|
if result['status'] == 'success':
|
|
_background_processes[name] = process
|
|
|
|
return result
|
|
|
|
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
|
|
|
|
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
|
|
|
|
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
|
|
|
|
def send_input_to_session(name, input_text):
|
|
"""Send input to a background session."""
|
|
with _process_lock:
|
|
process = _background_processes.get(name)
|
|
return process.send_input(input_text) if process else {'status': 'error', 'error': 'Session not found'}
|
|
|
|
def kill_session(name):
|
|
"""Kill a background session."""
|
|
with _process_lock:
|
|
process = _background_processes.get(name)
|
|
if process:
|
|
result = process.kill()
|
|
if result['status'] == 'success':
|
|
del _background_processes[name]
|
|
return result
|
|
return {'status': 'error', 'error': 'Session not found'}
|