""" PTY Terminal Engine - 跨平台 PTY 终端管理 """ import asyncio import os from dataclasses import dataclass, field from typing import AsyncGenerator from uuid import uuid4 @dataclass class PTYSession: """PTY 会话""" session_id: str process: asyncio.subprocess.Process workspace_path: str class PTYManager: """PTY 会话管理器""" def __init__(self): self._sessions: dict[str, PTYSession] = {} self._output_queues: dict[str, asyncio.Queue] = {} async def spawn( self, cli: str, args: list[str], cwd: str, session_id: str | None = None, env: dict | None = None, ) -> str: """启动 PTY 会话""" if session_id is None: session_id = f"pty_{uuid4().hex[:8]}" # 构建环境变量 process_env = {**os.environ, "TERM": "xterm-256color"} if env: process_env.update(env) # 创建 PTY 进程 process = await asyncio.create_subprocess_exec( cli, *args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, cwd=cwd, env=process_env, ) session = PTYSession( session_id=session_id, process=process, workspace_path=cwd, ) self._sessions[session_id] = session self._output_queues[session_id] = asyncio.Queue() # 启动输出读取协程 asyncio.create_task(self._read_output(session_id)) return session_id async def _read_output(self, session_id: str): """读取 PTY 输出并放入队列""" session = self._sessions.get(session_id) if not session: return queue = self._output_queues[session_id] try: while True: line = await session.process.stdout.readline() if not line: break decoded_line = line.decode(errors="replace") await queue.put(decoded_line) # 广播到 WebSocket await self._broadcast(session_id, decoded_line) # 读取 stderr stderr_line = await session.process.stderr.readline() if stderr_line: decoded_err = stderr_line.decode(errors="replace") await queue.put(decoded_err) await self._broadcast(session_id, decoded_err) except Exception: pass finally: await queue.put(None) # 结束标记 async def write(self, session_id: str, data: str): """写入 PTY(用户输入)""" session = self._sessions.get(session_id) if session and session.process.stdin: session.process.stdin.write(data) await session.process.stdin.drain() async def read(self, session_id: str) -> AsyncGenerator[str, None]: """读取 PTY 输出""" queue = self._output_queues.get(session_id) if not queue: return while True: line = await queue.get() if line is None: break yield line async def resize(self, session_id: str, rows: int, cols: int): """调整终端大小""" # TODO: 实现 resize (需要平台特定实现) pass async def kill(self, session_id: str): """终止 PTY 会话""" if session_id in self._sessions: session = self._sessions[session_id] try: session.process.terminate() await asyncio.wait_for(session.process.wait(), timeout=3.0) except asyncio.TimeoutError: session.process.kill() await session.process.wait() except Exception: pass finally: del self._sessions[session_id] if session_id in self._output_queues: del self._output_queues[session_id] async def _broadcast(self, session_id: str, data: str): """广播输出到 WebSocket""" from app.routers.terminal import manager try: await manager.send(session_id, data) except Exception: pass def get_session(self, session_id: str) -> PTYSession | None: """获取会话""" return self._sessions.get(session_id) def list_sessions(self) -> list[str]: """列出所有会话 ID""" return list(self._sessions.keys()) # 全局单例 pty_manager = PTYManager()