-
Notifications
You must be signed in to change notification settings - Fork 0
Platform Differences
pytmux는 macOS · Linux · Windows를 모두 지원하지만, 터미널 멀티플렉서의 핵심인 PTY(의사
터미널)는 OS마다 모델이 근본적으로 다르다. POSIX(macOS/Linux)는 커널이 openpty/fork로
PTY와 자식 프로세스를 한 번에 만드는 반면, Windows는 ConPTY(의사콘솔)라는 별도 메커니즘을
쓴다. 이 한 가지 차이에서 새 탭 지연, 세션 유지 재시작 가능 여부, 일부 자동 기능의 유무가
모두 갈린다.
이 문서는 사용자와 기여자를 위한 "무엇이 다르고 왜 다른가" 실무 참고서다. macOS와 Linux는 둘 다 POSIX이므로 거의 동일하게 묶어 "POSIX"로 표기한다.
| 서브시스템 | POSIX (macOS / Linux) | Windows |
|---|---|---|
| PTY 백엔드 |
pty.fork() — 커널 단일 호출 |
ConPTY/HPCON + 아웃오브프로세스 pty-host |
| 새 탭 spawn 비용 | ~10–50 ms | ~100 ms (풀 적중) / ~165–190 ms (미적중) |
| 셸당 프로세스 수 | 1개 (셸만) | 2개 (콘솔 호스트 + 셸) |
| 세션 유지 재시작 |
execv 제자리 교체 + fd 상속 |
pty-host가 PTY를 소유 → 서버만 재기동 |
| IME 한/영 상태 | TIS 감시 헬퍼 자식 프로세스 | IMM32 직접 조회 |
| 패널 cwd 상속 | 지원 | 폴백(서버 cwd에서 시작) |
| fg 명령 기반 자동 탭이름 | 지원 | 폴백(고정 이름) |
| 기본 셸 |
$SHELL (zsh/bash) |
COMSPEC (cmd.exe) |
이 절의 차이가 아래 모든 차이의 뿌리다.
-
pty.fork()한 번이 fork + PTY 마스터/슬레이브 생성 + 자식의 셸 exec를 동시에 처리한다. 중간에 끼는 호스트 프로세스가 없다. - 마스터는 단일 정수 파일 디스크립터(fd) 이고, 출력 읽기는
loop.add_reader(fd)로 커널 이벤트(kqueue/epoll)에 직접 건다. 폴링 스레드도 sleep도 없다.
- POSIX의
fork/execve가 없으므로 ConPTY(Windows 10 1809+)를 쓴다.CreatePseudoConsole이 콘솔 호스트 프로세스(번들OpenConsole.exe)를 띄우고,CreateProcessW가 그 콘솔에 attach된 셸을 띄운다 — 그래서 탭 하나당 프로세스 2개다. - 마스터는 fd가 아니라 핸들 묶음(conout 읽기 핸들, conin 쓰기 핸들, 의사콘솔 핸들
HPCON, 자식 프로세스 핸들, 전용 블로킹 리더 스레드)이다. - ConPTY 파이프는 asyncio의
add_reader로 못 읽는다(Proactor 루프 제약). 그래서 pytmux는 클라이언트 통신은 TCP 루프백 + Proactor 루프, PTY 읽기는 패널마다 블로킹ReadFile리더 스레드가 받아 이벤트 루프로 밀어넣는 구조를 쓴다. - 추가로 exit 감시 스레드가 ~200 ms 주기로 자식 종료를 폴링한다. 콘솔 호스트가 자식
사후에도 살아
ReadFile이 영구 블록되는 ConPTY 한계를 우회하기 위한 것으로, 정상 출력 경로에는 영향이 없다.
멀티바이트 무손상: Windows 백엔드는 raw 바이트를 그대로 읽어 점진적 디코더로 처리하므로 CJK·이모지가 read 경계에서 깨지지 않는다(U+FFFD 폭주 없음).
"macOS에서는 새 탭을 열면 프롬프트가 바로 뜨는데 Windows는 눈에 띄게 느리다"는 체감은 사실이며, 원인은 pytmux의 알고리즘이 아니라 OS의 PTY 생성 모델 차이다.
| OS | 새 탭 셸 spawn 비용 | 프로세스 생성 수 |
|---|---|---|
| macOS / Linux | ~10–50 ms | 1개 (셸만) |
| Windows (풀 적중) | ~101 ms | 2개 (콘솔 호스트 + 셸) |
| Windows (풀 미적중) | ~165–190 ms | 2개 |
핵심 원인 세 가지:
-
프로세스 생성 1개 vs 2개. ConPTY는 콘솔마다 호스트 프로세스를 요구한다. POSIX의
pty.fork에는 대응물이 없다(PTY가 커널 객체라 호스트가 불필요). -
프로세스 생성 단가 자체. Windows의
CreateProcessW(이미지 로드, 런타임 초기화)는 POSIX의 copy-on-writefork보다 본질적으로 무겁다. 호스트를 빼고 셸 생성만 봐도 더 비싸다. - 읽기 경로. POSIX는 커널 이벤트로 0-비용 통지, Windows는 블로킹 리더 스레드 + 이벤트 루프 홉이 필요하다.
새 탭 비용의 절반인 호스트 spawn(CreatePseudoConsole)은 작업 디렉토리·명령과 무관하다.
그래서 pytmux는 셸을 붙이지 않은 의사콘솔을 백그라운드로 미리 만들어 풀에 넣어 둔다.
새 탭은 풀에서 하나 꺼내 크기만 맞추고 셸만 띄우므로 호스트 생성 비용이 임계경로에서 빠진다.
풀 OFF 188.7 ms
풀 적중 101.7 ms → 약 46% 단축
기본 셸은 cmd.exe(COMSPEC)다. PowerShell(pwsh)은 프로파일 로딩으로 ~500–800 ms가
들어 새 탭 체감을 크게 악화시키므로 기본으로 두지 않는다. PowerShell이 필요하면 환경변수로
명시 선택할 수 있다.
결론: Windows의 남은 ~100 ms는 셸
CreateProcessW비용으로, ConPTY가 콘솔당 호스트 프로세스를 요구하는 한 OS 구조상 macOS 수준(즉시)으로 줄일 수 없다. 이는 기대치로 받아들이는 것이 맞다.
서버 코드를 갱신하면서 실행 중인 셸·작업을 살린 채 서버를 재시작하는 기능이다.
-
os.execv가 프로세스 메모리 이미지를 새 서버 코드로 갈아끼운다 — PID가 그대로라 자식 셸들이 계속 서버의 자식으로 남는다. - PTY 마스터 fd의 close-on-exec 플래그를 교체 직전에만 풀어, 새 이미지가 같은 fd 번호로 살아 있는 PTY를 그대로 상속한다.
- 상태 파일에 fd 번호·자식 PID만 적어두고, 새 이미지가 "정말 열린 PTY인가"를 검증한 뒤 fork 없이 재채택한다.
- Windows엔 진짜
execve가 없다. CRT의_execv계열은 내부적으로CreateProcess+ 호출자 종료라 결과는 새 PID다 → 자식 셸들이 고아가 된다. - 의사콘솔 핸들
HPCON은 그 DLL/프로세스 내부의 불투명 상태다. ConPTY 공개 API는 Create/Resize/Close 3종뿐이고 직렬화·재attach API가 없다. 새 서버 프로세스는 옛 HPCON을 이어받을 수 없다.
ConPTY 소유를 서버에서 분리해, 장수명 pty-host 프로세스가 HPCON·자식 프로세스·파이프· 워밍 풀을 소유한다. 서버는 host와 로컬 소켓으로 통신(spawn/read/write/resize/close)한다. 서버가 재시작해도 host는 그대로 살아 있고, 새 서버가 host에 재연결해 패널을 host-id로 다시 참조하므로 세션이 유지된다. (VS Code가 터미널 보존을 위해 PTY를 별도 ptyHost로 분리한 것과 같은 철학이다.) Windows에서 기본 활성화되어 있다.
POSIX에서는 패널 생존이 커널 fd라는 내재 속성이지만, Windows에서는 외부 브로커(host)를 통한 간접 속성이다. 그래서 host가 미연결이거나 크래시한 짧은 창에서 태어난 패널은 in-process로 폴백되어 재시작을 넘기지 못한다.
실제 재시작 전 드라이런으로 패널들이 재시작을 넘길 수 있는지 점검한다. "패널 master fd 보유" 항목이 Windows에서 macOS보다 더 자주 FAIL하는 것은 위의 구조 차이 때문이다 — host 연결이 잠깐 끊긴 창에서 태어난 패널이 생기면 부분 실패(예: 1/2)로 표시된다. 단, 이 FAIL은 재시작을 막지 않고 명시적 확인을 요구할 뿐이며, "재시작이 크래시한다"는 뜻이 아니라 **"그 패널 1개의 실행 셸/작업만 재시작을 못 넘긴다"**는 정직한 경고다. 서버 재시작 자체와 세션 구조, host-backed 패널은 보존된다.
클라이언트 재시작은 OS 중립으로 항상 가능하다 — 클라이언트는 PTY를 소유하지 않으므로 그냥 재접속하면 된다. 어려움은 전적으로 서버가 쥔 PTY 보존에 있다.
상태줄의 한/영 입력 상태 배지는 OS API를 통해 현재 키보드 입력 소스를 읽는다.
- macOS: Text Input Source(TIS) API로 조회한다. 단, 장수명 프로세스 안에서 인프로세스로 조회하면 런루프가 돌지 않아 상태가 멈추는 문제가 있어, pytmux는 별도 감시 헬퍼 자식 프로세스를 띄워 실제 런루프 + 시스템 알림으로 변경을 감지하고 그 출력을 받아 반영한다.
- Windows: IMM32로 직접 조회하며 정상 동작한다(별도 헬퍼 불필요).
배지에 표시되는 것은 확정된 입력 소스 상태다(조합 중 미확정 글자는 관찰 대상이 아님).
복사·붙여넣기에 쓰는 외부 도구가 OS마다 다르다.
| OS | 복사 | 붙여넣기 |
|---|---|---|
| macOS | pbcopy |
pbpaste |
| Linux |
xclip / wl-copy
|
xclip / wl-paste
|
| Windows | clip.exe |
powershell Get-Clipboard |
run-shell/if-shell/popup 같은 셸 실행도 POSIX는 /bin/sh -c, Windows는 cmd /c로
분기한다.
-
POSIX: 자식에게 새지 말아야 할 fd는
FD_CLOEXEC로 표시한다. 세션 유지 재시작 때만 넘길 PTY 마스터 fd의 이 플래그를 잠깐 풀어 새 서버 이미지가 상속하게 한다(§3). -
Windows: fd 대신 커널 핸들이고, 상속 여부는
SetHandleInformation으로 제어한다. 새 프로세스로 핸들을 넘길 때는 명시적 상속 핸들 리스트를 쓴다. 다만 §3에서 보았듯HPCON은 핸들 상속으로도 이관할 수 없어, 재시작 시 핸들 상속이 아니라 pty-host 재연결로 푼다.
데몬(서버) 기동도 다르다 — POSIX는 setsid 이중 fork로 분리하고, Windows는
DETACHED_PROCESS | CREATE_NO_WINDOW로 콘솔 창 없이 분리 기동한다(가능하면 콘솔을 만들지
않는 pythonw.exe 선호).
- POSIX는 자동 테스트로 거의 전부 덮인다. pty-host의 프로토콜·프록시·재연결·재시작 메커니즘은 백엔드 중립으로 짜여 있어 POSIX 위에서 자동 테스트로 검증된다. ConPTY 프리미티브 자체만 별도 스파이크로 검증했다.
- 실 ConPTY / 실 콘솔 경로는 드라이버로 검증 불가하다 — detached host 실기동, Windows TCP portfile 포트 발견, 실 ConPTY 패널의 재시작 across 생존, 실제 한글 IME, 실제 LLM 코딩 패널 같은 항목은 실제 Windows 머신에서 사람이 라이브 검증해야 한다.
- 테스트 하네스는 결정론을 위해 Windows에서 host 모드를 강제로 끄고 인프로세스 백엔드를 쓴다. 따라서 host 전용 테스트는 그 환경에서 자동 스킵된다.
- macOS 헤드리스 CI 주의: 헤드리스 macOS 러너는 일부 PTY 스위트를 인프라 레벨에서 wedge(멈춤)시켜, 코드로 고칠 수 없다. 그래서 CI 매트릭스에서 제외하고 로컬 macOS 실행을 권위로 삼는다. 이는 코드 결함이 아니라 러너 인프라의 한계다.
소개 · 사용
- Project-Overview
- User-Manual
- Screenshots
- Settings-Popup
- Single-Session-Model
- Tmux-Feature-Comparison
플러그인
Claude Code 플러그인
- Claude-Code-Plugins
- Dev-Guide-claude-code
- Dev-Guide-claude-token-usage-view
- Dev-Guide-claude-prompt-history
- Dev-Guide-claude-resume
- Dev-Guide-claude-disable-feedback
플랫폼 · 성능
품질 · 보안
리뷰·분석 보고서
연혁
기여