상주 VM 세션 — CrownyAI 브라우저 배선 (전용모드 가속)
개요
2026-07-11-상주VM세션-프로토타입.md(같은 날, 별도 세션)에서 만든
터미널상주서버.한선(TCP 상주 프로세스, 키왕복 12ms→4ms/렌더 20ms→8ms 실측)을
실제 CrownyAI 브라우저(native/crowny-browser.m)에 배선했다. 콜드 스폰(NSTask,
crownyc run 매회 재기동)을 브라우저 프로세스에 수명이 결합된 자식 TCP 상주
서버로 대체 — 영속 데몬/LaunchAgent 아님: 최소 1개 터미널 탭이 있을 때만
살아있고, 탭 전멸/브라우저 종료 시 반드시 kill된다.
무엇을 했는지
1. 서버 수명주기 (crowny-browser.m)
termSrvStart()— 전역 1개 인스턴스 가드(gTermSrvTask/gTermSrvStarting).
termSrvEnsureToauFresh()(4항 mtime 자가치유 — 소스·hanseonc_high·crownyc
바이너리 mtime 모두 비교, 기존 ensurePageSummaryToau 패턴 재사용)로
/tmp/터미널상주서버.toau 갱신 후 crownyc run을 NSTask로 백그라운드 기동.
termSrvFindFreePort()가 9801~9820을 lsof로 스캔해 미점유 포트 확보(개발기에
이미 9810/9811/9813/9815/9817가 다른 crownyc/node 상주 서비스로 점유돼
있음을 실측 확인 — 스캔이 필수임을 증명).
termSrvStop()— TCP "종료" 요청(정중 종료) +[NSTask terminate](응답
termSrvUpdateForTabChange(CrownyBrowserDelegate 메서드) —_tabs를
termSrvStart(), 0개면
termSrvStop(). 카운터 증감이 아니라 매번 재스캔 — configureTab:asAppService:
가 기존 앱뷰를 다른 서비스로 전환도 시키므로(탭 재사용 경로) 드리프트를
피하기 위함. 호출 지점 3곳: configureTab:asAppService:url: 끝,
closeTabAtIndex:(_tabs removeObjectAtIndex: 직후), applicationWillTerminate:
+ dealloc(이중 안전망, termSrvStop은 nil 가드라 재호출 무해).2. 키 입력 경로
termSrvRequest(NSString *line)— 인프로세스 BSD 소켓(NSTask 스폰 금지
select() 250ms 타임아웃, 연결 후
SO_RCVTIMEO/SO_SNDTIMEO 500ms 대칭 가드(가드레일 "소켓받기 recv 타임아웃
없음" 함정 방어).
appRunInputCoreCmdSmart(service, mode, value)— service=="터미널"이고
"키|<mode>|<value>" 우선 시도, 실패 시 2초 쿨다운
(gTermSrvDownUntil, 재시도 폭주 방지) 후 기존 appRunInputCoreCmd(NSTask
스폰) 폴백. 응답 포맷("OK|값|캐럿"/"SEL|.."/"오류:..")이 상주서버측
입력코어.한선 이식 로직과 완전 동일해 호출부(COPY/CUT의 "SEL|" 파싱 등)
무변경으로 재사용. 9개 호출 지점 전부 appRunInputCoreCmd(...) →
appRunInputCoreCmdSmart(self.service, ...)로 교체(FOCUS/KEY/BS/LEFT/RIGHT/
HOME/END/SELALL/PASTE/COPY/CUT/CLICKCARET).3. 렌더 cmd IR 경로
renderAndReload에서 service=="터미널"이고gTermSrvPort>0이면 렌더 스크립트
CROWNY_TERMSRV_PORT를 주입.
native/터미널렌더.sh—CROWNY_TERMSRV_PORT가 있으면/dev/tcp로
"렌더|CW|CH" 요청 → "OK|cmd" 응답이면 (서버가 이미 /tmp/터미널.cmd를
직접 기록했으므로) 콜드 스폰(CROWNY_OUT=cmd $VM run 터미널앱.toau) 스킵.
실패/미기동/타임아웃(연결 즉시거부 또는 응답 read -t 1)이면 자동으로 기존
스폰 경로로 폴백. 이후 페이지배선.py+페이지렌더러 단계는 완전 동일(무변경).4. 가드레일 준수 함정
- 한글 bash 변수명 함정 재발: 처음 작성 시
렌더됨/응답/줄같은
macOS bash 3.2가
렌더됨=0을 명령으로 오인 → command not found). 즉시 ASCII로 교정
(term_resident_rendered/term_resident_resp/term_resident_line).
값·주석은 한글 유지.검증 (헤드리스, GUI 불요)
- 서버 기동/종료 하네스(브라우저 없이):
hanseonc_high로
터미널상주서버.한선 컴파일 → CROWNY_TERMSRV_PORT=9801 crownyc run 백그라운드
기동 → /dev/tcp 로 FOCUS→KEY a/b/c→BS 순서 전송, /tmp/입력상태.psv
정확히 누적/갱신 확인(터미널입력|ab|2|2|2) → 렌더|960|640 → OK|cmd +
/tmp/터미널.cmd 17줄(INIT~END) 정상 생성 → 종료 → OK|종료 응답 후
프로세스 소멸(ps 확인) + 포트 반납(lsof 빈 결과) + 고아 0(pgrep -f 터미널상주서버
빈 결과).
- ObjC 소켓 클라이언트 독립 검증:
termSrvRequest와 동일 로직을 별도
.m 하네스로 컴파일(Foundation만 링크)해 실제 소켓 라운드트립 실측 —
살아있는 서버 FOCUS 0.49ms/KEY 0.67ms, 죽은 포트 0.05ms(즉시 nil, 250ms
타임아웃 발동 안 함 — loopback 미기동 포트는 커널이 즉시 RST), N=50 벤치
avg 1.40ms/min 0.86ms/max 1.94ms(bash /dev/tcp+nc 스폰 오버헤드가 없는
순수 인프로세스 소켓이라 프로토타입 문서의 4ms보다도 더 낮게 측정됨).
- 렌더 스크립트 양쪽 경로 검증:
CROWNY_TERMSRV_PORT=9801(살아있는 서버)
/tmp/터미널앱.runerr mtime 불변(스폰 스텝 스킵 확인) + /tmp/터미널.ppm
정상 생성(1,440,015B). CROWNY_TERMSRV_PORT 미설정 → runerr 새로 생성
(스폰 경로 확인) + 동일 크기 PPM. CROWNY_TERMSRV_PORT=9819(죽은 포트) →
99ms 내 자동 폴백, 정상 PPM 생성(연결 즉시거부라 타임아웃 대기 없음).
- 빌드 회귀:
make CrownyBrowser/make public/make universal/
make crownyonly/make all 전부 exit 0, 새 경고 0(기존에도 있던
crownyFontsRoot2/crownyButlerRoot 미사용 경고만 잔존, 무관).
- 고아 프로세스 최종 스윕:
pgrep -f 터미널상주서버= 0(테스트 종료 후),
termSrvFindFreePort가 이들을 올바르게 건너뛰는 근거가 됨.
- 바이너리 배포:
native/CrownyBrowser를mv로 (cp 금지 가드레일 준수)
CrownyAI.app/Contents/MacOS/CrownyBrowser에 배치 후 재빌드해 원본
보존. 배포본/빌드본 sha256 동일
(90669df8a285b2294719ca1cf866590a42e8a36393b707d628b4e51ffff42468).
기존 실행 중이던 CrownyAI.app(PID 77447, 구버전 바이너리로 이미 기동됨)은
mv가 원자적 교체라 영향 없이 계속 구동(다음 재시작부터 신버전 적용 —
상주서버 코드는 재시작 후 활성화).관련 파일
/Users/ef/CrownyBrowser/native/crowny-browser.m— 상주서버 관리 블록
kTermSrvSrc/kTermSrvToau/termSrvEnsureToauFresh/termSrvFindFreePort/
termSrvRequest/termSrvStop/termSrvStart/appRunInputCoreCmdSmart,
appRunInputCoreCmd 정의 직후 삽입), renderAndReload(CROWNY_TERMSRV_PORT
env 주입), configureTab:asAppService:url:/closeTabAtIndex:/
applicationWillTerminate:/dealloc(수명주기 훅), termSrvUpdateForTabChange
(CrownyBrowserDelegate 신규 메서드, closeTabAtIndex: 앞).
/Users/ef/CrownyBrowser/native/터미널렌더.sh— 상주 TCP 우선 + 콜드 스폰
/Users/ef/CrownyBrowser/src/터미널상주서버.한선— 서버 본체(수정 없음,
/Users/ef/CrownyBrowser/CrownyAI.app/Contents/MacOS/CrownyBrowser— 배포된
- 테스트 하네스(스크래치, 세션 종료 시 소멸 가능):
/private/tmp/claude-501/-Users-ef/929a8ec9-1c47-4929-9008-6af6618baaf0/scratchpad/termsrv_test.sh,
termsrv_client_test.m(+컴파일 바이너리).
- 크라우니코드 학습:
ObjC상주서버TCP소켓클라이언트논블로킹connect타임아웃폴백,
bash렌더sh상주TCP우선콜드스폰폴백devtcp (lookup MISS 확인 후 add).잔여 이슈
- 다른 앱뷰(메모/계산기 등)는 영향 없음 —
appRunInputCoreCmdSmart가
gTermSrvPort 단일 전역을 서비스별 딕셔너리로 일반화해야 함
(현재는 터미널 전용 하드코딩).
- 실 GUI 키보드 이벤트 E2E는 미실행 — 헤드리스 프로토콜/소켓 레벨
- 터미널상주서버 크래시 시 자동 재기동 없음(의도적 — "데몬 아님" 가드레일에
termSrvUpdateForTabChange 재호출) 시에만 재기동 시도.
libs/ANR서버.한선등TCP읽기(연결)1인자 호출 버그는 프로토타입
- 페이지렌더러(cmd→PPM) 상주화는 여전히 범위 밖(프로토타입 문서와 동일) —