Runs phases 2-4 against the live PipeWire graph on every registry event and
reports the complete eligible/excluded candidate partition with stable reason
codes. Creates no links, loads no modules, changes no routing.
Impl plan §5. Two entry points behind the hidden PIXELPASS_AUDIO_AUDIT=1
trigger: inside a real `pixelpass host` run (the plan-literal reading, proves
the path phase 6 will mutate), and a hidden `--audit-audio` standalone mode
with no iroh endpoint or capture pipeline, which is what drives the §5.1
matrix.
The recompute runs inline on the observer thread via a new ProjectionSink
hook, once per applied event. Polling `latest()` was rejected: it coalesces,
and phase 4 detects a module unload by observing the empty gap before the next
module appears — with indices reused verbatim (v3.4 §5.2 correction 3), a
missed gap aliases a fresh module onto a dead identity. Running inline is what
makes phase 4's "one observe per graph event" contract true, and it puts the
cost where O5 can measure it.
Split as usual: the auditor and the metrics are pure and unit-tested; the
clock, the writer and the env parsing are the thin edge in `sink`/`run`.
- audit/mod.rs Auditor: AEC validator + taint engine + record building.
The AEC gate and the engine's own reasons stay
distinguishable — a shut gate must not erase the reason codes
the §5.1 rows assert.
- audit/metrics.rs O5: event rate, bucketed recompute distribution + exact
max, busy fraction, and a documented lower-bound queueing
proxy (libpipewire exposes no queue depth).
- audit/sink.rs JSON Lines to stderr, or PIXELPASS_AUDIO_AUDIT_FILE. Never
stdout — peerspeak parses that stream.
- audit/run.rs Env parsing; a malformed AEC value is fatal, matching phase
4's rule that it must not silently become "no AEC".
Observer gains `EventKind` (derived from RegEvent, so a consumer's view of
"was this a real graph change?" cannot disagree with the model's) and
`Projection::readiness`, which distinguishes the three ways graph_ready can be
false. taint::fixture is now pub(crate) so audit tests share one graph
vocabulary with the taint tests.
33 new tests, 178 green, clippy -D warnings and fmt clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
108 lines
3.3 KiB
Rust
108 lines
3.3 KiB
Rust
mod cli;
|
|
mod common;
|
|
mod doctor;
|
|
#[cfg(feature = "gui")]
|
|
mod gui;
|
|
mod host;
|
|
mod interactive;
|
|
mod repair;
|
|
mod viewer;
|
|
|
|
use anyhow::Result;
|
|
use clap::Parser;
|
|
use cli::Cli;
|
|
use iroh_tickets::endpoint::EndpointTicket;
|
|
use tracing_subscriber::EnvFilter;
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
let cli = Cli::parse();
|
|
init_tracing(cli.verbose);
|
|
|
|
if matches!(cli.output, Some(cli::OutputFormat::Json)) {
|
|
common::output::set_json(true);
|
|
}
|
|
|
|
if cli.gui {
|
|
#[cfg(feature = "gui")]
|
|
{
|
|
return gui::run(cli.relay);
|
|
}
|
|
#[cfg(not(feature = "gui"))]
|
|
{
|
|
anyhow::bail!(
|
|
"this binary was built without GUI support. Rebuild with \
|
|
`cargo build --release --features gui` to use --gui."
|
|
);
|
|
}
|
|
}
|
|
|
|
// Diagnostics run before pipewire::init() (they don't need it) and work
|
|
// regardless of the `gui` feature, so a headless tester can probe their box.
|
|
if cli.doctor {
|
|
let relay = common::endpoint::relay_override(cli.relay.as_deref());
|
|
return doctor::run(relay).await;
|
|
}
|
|
|
|
// libpipewire requires global init before any pw_* call. Idempotent;
|
|
// safe to call even when the per-app audio thread never spawns.
|
|
pipewire::init();
|
|
|
|
if cli.repair {
|
|
return repair::run().await;
|
|
}
|
|
|
|
// Read-only diagnostic: observe the graph, report what the audio-exclusion
|
|
// engine concludes, create nothing. Placed before the host/viewer dispatch
|
|
// because it is neither — it shares no screen and connects to no peer.
|
|
if cli.audit_audio {
|
|
return host::audit::run::run_standalone().await;
|
|
}
|
|
|
|
if cli.reconfigure {
|
|
return interactive::run_reconfigure().await;
|
|
}
|
|
|
|
if cli.host {
|
|
if cli.ticket.is_some() {
|
|
anyhow::bail!(
|
|
"--host and a ticket argument are mutually exclusive: --host shares your \
|
|
screen, a ticket views someone else's."
|
|
);
|
|
}
|
|
return host::run(cli.into_host_opts(false)).await;
|
|
}
|
|
|
|
match cli.ticket.as_deref() {
|
|
Some(s) => {
|
|
let ticket: EndpointTicket = s.parse().map_err(|e| {
|
|
anyhow::anyhow!(
|
|
"argument doesn't look like a pixelpass ticket ({e}).\n\
|
|
Run with no arguments for the interactive menu, or pass a ticket to view."
|
|
)
|
|
})?;
|
|
viewer::run(ticket, cli.into_viewer_opts(false)).await
|
|
}
|
|
None => interactive::run(cli).await,
|
|
}
|
|
}
|
|
|
|
fn init_tracing(verbose: bool) {
|
|
let default = if verbose {
|
|
"pixelpass=trace,iroh=info"
|
|
} else {
|
|
"pixelpass=info,iroh=warn"
|
|
};
|
|
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default));
|
|
// Tracing MUST write to stderr. `tracing_subscriber::fmt()` defaults its
|
|
// writer to stdout, but with `--output json` stdout carries the JSON event
|
|
// stream the `--gui` front-end parses (see `common::output`) — logging there
|
|
// interleaves log lines into that stream (corrupting events and starving the
|
|
// GUI's stderr-tail diagnostics). Pin it to stderr to honor that contract.
|
|
tracing_subscriber::fmt()
|
|
.with_writer(std::io::stderr)
|
|
.with_env_filter(filter)
|
|
.with_target(false)
|
|
.init();
|
|
}
|