>_LSOFRS — THE FASTEST LSOF EVER WRITTEN
Maps the invisible topology between processes and the files they hold open. Regular files, directories, sockets, pipes, devices, kqueues — anything the kernel touches. Rust rewrite of lsofng with zero-copy repr(C) FFI to Darwin / Linux / FreeBSD kernel headers, rayon work-stealing across every CPU for per-PID FD enumeration, and a single streaming pass from gather → filter → print. 14.2 ms wall-clock vs lsof's 169.8 ms on a ~470-process / ~5000-FD macOS box (hyperfine, 10 runs, 3 warmup).
Quickstart
Install from crates.io or source. The same binary is published twice — lsofrs (long) and lsf (short, quicker to type):
# install cargo install lsofrs # from source git clone https://github.com/MenkeTechnologies/lsofrs cd lsofrs && cargo build --release sudo cp target/release/lsf /usr/local/sbin/ # everyday queries lsf # list all open files lsf -p 1234 # files for PID 1234 lsf -c Chrome # files for Chrome processes lsf -u root # files for root user lsf -i # network connections only lsf -i :8080 # who's listening on port 8080 lsf /path/to/file # who has this file open lsf -t -c nginx # just PIDs (for scripting) # pipelines lsf --json | jq '.[] | select(.fd == "cwd")' lsf --csv > snapshot.csv lsf -F pcfn # field output (p=pid, c=cmd, f=fd, n=name)
Full install + usage live in the README. Man page: man lsofrs (310-line roff under lsofrs.1). Zsh completion ships under completions/_lsofrs.
Why lsofrs — Wall-Clock Comparison
Benchmarked on macOS with hyperfine (10 runs, 3 warmup, ~470 processes / ~5000 open files). Numbers are reproducible from README §PERFORMANCE.
| Workload | lsofrs (Rust) | lsof 4.91 (C) | lsofng (C) | Speedup |
|---|---|---|---|---|
| All open files (default) | 14.2 ms | 169.8 ms | 173.0 ms | 12× |
Network only (-i TCP) | 7.2 ms | 91.7 ms | 88.1 ms | 12–13× |
Terse PIDs (-t) | 6.9 ms | 101.4 ms | 142.9 ms | 15–21× |
Structured (-J JSON / -F field) | 29.3 ms | 156.1 ms | 136.7 ms | 5× |
Feature parity vs incumbents
| Feature | lsofrs | lsof | lsofng | fuser | ss |
|---|---|---|---|---|---|
Built-in JSON (--json / -J) | ✓ | ✗ | ✓ | ✗ | ✗ |
Built-in CSV (--csv, RFC 4180) | ✓ | ✗ | ✗ | ✗ | ✗ |
Field output (-F) | ✓ | ✓ | ✓ | ✗ | ✗ |
Full-screen tabbed TUI (--tui) | 7 tabs | ✗ | ✗ | ✗ | ✗ |
Top-N FD dashboard (--top) | ✓ | ✗ | ✗ | ✗ | ✗ |
Stale-FD finder (--stale) | ✓ | ✗ | ✗ | ✗ | ✗ |
Listening-ports view (--ports) | ✓ | ✗ | ✗ | ✗ | ✓ |
Process tree (--tree) | ✓ | ✗ | ✗ | ✗ | ✗ |
Pipe / unix-socket IPC map (--pipe-chain) | ✓ | ✗ | ✗ | ✗ | ✗ |
Net-by-remote-host map (--net-map) | ✓ | ✗ | ✗ | ✗ | ✗ |
File-open/close watch (--watch) | ✓ | ✗ | ✗ | ✗ | ✗ |
FD-leak detector (--leak-detect) | ✓ | ✗ | ✗ | ✗ | ✗ |
Single-process follow (--follow) | ✓ | ✗ | ✗ | ✗ | ✗ |
Delta highlighting (--delta) | ✓ | ✗ | ✗ | ✗ | ✗ |
Aggregate summary + bar charts (--summary) | ✓ | ✗ | ✗ | ✗ | ✗ |
| Themeable output (31 palettes) | ✓ | ✗ | ✗ | ✗ | ✗ |
| Parallel per-PID gather (rayon) | ✓ | ✗ | ✗ | ✗ | ✗ |
| Cross-platform (Darwin + Linux + FreeBSD) | ✓ | ✓ | ✓ | Linux/BSD | Linux |
Overview
- CLI front-end — clap derive parser in
src/cli.rs(1,341 lines, 38#[arg(...)]attributes).main.rsdispatches to the active mode based on which flag is set. - Platform gather —
src/darwin.rs(1,253 lines, libproc FFI),src/linux.rs(820 lines,/procreader),src/freebsd.rs(835 lines, sysctl + procfs). All three return the sameVec<Process>fromtypes.rs; each per-PID FD scan is parallelized with rayon's work-stealing pool. - Selection —
src/filter.rs(2,812 lines) implements OR-mode (default) and AND-mode (-a) filter composition over PID / PGID / user / command / FD / network-spec / path. Supports comma-lists, ranges (0-10), exclusion (^root), and regex (/nginx|apache/). - Output —
src/output.rs(columnar + field),src/json.rs(serde),src/csv_out.rs(RFC 4180). TTY detection auto-switches between cyberpunk headers (H4XOR/CL4SS/T4RGET) and plain headers for pipelines. - Live modes —
--tui(7-tab dashboard, 5,312 lines),--top(1,008 lines),--summary -r(904 lines),--monitor(288 lines),--watch FILE,--follow PID,--leak-detect. All sharesrc/tui_app.rs'sTuiModetrait for common keybindings, alternate-screen entry/exit, and atomic frame rendering. - Themes —
src/theme.rs(1,005 lines) defines 31 named palettes via theThemeNameenum. Custom 6-color themes persist to~/.lsofrs.confthroughsrc/config.rs(TOML).
Modes — one card per entry point
--tui
Unified 7-tab dashboard: TOP / SUMMARY / PORTS / TREE / NET-MAP / PIPES / STALE. Click tabs, mouse-hover for tooltips, right-click for verbose detail (PID, FD breakdown, kill / copy hints). Theme chooser (c) + editor (C).
--top [N]
Live auto-refreshing top-N processes sorted by FD count. FD-type distribution bar, delta tracking, per-process breakdown. s cycles sort column, +/- grows/shrinks N.
--summary [-r N]
Aggregate FD breakdown with bar charts, top processes, per-user totals. With -r N becomes a live TUI; --json emits a structured report; -i restricts to network FDs.
--ports
Quick "what's listening where" summary — like ss -tlnp but cross-platform (macOS + Linux + FreeBSD). --json and per-user filtering supported.
--tree
Hierarchical process tree with FD counts, per-PID type breakdown ([REG:12 IPv4:3 PIPE:2]), network connection counts, and notable open files inline.
--net-map
Network connections grouped by remote host — fastest way to see which servers your box talks to and how many sockets each peer holds.
--pipe-chain
Trace pipe and unix-socket pairs between processes — visualize the IPC topology that ps and top hide.
--stale
Find FDs pointing to deleted files. Common source of disk-space leaks, zombie handles, and security issues (process held an unlinked secret file).
--watch FILE
Print timestamped +OPEN / -CLOSE events for a single path. Lightweight inotifywait / fs_usage equivalent that runs on macOS, Linux, and FreeBSD.
--follow PID
Watch one process's FDs in real time. New opens highlight +NEW (green), closes -DEL (red).
--leak-detect[=I,N]
Sample per-process FD counts every I seconds; flag any PID whose count rises N consecutive intervals in a row. Circular buffer per PID in src/leak.rs.
--delta -r N
Color-code changes between repeat iterations. New FDs in green, removed in red — useful inside watch-style loops without the noise of full re-prints.
--monitor / -W
Classic full-screen alternate-buffer monitor like top(1). s sort / r reverse / f filter / p pause / ? help.
--csv
Pure RFC-4180 CSV dump for pipelines and spreadsheets. Composes with every selector — lsf --csv -i TCP -u root > root_tcp.csv.
--json / -J
Serde-backed JSON array. Composes with every selector. Each element carries the full OpenFile shape from src/types.rs (PID, command, FD, type, device, size/offset, node, name, plus parsed socket / protocol fields when present).
-F <chars>
Per-record field output for scripting (compat with the original lsof). p=pid, c=command, f=fd, n=name, t=type, etc.
Selection grammar
Every selector is independent and composable. By default selectors OR together; -a switches the entire predicate to AND mode. Negation via ^, ranges via N-M, regex via /…/.
lsf -p 1234,5678 # PIDs 1234 OR 5678 lsf -u root,wizard # users root OR wizard lsf -p ^1234 # everything except PID 1234 lsf -u ^root # everything except root lsf -d 0-10 # FDs 0 through 10 lsf -c '/nginx|apache/' # command name matches regex lsf -a -p 1234 -i # AND: PID 1234 AND has network FD lsf -a -u nginx -i TCP:443 # AND: nginx user AND TCP/443 lsf -i # any network FD lsf -i 4 # IPv4 only lsf -i 6 # IPv6 only lsf -i TCP # TCP only lsf -i :443 # any protocol on port 443 lsf -i TCP:443 # TCP on port 443
Cyberpunk theming
When stdout is a TTY, column headers swap from utilitarian (COMMAND / PID / USER) to cyberpunk (PROCESS / PRC / H4XOR) and ANSI coloring activates. Piped output is always plain — safe for scripts. 31 named palettes ship in src/theme.rs:
| Palette family | Themes |
|---|---|
| Neon / synth | NeonSprawl (default), NeonNoir, Synthwave, GlitchPop, HoloShift |
| Matrix / phosphor | Matrix, AcidRain, BioHazard, ToxicWaste |
| Blade Runner / noir | BladeRunner, NightCity, Megacorp, Zaibatsu, VoidWalker, Darkwave, DarkSignal |
| Frost / ice | IceBreaker, CyberFrost, GhostWire, ChromeHeart, SteelNerve, QuantumFlux |
| Solar / ember | SolarFlare, PlasmaCore, LaserGrid, RedSector, SakuraDen |
| Data / minimal | Classic, DeepNet, DataStream, Overlock |
Inside any TUI mode press c for the theme chooser (live preview), C for the 6-color editor. Selections persist to ~/.lsofrs.conf via src/config.rs.
Interactive controls (shared across TUI modes)
| Key | Action |
|---|---|
1–9 | Set refresh interval (seconds) |
< / > | Fine-adjust refresh interval (±1s) |
p | Pause / resume data collection |
? / h | Toggle help overlay |
c / C | Theme chooser / editor |
T | Toggle hover tooltips (right-click still works) |
x | Toggle border |
t | Toggle compact / expanded view |
o | Freeze / unfreeze sort order |
/ / 0 | Filter popup (regex) / clear filter |
j / k / ↑ / ↓ | Navigate rows |
F | Pin / unpin selected row |
y | Copy selected row to clipboard |
e | Export current tab to file |
q / Esc / Ctrl-C | Quit |
--tui adds tab navigation (Tab / BackTab / 1–7 / click). --top adds s sort, r reverse, +/- grow/shrink, b bar toggle, d delta toggle.
Architecture in one screen
src/ ├── main.rs # 282 CLI entry, mode dispatch, repeat/leak loops ├── cli.rs # 1,341 clap derive args (38 #[arg], 20 short flags) ├── types.rs # 867 Process, OpenFile, SocketInfo, NetSpec, … ├── darwin.rs # 1,253 macOS libproc FFI (zero-copy repr(C)) ├── linux.rs # 820 /proc reader (parallel per-PID via rayon) ├── freebsd.rs # 835 sysctl + procfs reader ├── filter.rs # 2,812 selection & filter composition (AND/OR, regex) ├── strutil.rs # 76 safe UTF-8 truncation for fixed-width display ├── output.rs # 772 columnar & field formatting, ANSI theming ├── json.rs # serde JSON ├── csv_out.rs # RFC 4180 CSV ├── monitor.rs # 288 classic full-screen alternate-buffer monitor ├── follow.rs # 369 single-PID FD tracker (status transitions) ├── leak.rs # 339 circular-buffer FD-count leak detector ├── delta.rs # iteration-diff engine for change highlighting ├── summary.rs # 904 aggregate stats + bars (+ live TUI mode) ├── tree.rs # 556 process tree w/ FD inheritance ├── tui_app.rs # 564 shared TuiMode trait (ratatui) ├── tui_tabs.rs # 5,312 unified 7-tab TUI (mouse, tooltips) ├── theme.rs # 1,005 31 themes + custom palette support ├── config.rs # TOML config persistence (~/.lsofrs.conf) ├── top.rs # 1,008 live top-N FD dashboard ├── watch.rs # 369 file-open/close watch ├── stale.rs # 313 deleted-file FD finder ├── ports.rs # 450 listening-ports summary ├── pipe_chain.rs # 358 pipe / unix-socket IPC topology └── net_map.rs # 608 connections grouped by remote host lsofrs.1 # 310-line roff man page completions/_lsofrs # 101-line zsh completion (#compdef lsofrs)
Platform support matrix
| OS | Module | Mechanism | Status |
|---|---|---|---|
| macOS / Darwin | src/darwin.rs (1,253 lines) | libproc FFI via zero-copy #[repr(C)] structs matching kernel headers; proc_listpids + proc_pidinfo + proc_pidfdinfo. | Primary platform |
| Linux | src/linux.rs (820 lines) | /proc/<pid>/{stat,status,fd,fdinfo} reader; parallelized per-PID with rayon. | Supported |
| FreeBSD | src/freebsd.rs (835 lines) | sysctl(KERN_PROC_FILEDESC) + /proc when mounted; FreeBSD-specific socket / kqueue decoding. | Supported |
Each platform module is gated by #[cfg(target_os = "…")] in src/lib.rs. The shared Process / OpenFile / SocketInfo shapes live in src/types.rs (867 lines, 11 public structs / enums) — every gather backend returns the same data, so the entire stack above filter.rs is platform-agnostic.
Why it's fast
- Zero-copy FFI. Darwin gather uses raw
#[repr(C)]structs sized to kernel headers — no intermediate parse step, no allocator pressure per-FD. - Rayon work-stealing per-PID. Each PID's FD scan runs on its own worker; CPU count = parallel degree.
- Username caching. v4.8.0 added a
OnceCell-backedgetpwuidcache — previously the hot loop called the syscall once per record. - Precomputed filter paths. The filter predicate canonicalizes its lookups once before the gather pass, not per-FD.
- Streaming output. No intermediate
Vec<Record>for the common path — gather → filter → format → write goes through the same iterator chain. - serde + clap derive. No hand-rolled JSON escaping, no hand-rolled getopt loop.
Net result: 14.2 ms wall-clock for the default lsf invocation vs 169.8 ms for lsof 4.91 on the same machine (hyperfine, ~470 procs, ~5000 FDs).
JSON shape
--json emits an array of records. Each record is the public OpenFile serialization from src/types.rs:
[
{
"pid": 1234,
"ppid": 1,
"pgid": 1234,
"uid": 0,
"user": "root",
"command": "nginx",
"fd": "10u",
"type": "IPv4",
"device": "0x0",
"size_off": null,
"node": "TCP",
"name": "*:8080 (LISTEN)",
"socket": {
"protocol": "TCP",
"local_addr": "0.0.0.0",
"local_port": 8080,
"remote_addr": null,
"remote_port": null,
"state": "LISTEN"
}
}
]
Hard-coded JSON contract tests live in tests/json_shape.rs, tests/json_wrappers.rs, and tests/json_and_csv_contracts.rs (233 #[test] functions in the latter alone) — refactors can't silently break downstream consumers.
Where to go next
- Engineering report — per-subsystem LOC, top-20 files, dependency table, test surface.
- README — full install, every flag, full benchmark table.
- docs.rs/lsofrs — rustdoc for the library crate (
pub mod cli; pub mod types; pub mod filter; …). - lsofrs.1 — 310-line roff man page (also
man lsofrsafter install).