Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ is the full contributor document and wins wherever the two disagree.
## Layout

- `crates/termlens/` — the published library. `terminal.rs` is the runtime,
`spawn.rs` starts the child (on Unix by itself, see DESIGN §1),
`screen.rs` the snapshot types, `emu/` the VT emulation behind a small
internal trait, `graphics.rs` the inline-image capture and decoder.
- `crates/termlens-cli/` — the `termlens` command (`inspect`, `diff`,
Expand All @@ -17,7 +18,8 @@ is the full contributor document and wins wherever the two disagree.
- `fixtures/` — deterministic terminal programs the integration suite spawns
in real PTYs. Never published; `publish = false`.
- `docs/DESIGN.md` — four layers, the wait-semantics contract, the snapshot
format. **Read it before touching `wait.rs`, `terminal.rs`, or `emu/`.**
format. **Read it before touching `wait.rs`, `terminal.rs`, `spawn.rs`,
or `emu/`.**

## Build and test

Expand Down
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,31 @@ reads that marker.

## [Unreleased]

### Fixed

- On Unix, a program that exists but cannot be executed now fails
`TerminalBuilder::spawn` with an `Error::Spawn` that says why, and
`termlens inspect` exits 2 with that message (#563). Typical causes are a
missing `#!` interpreter, an interpreter that is not a program, a missing
dynamic loader, and an argument list over the kernel's limit. The message
names the interpreter, with any `\r` that CRLF line endings left on it
shown escaped. `spawn` used to return `Ok` with a child already killed by
SIGABRT (`fatal runtime error: assertion failed:
output.write(&bytes).is_ok()`), which inspect showed as `exited: killed
by signal: Aborted`. portable-pty closed the pipe std reports a failed
exec through, so on Unix termlens now starts the child itself. Program
resolution, the environment, the session and the signals are unchanged,
and the child still inherits no descriptor besides its terminal. A
working directory the child cannot enter is now named in the error. A
few rare setups are still left to portable-pty, as before, and
`docs/DESIGN.md` lists them.
- The second writer termlens opens on the PTY master is close-on-exec. A
process the test started while a terminal was alive inherited it before,
which kept the master open in a process unrelated to the terminal.
- With `TERM` set twice through `.env`, XTGETTCAP's `TN` reply reports the
later value, the one the child gets. It reported the first, so an
application could get two answers to "which terminal is this?".

## [0.11.5] - 2026-10-07

### Changed
Expand Down
9 changes: 5 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,11 @@ cargo insta review # inspect and accept/reject each diff
`wait_idle`) must pass the **stress workflow** (`stress.yml` — the suite in
a 100-iteration loop on Ubuntu, macOS and Windows, sharded across five
`--test-threads` values). Trigger it from the Actions tab on your branch,
or ask a maintainer to. So must a test that *adds* PTY spawns: three of
the tests added on one day in September 2026 passed locally and went red
only under stress, and stress on `main` is what told one author's flake
apart from an inherited one.
or ask a maintainer to. So must a change to how the child is started
(`spawn.rs`), which every test goes through, and a test that *adds* PTY
spawns: three of the tests added on one day in September 2026 passed
locally and went red only under stress, and stress on `main` is what told
one author's flake apart from an inherited one.
- **No `sleep` as a wait.** A sleep is either too short and flaky under
load, or too long and slow on every run, and it names nothing: when it
fails, the log says only that time passed. Wait on what you mean —
Expand Down
29 changes: 18 additions & 11 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,24 @@ issues in escape-sequence handling) are treated as security issues.

What the project does continuously, enforced by required CI on every change:

- **`unsafe` is confined to three audited single-line blocks** — `dup(2)`,
to open a second writer on the PTY master for the responder thread;
`File::from_raw_fd`, which takes sole ownership of that duplicate so it is
closed exactly once; and `kill(2)` behind `Terminal::signal`. They sit
under `SAFETY` comments (the first two share one) and each carries its own
`#[allow(unsafe_code)]`. The workspace sets `unsafe_code = "warn"` and CI
runs clippy with `-D warnings`, so a new block fails unless it carries
that `#[allow]` as well: the attribute is what review has to see, not a
count the lint keeps. None touches memory the child can influence.
Everything on the parsing path is pure Rust: escape sequences from the
child are parsed, never executed or evaluated.
- **`unsafe` is confined to five audited blocks, all Unix-only.** Three
are single lines. `BorrowedFd::borrow_raw` borrows the PTY master's
descriptor, which std then duplicates, close-on-exec, into the responder
thread's second writer. `access(2)` resolves the program the way the PTY
layer does. `kill(2)` sits behind `Terminal::signal`. The fourth asks
`sigaction(2)` whether the kernel reaps this process's children. The
fifth is the `pre_exec` hook in `src/spawn.rs`, which runs in the forked
child before exec. It repeats portable-pty's own hook step by step, with
two changes: inherited descriptors are marked close-on-exec rather than
closed, so that a failed exec can be reported (#563), and the umask step
is left out because termlens never sets a umask. Each block sits under
its own `SAFETY` comment and carries its own `#[allow(unsafe_code)]`.
The workspace sets `unsafe_code = "warn"` and CI runs clippy with
`-D warnings`, so a new block fails unless it carries that `#[allow]` as
well: the attribute is what review has to see, not a count the lint
keeps. None touches memory the child can influence. Everything on the
parsing path is pure Rust: escape sequences from the child are parsed,
never executed or evaluated.
- **Dependency policy** (`cargo-deny` job): RUSTSEC advisories, yanked
crates, license allowlist, and crates.io-only sources — on every PR, with
weekly grouped Dependabot updates and Dependabot alerts enabled.
Expand Down
1 change: 1 addition & 0 deletions crates/termlens/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ mod error;
mod graphics;
mod keys;
mod screen;
mod spawn;
mod terminal;
mod utf8;
mod wait;
Expand Down
Loading
Loading