콘텐츠로 이동

네이티브 셸, PTY, 프로세스 및 키 내부 구조

이 문서는 @f5-sales-demo/pi-natives실행/프로세스/터미널 기본 요소shell, pty, ps, keysdocs/natives-architecture.md의 아키텍처 용어를 사용하여 설명합니다.

  • crates/pi-natives/src/shell.rs
  • crates/pi-natives/src/shell/windows.rs (Windows 전용)
  • crates/pi-natives/src/pty.rs
  • crates/pi-natives/src/ps.rs
  • crates/pi-natives/src/keys.rs
  • crates/pi-natives/src/task.rs (shell/pty에서 사용하는 공유 취소 동작)
  • packages/natives/src/shell/index.ts
  • packages/natives/src/shell/types.ts
  • packages/natives/src/pty/index.ts
  • packages/natives/src/pty/types.ts
  • packages/natives/src/ps/index.ts
  • packages/natives/src/ps/types.ts
  • packages/natives/src/keys/index.ts
  • packages/natives/src/keys/types.ts
  • packages/natives/src/bindings.ts
  • TS 래퍼/API 레이어 (packages/natives/src/*): 타입이 지정된 진입점, 취소 인터페이스 (timeoutMs, AbortSignal), JS 편의 기능.
  • Rust N-API 모듈 레이어 (crates/pi-natives/src/*): 셸/PTY 프로세스 실행, 프로세스 트리 순회/종료, 키 시퀀스 파싱.
  • 유효성 검사 게이트 (native.ts, 아키텍처 레벨): 래퍼가 사용되기 전에 필요한 내보내기(Shell, executeShell, PtySession, killTree, listDescendants, 키 헬퍼)가 존재하는지 확인.

두 가지 실행 모드가 제공됩니다:

  1. executeShell(options, onChunk?)를 통한 일회성 실행.
  2. new Shell(options?)을 통한 영구 세션 생성 후 shell.run(...)을 반복 호출.

두 모드 모두 스레드 안전 콜백을 통해 출력을 스트리밍하며 { exitCode?, cancelled, timedOut }을 반환합니다.

Rust는 다음과 같이 brush_core::Shell을 생성합니다:

  • 비대화형 모드,
  • do_not_inherit_env: true,
  • 호스트 환경에서의 명시적인 환경 재구성,
  • 셸 민감 변수(PS1, PWD, SHLVL, bash 함수 내보내기 등)에 대한 스킵 목록 적용.

세션 환경 동작:

  • ShellOptions.sessionEnv는 세션 생성 시 한 번만 적용됩니다.
  • ShellRunOptions.env는 명령 범위(EnvironmentScope::Command)로 각 실행 후 팝됩니다.
  • PATH는 Windows에서 대소문자 구분 없는 중복 제거와 함께 특별히 병합됩니다.

Windows 전용 경로 보강(shell/windows.rs): 발견된 Git-for-Windows 경로(cmd, bin, usr/bin)가 존재하고 아직 포함되지 않은 경우 추가됩니다.

영구 셸(Shell.run)은 다음 상태 머신을 사용합니다:

  • 유휴/미초기화: session: None.
  • 실행 중: 첫 번째 run()이 세션을 지연 생성하고, current_abort 토큰을 저장하며, 명령을 실행합니다.
  • 완료 + 킵얼라이브: 실행 제어 흐름이 Normal이면 current_abort가 지워지고 세션이 재사용됩니다.
  • 완료 + 종료: 제어 흐름이 루프/스크립트/셸 종료 관련(BreakLoop, ContinueLoop, ReturnFromFunctionOrScript, ExitShell)이면 세션이 삭제됩니다(session: None).
  • 취소됨/타임아웃: 실행 태스크가 취소되고, 2초의 유예 대기 후 강제 중단되며, 세션이 삭제됩니다.
  • 오류: 세션이 삭제됩니다.

일회성 셸(executeShell)은 호출마다 항상 새 세션을 생성하고 삭제합니다.

  • 표준 출력/표준 오류는 공유 파이프로 라우팅되어 동시에 읽힙니다.
  • 리더는 UTF-8을 증분 방식으로 디코딩하며, 잘못된 바이트 시퀀스는 U+FFFD 대체 청크를 내보냅니다.
  • 프로세스 완료 후, 출력 드레인에는 백그라운드 작업이 디스크립터를 열어두는 경우 중단을 방지하기 위해 유휴/최대 가드(250ms 유휴, 2s 최대)가 적용됩니다.

취소, 타임아웃 및 백그라운드 작업

섹션 제목: “취소, 타임아웃 및 백그라운드 작업”
  • CancelTokentimeoutMs와 선택적 AbortSignal로부터 구성됩니다.
  • 취소/타임아웃 시 셸 취소 토큰이 트리거되고, 강제 중단 전에 태스크에 2초의 유예 기간이 부여됩니다.
  • 취소가 발생하면 brush 작업 메타데이터를 사용하여 백그라운드 작업이 종료됩니다(TERM, 이후 지연 KILL).

Shell.abort() 동작:

  • 해당 Shell 인스턴스에서 현재 실행 중인 명령만 중단합니다,
  • 실행 중인 것이 없으면 성공적인 no-op입니다.

자주 발생하는 오류에는 다음이 포함됩니다:

  • 세션 초기화 실패(Failed to initialize shell),
  • cwd 오류(Failed to set cwd),
  • 환경 설정/팝 실패,
  • 스냅샷 소스 실패,
  • 파이프 생성/복제 실패,
  • 실행 실패(Shell execution failed: ...),
  • 태스크 래퍼 실패(Shell execution task failed: ...).

결과 수준의 취소 플래그:

  • 타임아웃 -> exitCode: undefined, timedOut: true.
  • 중단 신호 -> exitCode: undefined, cancelled: true.

new PtySession()은 다음을 제공합니다:

  • start(options, onChunk?) -> Promise<{ exitCode?, cancelled, timedOut }>
  • write(data)
  • resize(cols, rows)
  • kill()

PtySession 상태 머신:

  • 유휴: core: None.
  • 예약됨: start()가 비동기 작업 시작 전에 동기적으로 제어 채널을 설치하므로(core: Some), write/resize/kill이 즉시 유효해집니다.
  • 실행 중: 블로킹 PTY 루프가 자식 상태, 리더 이벤트, 취소 하트비트 및 제어 메시지를 처리합니다.
  • 터미널 종료: 자식 종료 + 리더 완료.
  • 최종화됨: core는 start 태스크 완료(성공 또는 오류) 후 항상 None으로 재설정됩니다.

동시성 가드:

  • 이미 실행 중인 상태에서 시작하면 PTY session already running을 반환합니다.
  • PTY는 portable_pty::native_pty_system().openpty(...)를 통해 열립니다.
  • 명령은 현재 선택적 cwd 및 환경 오버라이드와 함께 sh -lc <command>로 실행됩니다.
  • write()는 원시 바이트를 PTY 표준 입력으로 전송합니다.
  • resize()는 차원을 클램프(cols 20..400, rows 5..200)하고 마스터 크기 조정을 호출합니다.
  • kill()은 실행을 취소됨으로 표시하고 자식 프로세스를 종료합니다.

출력 경로:

  • 전용 리더 스레드가 마스터 스트림을 읽고,
  • 잘못된 바이트에서 U+FFFD 대체와 함께 증분 UTF-8 디코딩을 수행하며,
  • 청크는 N-API 스레드 안전 콜백을 통해 전달됩니다.
  • timeoutMsAbortSignalCancelToken을 구성합니다.
  • 루프는 주기적으로 ct.heartbeat()를 호출하며, 중단은 자식 종료를 트리거합니다.
  • 타임아웃 분류는 하트비트 오류에서 문자열 기반("Timeout" 부분 문자열)으로 수행됩니다.

오류 발생 지점에는 다음이 포함됩니다:

  • PTY 할당/열기 실패,
  • PTY 스폰 실패,
  • 라이터/리더 획득 실패,
  • 자식 상태/대기 실패,
  • 락 포이즈닝,
  • 제어 채널 연결 끊김(PTY session is no longer available).

실행 중이 아닐 때의 제어 호출 실패:

  • write/resize/killPTY session is not running을 반환합니다.
  • killTree(pid, signal) -> number
  • listDescendants(pid) -> number[]

TS 래퍼는 또한 setNativeKillTree(native.killTree)를 통해 네이티브 킬 트리 통합을 공유 유틸리티에 등록합니다.

  • Linux: /proc/<pid>/task/<pid>/children을 재귀적으로 읽습니다.
  • macOS: libprocproc_listchildpids를 사용합니다.
  • Windows: CreateToolhelp32Snapshot으로 프로세스 테이블을 스냅샷하고, 부모->자식 맵을 구성하며, OpenProcess(PROCESS_TERMINATE) + TerminateProcess로 종료합니다.
  • 자손은 재귀적으로 수집됩니다.
  • 종료 순서는 하위에서 상위 방향(가장 깊은 자손 먼저)으로, 고아 재부모화를 줄이기 위함입니다.
  • 루트 pid는 마지막에 종료됩니다.
  • 반환 값은 성공적으로 종료된 수입니다.

시그널 동작:

  • POSIX: 제공된 signalkill에 전달됩니다.
  • Windows: signal은 무시되며, 종료는 무조건적인 프로세스 종료입니다.

이 모듈은 API 표면에서 의도적으로 예외를 발생시키지 않습니다:

  • 누락되거나 접근 불가능한 프로세스 트리 브랜치는 건너뜁니다,
  • pid별 종료 실패는 오류가 아닌 실패로 계산됩니다,
  • 조회 실패 시 일반적으로 listDescendants[]를, killTree0을 반환합니다.

제공되는 헬퍼:

  • parseKey(data, kittyProtocolActive)
  • matchesKey(data, keyId, kittyProtocolActive)
  • parseKittySequence(data)
  • matchesKittySequence(data, expectedCodepoint, expectedModifier)
  • matchesLegacySequence(data, keyName)

파서는 다음을 결합합니다:

  • 직접 단일 바이트 매핑(enter, tab, ctrl+<letter>, 인쇄 가능한 ASCII),
  • O(1) 레거시 이스케이프 시퀀스 조회 (PHF 맵),
  • xterm modifyOtherKeys 파싱,
  • Kitty 프로토콜 파싱(CSI u, CSI ~, CSI 1;...<letter>),
  • 키 ID로의 정규화(ctrl+c, shift+tab, pageUp, f5 등).

수정자 처리:

  • 키 매칭에는 shift/alt/ctrl 비트만 비교됩니다,
  • 잠금 비트는 비교 전에 마스크됩니다.

레이아웃 동작:

  • 기본 레이아웃 폴백은 의도적으로 제한되어 있어 리매핑된 레이아웃이 ASCII 문자/기호에 대한 잘못된 매칭을 생성하지 않습니다.
  • 인식되지 않거나 잘못된 시퀀스는 파싱 함수에서 null을 반환합니다.
  • 매치 함수는 파싱 실패 또는 불일치 시 false를 반환합니다.
  • 잘못된 키 입력에 대해 발생하는 오류 표면이 없습니다.

JS 래퍼 API ↔ Rust 내보내기 매핑

섹션 제목: “JS 래퍼 API ↔ Rust 내보내기 매핑”
TS 래퍼 APIRust N-API 내보내기비고
executeShell(options, onChunk?)executeShell (execute_shell)일회성 셸 실행
new Shell(options?)Shell 클래스영구 셸 세션
shell.run(options, onChunk?)Shell::run킵얼라이브 제어 흐름에서 세션 재사용
shell.abort()Shell::abort해당 셸 인스턴스의 활성 실행 중단
new PtySession()PtySession 클래스상태 저장 PTY 세션
pty.start(options, onChunk?)PtySession::start대화형 PTY 실행
pty.write(data)PtySession::write원시 표준 입력 패스스루
pty.resize(cols, rows)PtySession::resize클램프된 터미널 크기
pty.kill()PtySession::kill활성 PTY 자식 강제 종료
killTree(pid, signal)killTree (kill_tree)자식 우선 프로세스 트리 종료
listDescendants(pid)listDescendants (list_descendants)재귀적 자손 목록 조회
TS 래퍼 APIRust N-API 내보내기비고
matchesKittySequence(data, cp, mod)matchesKittySequence (matches_kitty_sequence)Kitty 코드포인트+수정자 매칭
parseKey(data, kittyProtocolActive)parseKey (parse_key)정규화된 키 ID 파서
matchesLegacySequence(data, keyName)matchesLegacySequence (matches_legacy_sequence)정확한 레거시 시퀀스 맵 확인
parseKittySequence(data)parseKittySequence (parse_kitty_sequence)구조화된 Kitty 파싱 결과
matchesKey(data, keyId, kittyProtocolActive)matchesKey (matches_key)고수준 키 매처

포기된 세션 정리 및 최종화 참고 사항

섹션 제목: “포기된 세션 정리 및 최종화 참고 사항”
  • 셸 영구 세션: 실행이 취소/타임아웃/오류/비킵얼라이브 제어 흐름인 경우, Rust는 내부 세션 상태를 명시적으로 삭제합니다. 정상적인 성공 실행은 세션을 재사용을 위해 유지합니다.
  • PTY 세션: core는 실패 경로를 포함하여 start()가 완료된 후 항상 지워집니다.
  • 명시적인 JS 파이널라이저 기반 종료 계약은 래퍼에 의해 제공되지 않으며, 정리는 주로 실행 완료/취소 경로에 연결됩니다. 호출자는 결정론적 종료를 위해 timeoutMs, AbortSignal, shell.abort() 또는 pty.kill()을 사용해야 합니다.