82 lines
2.7 KiB
Python
82 lines
2.7 KiB
Python
"""기존 tmux 세션에 attach하여 CLI 입출력을 중계한다."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import subprocess
|
|
|
|
import pexpect
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class PtyManager:
|
|
"""기존 tmux 세션에 attach하고 입출력을 제어한다."""
|
|
|
|
def __init__(self, session_name: str = "claude", cli_name: str = "claude") -> None:
|
|
self.session_name = session_name
|
|
self.cli_name = cli_name
|
|
self._process: pexpect.spawn | None = None
|
|
|
|
@property
|
|
def is_alive(self) -> bool:
|
|
return self._process is not None and self._process.isalive()
|
|
|
|
def _ensure_session_exists(self) -> None:
|
|
"""attach 대상 tmux 세션 존재 여부를 검증한다."""
|
|
result = subprocess.run(
|
|
["tmux", "has-session", "-t", self.session_name],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode != 0:
|
|
raise RuntimeError(
|
|
"tmux 세션이 없습니다. 먼저 실행하세요: "
|
|
f"tmux new -s {self.session_name} {self.cli_name}"
|
|
)
|
|
|
|
def start(self) -> None:
|
|
"""기존 tmux 세션에 attach한다."""
|
|
self._ensure_session_exists()
|
|
logger.info("tmux 세션 attach: %s", self.session_name)
|
|
self._process = pexpect.spawn(
|
|
"tmux",
|
|
["attach-session", "-t", self.session_name],
|
|
encoding="utf-8",
|
|
timeout=None,
|
|
)
|
|
|
|
def send(self, text: str, submit: bool = False) -> None:
|
|
"""프로세스에 텍스트 입력을 전달한다."""
|
|
if not self.is_alive:
|
|
raise RuntimeError("프로세스가 실행 중이 아닙니다.")
|
|
assert self._process is not None
|
|
logger.debug("입력 전송: %s", text)
|
|
if submit:
|
|
self._process.sendline(text)
|
|
return
|
|
self._process.send(text)
|
|
|
|
def send_enter(self) -> None:
|
|
"""엔터 키 입력만 전송한다."""
|
|
self.send("", submit=True)
|
|
|
|
def read_output(self, timeout: float = 5) -> str:
|
|
"""프로세스의 출력을 읽는다."""
|
|
if not self.is_alive:
|
|
raise RuntimeError("프로세스가 실행 중이 아닙니다.")
|
|
assert self._process is not None
|
|
try:
|
|
self._process.expect(pexpect.TIMEOUT, timeout=timeout)
|
|
except pexpect.TIMEOUT:
|
|
pass
|
|
return self._process.before or ""
|
|
|
|
def stop(self) -> None:
|
|
"""attach 연결만 종료한다(원격 세션은 유지)."""
|
|
if self._process is not None:
|
|
logger.info("tmux attach 종료")
|
|
self._process.close(force=True)
|
|
self._process = None
|