F2: reapply routing on profile / rule changes

Codex flagged that `profile use`, `profile reload`, `route set`, and
`route unset` updated overlay state and (sometimes) propagated DSP
configs but never asked the registry thread to re-route existing
streams. The new policy only applied to *future* connections;
anything already routed kept its old explicit links until the app
disconnected.

The plumbing was actually already in place from F1 — the bypass
toggle posted `PwCommand::ReevaluateAll`, the registry handled it,
and `reevaluate_all` iterated the `known_streams` cache. This
commit is just the missing call sites: a `post_reevaluate(state)`
helper that reads `state.pw_command_tx` and sends
`ReevaluateAll`, called after each of the four mutating IPC ops.
`execute_reload` (which the profile-watcher also calls) gets the
post too, so editing a TOML on disk now re-routes live streams.

Tests

  All 188 still pass; clippy clean.

Live verification

  Sine flowing through `headroom-processed` while the daemon is
  on the `layer-a-test` profile (default_route = processed):
  - `headroom profile use bypass-all` → pw-cat's explicit link
    flips from processed → Mbox within ~50 ms (one drain tick).
  - `headroom profile use layer-a-test` → flips back to
    processed.
  - Layer A tap link survives both transitions (orthogonal,
    unaffected by bus rerouting — same invariant as F1).

Adjacent issue noted (not in F2 scope)

  `headroom route set <app> <route>` only writes the rule's
  `process_binary` field. Streams that don't advertise
  `application.process.binary` (pw-cat is one) can't be matched
  by this single-field rule even though they have an
  `application.name`. The fix is either to widen `route.set` into
  a smarter "match by app label" verb (which would either need a
  new OR-across-fields matcher kind or a CLI flag to pick which
  field) or to teach the materialiser to produce both
  process_binary AND application_name rules with the same name,
  with the matcher then OR'd. Either way it's a separate UX bug;
  filed as a follow-up.
This commit is contained in:
atagen 2026-05-21 18:40:02 +10:00
parent e0c23ec459
commit 0e718abe27

View file

@ -193,6 +193,7 @@ fn profile_use(id: u64, name: &str, state: &SharedState) -> Response {
publish_profile_changed(&mut s, name);
let control = s.filter_control.clone();
let snap = build_dsp_configs(&s);
post_reevaluate(&s);
drop(s);
push_dsp_update(control.as_ref(), snap);
ok(id, &json!({ "name": name }))
@ -234,6 +235,7 @@ pub(crate) fn execute_reload(
publish_profile_reloaded(&mut s, &report.loaded);
let control = s.filter_control.clone();
let snap = build_dsp_configs(&s);
post_reevaluate(&s);
drop(s);
push_dsp_update(control.as_ref(), snap);
Ok(report)
@ -245,6 +247,7 @@ fn route_set(id: u64, app: &str, to: Route, state: &SharedState) -> Response {
Ok(()) => {
tracing::info!(app, ?to, "route.set applied");
publish_rule_changed(&mut s);
post_reevaluate(&s);
drop(s);
ok(id, &Value::Null)
}
@ -258,6 +261,7 @@ fn route_unset(id: u64, app: &str, state: &SharedState) -> Response {
Ok(()) => {
tracing::info!(app, "route.unset applied");
publish_rule_changed(&mut s);
post_reevaluate(&s);
drop(s);
ok(id, &Value::Null)
}
@ -271,6 +275,25 @@ fn publish_rule_changed(state: &mut crate::state::DaemonState) {
}
}
/// Ask the PipeWire main loop to re-run `routing::evaluate` against
/// every known stream. Called after any IPC mutation that changes
/// the inputs to that decision: active profile, profile contents
/// reloaded from disk, or a `route.set` / `route.unset` overlay
/// edit. Without this, the new policy only applies to *future*
/// streams; everything already routed keeps its old links until the
/// app reconnects. A stale or duplicate post is harmless — the
/// handler reads current state at apply time and is idempotent
/// when nothing changed.
fn post_reevaluate(state: &crate::state::DaemonState) {
let Some(tx) = state.pw_command_tx.as_ref() else {
tracing::debug!("no PipeWire command channel; reevaluation skipped (test mode)");
return;
};
if tx.send(PwCommand::ReevaluateAll).is_err() {
tracing::warn!("PipeWire command channel closed; reevaluation lost");
}
}
fn publish_profile_changed(state: &mut crate::state::DaemonState, name: &str) {
if let Ok(event) = Event::new(Topic::Profile, "used", &json!({ "name": name })) {
state.broadcaster.publish(Topic::Profile, event);