- Home
- Documentation
- natives
- Natives Shell, PTY, Process, and Key Internals
Natives Shell, PTY, Process, and Key Internals
Natives Shell, PTY, Process, and Key Internals
Section titled “Natives Shell, PTY, Process, and Key Internals”This document covers the execution/process/terminal primitives in @f5xc-salesdemos/pi-natives: shell, pty, ps, and keys, using the architecture terms from docs/natives-architecture.md.
Implementation files
Section titled “Implementation files”crates/pi-natives/src/shell.rscrates/pi-natives/src/shell/windows.rs(Windows only)crates/pi-natives/src/pty.rscrates/pi-natives/src/ps.rscrates/pi-natives/src/keys.rscrates/pi-natives/src/task.rs(shared cancellation behavior used by shell/pty)packages/natives/src/shell/index.tspackages/natives/src/shell/types.tspackages/natives/src/pty/index.tspackages/natives/src/pty/types.tspackages/natives/src/ps/index.tspackages/natives/src/ps/types.tspackages/natives/src/keys/index.tspackages/natives/src/keys/types.tspackages/natives/src/bindings.ts
Layer ownership
Section titled “Layer ownership”- TS wrapper/API layer (
packages/natives/src/*): typed entrypoints, cancellation surface (timeoutMs,AbortSignal), and JS ergonomics. - Rust N-API module layer (
crates/pi-natives/src/*): shell/PTY process execution, process-tree traversal/termination, and key-sequence parsing. - Validation gate (
native.ts, architecture-level): ensures required exports (Shell,executeShell,PtySession,killTree,listDescendants, key helpers) exist before wrappers are used.
Shell subsystem (shell)
Section titled “Shell subsystem (shell)”API model
Section titled “API model”Two execution modes are exposed:
- One-shot via
executeShell(options, onChunk?). - Persistent session via
new Shell(options?)thenshell.run(...)repeatedly.
Both stream output through a threadsafe callback and return { exitCode?, cancelled, timedOut }.
Session creation and environment model
Section titled “Session creation and environment model”Rust creates brush_core::Shell with:
- non-interactive mode,
do_not_inherit_env: true,- explicit environment reconstruction from host env,
- skip-list for shell-sensitive vars (
PS1,PWD,SHLVL, bash function exports, etc.).
Session env behavior:
ShellOptions.sessionEnvis applied once at session creation.ShellRunOptions.envis command-scoped (EnvironmentScope::Command) and popped after each run.PATHis merged specially on Windows with case-insensitive dedupe.
Windows-only path enrichment (shell/windows.rs): discovered Git-for-Windows paths (cmd, bin, usr/bin) are appended if present and not already included.
Runtime lifecycle and state transitions
Section titled “Runtime lifecycle and state transitions”Persistent shell (Shell.run) uses this state machine:
- Idle/Uninitialized:
session: None. - Running: first
run()lazily creates session, storescurrent_aborttoken, executes command. - Completed + keepalive: if execution control flow is
Normal,current_abortis cleared and session is reused. - Completed + teardown: if control flow is loop/script/shell-exit related (
BreakLoop,ContinueLoop,ReturnFromFunctionOrScript,ExitShell), session is dropped (session: None). - Cancelled/Timed out: run task is cancelled, grace wait (2s), then force-abort; session is dropped.
- Error: session is dropped.
One-shot shell (executeShell) always creates and drops a fresh session per call.
Streaming/output behavior
Section titled “Streaming/output behavior”- Stdout/stderr are routed into a shared pipe and read concurrently.
- Reader decodes UTF-8 incrementally; invalid byte sequences emit
U+FFFDreplacement chunks. - After process completion, output drain has idle/max guards (
250msidle,2smax) to avoid hanging on background jobs keeping descriptors open.
Cancellation, timeout, and background jobs
Section titled “Cancellation, timeout, and background jobs”CancelTokenis constructed fromtimeoutMsand optionalAbortSignal.- On cancellation/timeout, shell cancellation token is triggered, then task gets a 2s graceful window before forced abort.
- If cancellation occurs, background jobs are terminated (
TERM, then delayedKILL) using brush job metadata.
Shell.abort() behavior:
- aborts only current running command for that
Shellinstance, - no-op success when nothing is running.
Failure behavior
Section titled “Failure behavior”Common surfaced errors include:
- session init failures (
Failed to initialize shell), - cwd errors (
Failed to set cwd), - env set/pop failures,
- snapshot source failures,
- pipe creation/clone failures,
- execution failure (
Shell execution failed: ...), - task wrapper failures (
Shell execution task failed: ...).
Result-level cancellation flags:
- timeout ->
exitCode: undefined,timedOut: true. - abort signal ->
exitCode: undefined,cancelled: true.
PTY subsystem (pty)
Section titled “PTY subsystem (pty)”API model
Section titled “API model”new PtySession() exposes:
start(options, onChunk?) -> Promise<{ exitCode?, cancelled, timedOut }>write(data)resize(cols, rows)kill()
Runtime lifecycle and state transitions
Section titled “Runtime lifecycle and state transitions”PtySession state machine:
- Idle:
core: None. - Reserved:
start()installs control channel synchronously (core: Some) before async work begins, sowrite/resize/killbecome immediately valid. - Running: blocking PTY loop handles child state, reader events, cancellation heartbeat, and control messages.
- Terminal closed: child exit + reader completion.
- Finalized:
coreis always reset toNoneafter start task completion (success or error).
Concurrency guard:
- starting while already running returns
PTY session already running.
Spawn/attach/write/read/terminate patterns
Section titled “Spawn/attach/write/read/terminate patterns”- PTY opened via
portable_pty::native_pty_system().openpty(...). - Command currently runs as
sh -lc <command>with optionalcwdand env overrides. write()sends raw bytes to PTY stdin.resize()clamps dimensions (cols 20..400,rows 5..200) and calls master resize.kill()marks run as cancelled and kills child process.
Output path:
- dedicated reader thread reads master stream,
- incremental UTF-8 decode with
U+FFFDreplacement on invalid bytes, - chunks forwarded through N-API threadsafe callback.
Cancellation and timeout semantics
Section titled “Cancellation and timeout semantics”timeoutMsandAbortSignalfeed aCancelToken.- loop calls
ct.heartbeat()periodically; abort triggers child kill. - timeout classification is string-based (
"Timeout"substring in heartbeat error).
Failure behavior
Section titled “Failure behavior”Error surfaces include:
- PTY allocation/open failure,
- PTY spawn failure,
- writer/reader acquisition failure,
- child status/wait failures,
- lock poisoning,
- control-channel disconnection (
PTY session is no longer available).
Control call failures when not running:
write/resize/killreturnPTY session is not running.
Process-tree subsystem (ps)
Section titled “Process-tree subsystem (ps)”API model
Section titled “API model”killTree(pid, signal) -> numberlistDescendants(pid) -> number[]
TS wrapper also registers native kill-tree integration into shared utils via setNativeKillTree(native.killTree).
Platform-specific implementation
Section titled “Platform-specific implementation”- Linux: recursively reads
/proc/<pid>/task/<pid>/children. - macOS: uses
libprocproc_listchildpids. - Windows: snapshots process table with
CreateToolhelp32Snapshot, builds parent->children map, terminates withOpenProcess(PROCESS_TERMINATE)+TerminateProcess.
Kill-tree behavior
Section titled “Kill-tree behavior”- Descendants are collected recursively.
- Kill order is bottom-up (deepest descendants first) to reduce orphan re-parenting.
- Root pid is killed last.
- Return value is count of successful terminations.
Signal behavior:
- POSIX: provided
signalis passed tokill. - Windows:
signalis ignored; termination is unconditional process terminate.
Failure behavior
Section titled “Failure behavior”This module is intentionally non-throwing at API surface:
- missing/inaccessible process tree branches are skipped,
- per-pid kill failures are counted as unsuccessful (not errors),
- lookup miss typically yields
[]fromlistDescendantsand0fromkillTree.
Key parsing subsystem (keys)
Section titled “Key parsing subsystem (keys)”API model
Section titled “API model”Exposed helpers:
parseKey(data, kittyProtocolActive)matchesKey(data, keyId, kittyProtocolActive)parseKittySequence(data)matchesKittySequence(data, expectedCodepoint, expectedModifier)matchesLegacySequence(data, keyName)
Parsing model
Section titled “Parsing model”The parser combines:
- direct single-byte mappings (
enter,tab,ctrl+<letter>, printable ASCII), - O(1) legacy escape-sequence lookup (PHF map),
- xterm
modifyOtherKeysparsing, - Kitty protocol parsing (
CSI u,CSI ~,CSI 1;...<letter>), - normalization to key IDs (
ctrl+c,shift+tab,pageUp,f5, etc.).
Modifier handling:
- only shift/alt/ctrl bits are compared for key matching,
- lock bits are masked out before comparisons.
Layout behavior:
- base-layout fallback is intentionally constrained so remapped layouts do not create false matches for ASCII letters/symbols.
Failure behavior
Section titled “Failure behavior”- Unrecognized or invalid sequences produce
nullfrom parse functions. - Match functions return
falseon parse failure or mismatch. - No thrown error surface for malformed key input.
JS wrapper API ↔ Rust export mapping
Section titled “JS wrapper API ↔ Rust export mapping”Shell + PTY + Process
Section titled “Shell + PTY + Process”| TS wrapper API | Rust N-API export | Notes |
|---|---|---|
executeShell(options, onChunk?) | executeShell (execute_shell) | One-shot shell execution |
new Shell(options?) | Shell class | Persistent shell session |
shell.run(options, onChunk?) | Shell::run | Reuses session on keepalive control flow |
shell.abort() | Shell::abort | Aborts active run for that shell instance |
new PtySession() | PtySession class | Stateful PTY session |
pty.start(options, onChunk?) | PtySession::start | Interactive PTY run |
pty.write(data) | PtySession::write | Raw stdin passthrough |
pty.resize(cols, rows) | PtySession::resize | Clamped terminal dimensions |
pty.kill() | PtySession::kill | Force-kills active PTY child |
killTree(pid, signal) | killTree (kill_tree) | Children-first process tree termination |
listDescendants(pid) | listDescendants (list_descendants) | Recursive descendants listing |
| TS wrapper API | Rust N-API export | Notes |
|---|---|---|
matchesKittySequence(data, cp, mod) | matchesKittySequence (matches_kitty_sequence) | Kitty codepoint+modifier match |
parseKey(data, kittyProtocolActive) | parseKey (parse_key) | Normalized key-id parser |
matchesLegacySequence(data, keyName) | matchesLegacySequence (matches_legacy_sequence) | Exact legacy sequence map check |
parseKittySequence(data) | parseKittySequence (parse_kitty_sequence) | Structured Kitty parse result |
matchesKey(data, keyId, kittyProtocolActive) | matchesKey (matches_key) | High-level key matcher |
Abandoned session cleanup and finalization notes
Section titled “Abandoned session cleanup and finalization notes”- Shell persistent session: if a run is cancelled/timed out/errors/non-keepalive control flow, Rust explicitly drops the internal session state. Successful normal runs keep the session for reuse.
- PTY session:
coreis always cleared afterstart()finishes, including failure paths. - No explicit JS finalizer-driven kill contract is exposed by wrappers; cleanup is primarily tied to run completion/cancellation paths. Callers should use
timeoutMs,AbortSignal,shell.abort(), orpty.kill()for deterministic teardown.