stage 4 (a–d): IPC server, ops, broadcast
Phase 4 first four checkpoints — daemon now serves the wire protocol
specified in IPC.md and broadcasts events to subscribers.
4a IPC server skeleton
UnixListener at $XDG_RUNTIME_DIR/headroom/control.sock, accept
thread, per-connection thread, hello-on-connect, codec
round-trip, 0600 perms with stale-socket detection. Caught and
fixed a sigprocmask ordering bug: block SIGTERM/SIGINT
process-wide BEFORE the IPC accept thread spawns, otherwise it
inherits the unblocked mask and the signal takes the default
disposition before pipewire's signalfd can read it.
4b Read-only ops + shared state
Arc<Mutex<DaemonState>> (parking_lot) for cross-thread daemon
state. RoutingState moved off Rc<RefCell<>>-only and reads
profile from the shared lock. Captures the headroom-processed
node id via the registry. Implements: status, profile.list,
profile.show, route.list, setting.get (serde-roundtrip dotted
lookup), setting.list (flattened).
4c Mutating ops
profile.use (idempotent no-op until 4e ships the disk loader),
profile.reload (empty list till 4e), route.set/unset with
single-app user-rule replace semantics, setting.set with serde
round-trip type-safety, bypass.set. CLI fix:
allow_hyphen_values so 'headroom set foo.bar -0.5' works.
4d Subscriptions + broadcast
Per-connection split into reader thread + writer thread, joined
by a bounded crossbeam_channel<ServerFrame>(64). Broadcaster in
DaemonState fans out events via try_send; bounded queues drop
on overflow with per-(subscriber, topic) counters and a
daemon::overflow flush event piggybacked onto the next
successful publish.
Live events wired: daemon::started, daemon::shutdown,
routing::rule_changed, routing::stream_routed,
routing::stream_removed. CLI 'monitor [topics]' command
subscribes by topic list.
Workspace deps unchanged; uses already-declared crossbeam-channel,
parking_lot. Sinks/SinkInfo gained Default derives.
Tests: 97 passing (28 dsp, 20 ipc, 45 core, 4 client). Clippy clean
at default level under -D warnings.
Remaining Phase 4 punch-list (recommended order):
4e profile TOML loader + hot reload (notify-debouncer-mini)
4h preferred_real_sink tracking
4i target.object routing reliability on real WirePlumber
4f slow AGC loop with ebur128
4g meters publishing
4j auto-promote to default sink (optional flag)
This commit is contained in:
parent
ae83310772
commit
9edd809416
14 changed files with 1889 additions and 78 deletions
|
|
@ -50,7 +50,9 @@ enum Cmd {
|
|||
Set {
|
||||
/// Dotted setting key.
|
||||
key: String,
|
||||
/// New value, JSON-encoded.
|
||||
/// New value, JSON-encoded. Negative numbers (`-0.5` etc.)
|
||||
/// would otherwise be parsed by clap as flags.
|
||||
#[arg(allow_hyphen_values = true)]
|
||||
value: String,
|
||||
},
|
||||
|
||||
|
|
@ -64,8 +66,32 @@ enum Cmd {
|
|||
/// Reload profile files from disk.
|
||||
Reload,
|
||||
|
||||
/// Subscribe to meter ticks and print as line-delimited JSON.
|
||||
Monitor,
|
||||
/// Subscribe to event topics and print as line-delimited JSON.
|
||||
Monitor {
|
||||
/// Topics to subscribe to (comma-separated).
|
||||
/// Defaults to `meters` if none given.
|
||||
#[arg(value_delimiter = ',', default_value = "meters")]
|
||||
topics: Vec<MonitorTopic>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
enum MonitorTopic {
|
||||
Meters,
|
||||
Profile,
|
||||
Routing,
|
||||
Daemon,
|
||||
}
|
||||
|
||||
impl From<MonitorTopic> for Topic {
|
||||
fn from(t: MonitorTopic) -> Self {
|
||||
match t {
|
||||
MonitorTopic::Meters => Topic::Meters,
|
||||
MonitorTopic::Profile => Topic::Profile,
|
||||
MonitorTopic::Routing => Topic::Routing,
|
||||
MonitorTopic::Daemon => Topic::Daemon,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
|
|
@ -221,11 +247,18 @@ fn dispatch(client: &mut Client, cmd: Cmd) -> Result<(), CliError> {
|
|||
let reloaded = client.profile_reload()?;
|
||||
println!("reloaded: {reloaded:?}");
|
||||
}
|
||||
Cmd::Monitor => {
|
||||
client.subscribe(&[Topic::Meters])?;
|
||||
Cmd::Monitor { topics } => {
|
||||
let pw_topics: Vec<Topic> = topics.iter().copied().map(Topic::from).collect();
|
||||
client.subscribe(&pw_topics)?;
|
||||
loop {
|
||||
let ev = client.next_event()?;
|
||||
println!("{}", serde_json::to_string(&ev.data)?);
|
||||
println!(
|
||||
"{} {}/{} {}",
|
||||
chrono_like_now(),
|
||||
ev.topic,
|
||||
ev.event,
|
||||
serde_json::to_string(&ev.data)?,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -247,6 +280,16 @@ enum CliError {
|
|||
Other(String),
|
||||
}
|
||||
|
||||
/// Cheap monotonic-style label for monitor output. Not real
|
||||
/// wall-clock to avoid pulling chrono — `SystemTime` is enough.
|
||||
fn chrono_like_now() -> String {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
let t = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default();
|
||||
format!("{}.{:03}", t.as_secs(), t.subsec_millis())
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
if let Err(e) = run() {
|
||||
eprintln!("headroom: {e}");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue