Third review round found three more blocking P2s, and they shared one root cause: `pactl`'s text output cannot carry the guarantees repair was claiming. All three are closed by talking to the protocol instead. New dependency taken with the user's explicit sign-off after vetting. **Record boundaries were unprovable.** `pactl list short modules` prints a module's argument raw into a tab/newline-delimited format with no escaping. A *genuine* module whose argument contains a newline renders a first line that reads byte-exactly like one of our canonical forms, with the remainder dropped as an unparseable continuation — no forged index, so the duplicate-index check could not see it. Repair would have classified and unloaded a module it never saw in full. **Field-confirmed on the live server**, because this needed no adversary: loading a loopback whose argument is canonical-then-newline-then-`remix=false` (a real loopback option) produces exactly that listing. A tab in the same position is worse: it hid a sink reference from the gate that protects a still-referenced sink. **Index and argument could be mis-paired.** The listing carrying exact arguments (`-f json`) carries no index at all on pactl 17; the one carrying the index cannot carry the argument faithfully. Correlating them by position — which the previous commit did — is unsound whenever module names repeat: another client loading one module and unloading another between the two calls leaves counts and names aligned while every argument has shifted by one, so a foreign module inherits a canonical fingerprint. The name check cannot see it and the retry never fires. **Locality was a guess.** `PULSE_SERVER` is a fallback *list*, so `unix:/missing tcp:remote:4713` passes any "starts with unix:" test and then connects to another machine, where local pids mean nothing and a live remote host's modules look dead. A remote server can also be selected by client config with the variable unset entirely. New `repair/introspect.rs` owns one verified-local connection: `pa_module_info` gives index, name and exact argument in a single record, `pa_context_is_local()` answers locality about the connection actually established, and unloading goes back through that same connection so listing and destruction cannot disagree about which server they mean. It holds no policy beyond refusing the wrong server; every decision stays in the pure planner. ⚠️ **The field test caught a real bug that no unit test could have.** The first version did its work correctly and then aborted on the way out: Assertion '!e->dead' failed at ../pulseaudio/src/pulse/mainloop.c:207, function mainloop_io_free(). Aborting. SIGABRT, core dumped, exit 134 — a fully successful repair reporting failure to its caller. Cause: Rust drops fields in declaration order and the context's teardown frees IO events living in the mainloop, which I had declared first. **This is exactly the invariant phase 0b exists for, met again one layer down.** Fixed, and then hardened past the fix: `Drop` explicitly takes and destroys the context before the mainloop, so the ordering no longer depends on where the fields are written. Liveness keeps its `NSpid`/container checks but the claim is corrected: `NSpid > 1` means "definitely nested", while `NSpid == 1` is NOT proof of the initial namespace — its leftmost value is relative to the procfs that was mounted, so a nested namespace with its own `/proc` reports one entry legitimately. These are negative signals that fail closed, not a proof of trustworthy pids. Closing that properly needs modules to carry an owner token (machine/boot plus pid-namespace identity), which changes what pixelpass writes into the graph and how far back `--repair` can clean up: recorded as a design decision, not guessed at. libpulse-binding 2.30.1 vetted before use: MIT/Apache-2.0, 5.5M downloads, 3 new crates total, build script does nothing but probe pkg-config, no network or subprocess use anywhere in the sources, and all three historical RustSec advisories (2018-0020, 2018-0021, 2019-0038) were fixed by 2.6.0. The reasoning is recorded in Cargo.toml beside the dependency. 247 tests, clippy clean, fmt clean apart from the pre-existing taint/tests.rs:2683. The text parser's tests are gone with the parser; the liveness probe keeps its own, and the live field gates — A/B orphan removal, the reference/unrecognised fixture, and the newline fixture — all pass with exit 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
104 lines
5.7 KiB
TOML
104 lines
5.7 KiB
TOML
[package]
|
|
name = "pixelpass"
|
|
version = "0.1.0"
|
|
edition = "2024"
|
|
description = "P2P screen sharing CLI over iroh"
|
|
license = "MIT OR Apache-2.0"
|
|
publish = false
|
|
|
|
# Debian/Ubuntu packaging (cargo-deb). Headless default build (no `gui` feature) —
|
|
# that is exactly what peerspeak spawns as a child. Runtime shared-lib deps
|
|
# (libpipewire, libc, …) are resolved by dpkg-shlibdeps via `depends = "$auto"`.
|
|
# Build inside a Debian/Ubuntu distrobox, then `cargo deb --no-build`.
|
|
[package.metadata.deb]
|
|
maintainer = "mollusk <jitty+lc1iz0dc@protonmail.com>"
|
|
section = "net"
|
|
priority = "optional"
|
|
# $auto covers linked shared libs (dpkg-shlibdeps). The GStreamer capture stack
|
|
# and pactl are invoked as *subprocesses* (gst-launch-1.0 / gst-inspect-1.0 /
|
|
# pactl), so shlibdeps can't see them — list them explicitly or a fresh Ubuntu
|
|
# host bails at `deps::check_host_binaries` before emitting its ticket. Covers
|
|
# both backends: pipewiresrc (Wayland), ximagesrc (X11, in plugins-good), the
|
|
# VAAPI + software H.264 encoders, the AAC/TS mux tail, and the PulseAudio src.
|
|
depends = "$auto, gstreamer1.0-tools, gstreamer1.0-plugins-base, gstreamer1.0-plugins-good, gstreamer1.0-plugins-bad, gstreamer1.0-plugins-ugly, gstreamer1.0-libav, gstreamer1.0-pipewire, gstreamer1.0-pulseaudio, pulseaudio-utils, x11-utils"
|
|
recommends = "mpv"
|
|
extended-description = "Peer-to-peer screen sharing over iroh (QUIC). Companion to peerspeak: shares a window or screen directly to a peer with no central server, driven via the CLI and its JSON event stream."
|
|
assets = [
|
|
["target/release/pixelpass", "usr/bin/", "755"],
|
|
]
|
|
|
|
[[bin]]
|
|
name = "pixelpass"
|
|
path = "src/main.rs"
|
|
|
|
[dependencies]
|
|
iroh = "1.0.2"
|
|
tokio = { version = "1", features = ["macros", "rt-multi-thread", "io-util", "net", "signal", "process", "sync", "time"] }
|
|
tokio-util = { version = "0.7", features = ["io"] }
|
|
clap = { version = "4", features = ["derive"] }
|
|
anyhow = "1"
|
|
thiserror = "2"
|
|
tracing = "0.1"
|
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
|
nix = { version = "0.30", features = ["signal", "process"] }
|
|
serde = { version = "1", features = ["derive"] }
|
|
serde_json = "1"
|
|
directories = "5"
|
|
ashpd = { version = "0.9", default-features = false, features = ["tokio"] }
|
|
pipewire = "0.9"
|
|
# `--repair` reads and unloads Pulse modules through libpulse introspection rather
|
|
# than by parsing `pactl` output. `pa_module_info` carries index, name and the exact
|
|
# argument in one record, and `pa_context_is_local()` answers whether the server we
|
|
# actually reached is local — neither of which the text listings can do (an argument
|
|
# may contain tabs and newlines that the short format cannot escape, the JSON
|
|
# listing carries no module index at all, and `PULSE_SERVER` is a fallback list, so
|
|
# it never proved locality). Vetted at 2.30.1: MIT/Apache-2.0, no build script
|
|
# beyond a pkg-config probe, no network or subprocess use, and all three historical
|
|
# RustSec advisories (2018-0020, 2018-0021, 2019-0038) fixed by 2.6.0.
|
|
libpulse-binding = "2.30"
|
|
x11rb = { version = "0.13", default-features = false, features = ["allow-unsafe-code"] }
|
|
uuid = { version = "1", features = ["v4"] }
|
|
iroh-tickets = "1.0.0"
|
|
dialoguer = { version = "0.12", default-features = false }
|
|
arboard = { version = "3", default-features = false, features = ["wayland-data-control"] }
|
|
ureq = { version = "3", default-features = false, features = ["rustls"] }
|
|
toml = "1"
|
|
chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] }
|
|
eframe = { version = "0.34.2", default-features = false, features = ["glow", "default_fonts", "wayland", "x11"], optional = true }
|
|
# Desktop notifications on viewer join/leave. Default features give the
|
|
# pure-Rust zbus backend (no system libdbus, no image crate).
|
|
notify-rust = { version = "4", optional = true }
|
|
# System-tray icon (StatusNotifierItem over D-Bus). Pure-Rust, riding the same
|
|
# zbus stack notify-rust already pulls — no GTK, no libappindicator/C libdbus.
|
|
ksni = { version = "0.3", optional = true }
|
|
# Hand-rolled windowing stack for the GUI (replaces eframe::run_native) so we
|
|
# can drop the OS window on "hide to tray" — the only way to truly hide a
|
|
# toplevel on Wayland — and recreate it on Show. All of these are already pulled
|
|
# in transitively by eframe; making them direct adds no new crates to vet.
|
|
# eframe is kept for its egui re-export + icon_data PNG decoder. egui_glow needs
|
|
# its (non-default) `winit` feature for the `EguiGlow` integration type; eframe
|
|
# pulls egui_glow but without that feature, so we enable it here.
|
|
egui_glow = { version = "0.34.2", default-features = false, features = ["winit", "wayland", "x11"], optional = true }
|
|
# winit's default set minus `wayland-csd-adwaita`: KWin (and most desktop
|
|
# compositors) draw server-side decorations, and eframe never enabled CSD
|
|
# either, so dropping it keeps the dependency tree identical to before (no
|
|
# sctk-adwaita / tiny-skia / ttf-parser pulled in just for a fallback titlebar).
|
|
winit = { version = "0.30", default-features = false, features = ["rwh_06", "x11", "wayland", "wayland-dlopen"], optional = true }
|
|
glutin = { version = "0.32", optional = true }
|
|
glutin-winit = { version = "0.5", optional = true }
|
|
# QR-encode the host ticket so a phone (or a second laptop with a webcam) can
|
|
# pick it up without typing 140 chars. default-features = false to skip the
|
|
# `image` crate dep tree — we render the modules to an `egui::ColorImage`
|
|
# directly.
|
|
qrcode = { version = "0.14", default-features = false, optional = true }
|
|
|
|
[profile.release]
|
|
lto = "thin"
|
|
codegen-units = 1
|
|
strip = "symbols"
|
|
|
|
[features]
|
|
# Opt-in graphical front-end (pixelpass --gui). Default-off so the headless
|
|
# build never pulls the GUI toolkit tree.
|
|
gui = ["dep:eframe", "dep:notify-rust", "dep:ksni", "dep:egui_glow", "dep:winit", "dep:glutin", "dep:glutin-winit", "dep:qrcode"]
|