|
import subprocess
|
|
import threading
|
|
import importlib
|
|
|
|
|
|
def _get_multiplexer_functions():
|
|
"""Lazy import multiplexer functions to avoid circular imports."""
|
|
multiplexer = importlib.import_module("pr.multiplexer")
|
|
return {
|
|
"create_multiplexer": multiplexer.create_multiplexer,
|
|
"get_multiplexer": multiplexer.get_multiplexer,
|
|
"close_multiplexer": multiplexer.close_multiplexer,
|
|
"get_all_multiplexer_states": multiplexer.get_all_multiplexer_states,
|
|
}
|
|
|
|
|
|
def start_interactive_session(command, session_name=None, process_type="generic", cwd=None):
|
|
"""
|
|
Start an interactive session in a dedicated multiplexer.
|
|
|
|
Args:
|
|
command: The command to run (list or string)
|
|
session_name: Optional name for the session
|
|
process_type: Type of process (ssh, vim, apt, etc.)
|
|
cwd: Current working directory for the command
|
|
|
|
Returns:
|
|
session_name: The name of the created session
|
|
"""
|
|
funcs = _get_multiplexer_functions()
|
|
name, mux = funcs["create_multiplexer"](session_name)
|
|
mux.update_metadata("process_type", process_type)
|
|
|
|
# Start the process
|
|
if isinstance(command, str):
|
|
command = command.split()
|
|
|
|
try:
|
|
process = subprocess.Popen(
|
|
command,
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
bufsize=1,
|
|
cwd=cwd,
|
|
)
|
|
|
|
mux.process = process
|
|
mux.update_metadata("pid", process.pid)
|
|
|
|
# Set process type and handler
|
|
from pr.tools.process_handlers import detect_process_type
|
|
|
|
detected_type = detect_process_type(command)
|
|
mux.set_process_type(detected_type)
|
|
|
|
# Start output readers
|
|
stdout_thread = threading.Thread(
|
|
target=_read_output, args=(process.stdout, mux.write_stdout, detected_type), daemon=True
|
|
)
|
|
stderr_thread = threading.Thread(
|
|
target=_read_output, args=(process.stderr, mux.write_stderr, detected_type), daemon=True
|
|
)
|
|
|
|
stdout_thread.start()
|
|
stderr_thread.start()
|
|
|
|
mux.stdout_thread = stdout_thread
|
|
mux.stderr_thread = stderr_thread
|
|
|
|
return name
|
|
except Exception as e:
|
|
funcs["close_multiplexer"](name)
|
|
raise e
|
|
|
|
|
|
def _read_output(stream, write_func, process_type):
|
|
"""Read from a stream and write to multiplexer buffer."""
|
|
if process_type in ["vim", "ssh"]:
|
|
try:
|
|
while True:
|
|
char = stream.read(1)
|
|
if not char:
|
|
break
|
|
write_func(char)
|
|
except Exception as e:
|
|
print(f"Error reading output: {e}")
|
|
else:
|
|
try:
|
|
for line in iter(stream.readline, ""):
|
|
if line:
|
|
write_func(line.rstrip("\n"))
|
|
except Exception as e:
|
|
print(f"Error reading output: {e}")
|
|
|
|
|
|
def send_input_to_session(session_name, input_data):
|
|
"""
|
|
Send input to an interactive session.
|
|
|
|
Args:
|
|
session_name: Name of the session
|
|
input_data: Input string to send
|
|
"""
|
|
funcs = _get_multiplexer_functions()
|
|
mux = funcs["get_multiplexer"](session_name)
|
|
if not mux:
|
|
raise ValueError(f"Session {session_name} not found")
|
|
|
|
if not hasattr(mux, "process") or mux.process.poll() is not None:
|
|
raise ValueError(f"Session {session_name} is not active")
|
|
|
|
try:
|
|
mux.process.stdin.write(input_data + "\n")
|
|
mux.process.stdin.flush()
|
|
except Exception as e:
|
|
raise RuntimeError(f"Failed to send input to session {session_name}: {e}")
|
|
|
|
|
|
def read_session_output(session_name, lines=None):
|
|
funcs = _get_multiplexer_functions()
|
|
"""
|
|
Read output from a session.
|
|
|
|
Args:
|
|
session_name: Name of the session
|
|
lines: Number of recent lines to return (None for all)
|
|
|
|
Returns:
|
|
dict: {'stdout': str, 'stderr': str}
|
|
"""
|
|
mux = funcs["get_multiplexer"](session_name)
|
|
if not mux:
|
|
raise ValueError(f"Session {session_name} not found")
|
|
|
|
output = mux.get_all_output()
|
|
if lines is not None:
|
|
# Return last N lines
|
|
stdout_lines = output["stdout"].split("\n")[-lines:] if output["stdout"] else []
|
|
stderr_lines = output["stderr"].split("\n")[-lines:] if output["stderr"] else []
|
|
output = {"stdout": "\n".join(stdout_lines), "stderr": "\n".join(stderr_lines)}
|
|
return output
|
|
|
|
|
|
def list_active_sessions():
|
|
"""
|
|
List all active interactive sessions.
|
|
|
|
Returns:
|
|
dict: Session states
|
|
"""
|
|
funcs = _get_multiplexer_functions()
|
|
return funcs["get_all_multiplexer_states"]()
|
|
|
|
|
|
def get_session_status(session_name):
|
|
"""
|
|
Get detailed status of a session.
|
|
|
|
Args:
|
|
session_name: Name of the session
|
|
|
|
Returns:
|
|
dict: Session metadata and status
|
|
"""
|
|
funcs = _get_multiplexer_functions()
|
|
mux = funcs["get_multiplexer"](session_name)
|
|
if not mux:
|
|
return None
|
|
|
|
status = mux.get_metadata()
|
|
status["is_active"] = hasattr(mux, "process") and mux.process.poll() is None
|
|
if status["is_active"]:
|
|
status["pid"] = mux.process.pid
|
|
status["output_summary"] = {
|
|
"stdout_lines": len(mux.stdout_buffer),
|
|
"stderr_lines": len(mux.stderr_buffer),
|
|
}
|
|
return status
|
|
|
|
|
|
def close_interactive_session(session_name):
|
|
"""
|
|
Close an interactive session.
|
|
"""
|
|
try:
|
|
funcs = _get_multiplexer_functions()
|
|
mux = funcs["get_multiplexer"](session_name)
|
|
if mux:
|
|
mux.process.kill()
|
|
funcs["close_multiplexer"](session_name)
|
|
return {"status": "success"}
|
|
except Exception as e:
|
|
return {"status": "error", "error": str(e)}
|