feat(qbz-audio): add device-reservation module skeleton with parsers

Adds the new module that will host the org.freedesktop.ReserveDevice1
D-Bus client. This commit only ships the public surface, the parser
helpers (hw:N -> Audio<N> bus name, hw:CARD=name resolution) and unit
tests for them. acquire() currently returns a degraded guard; D-Bus
implementation lands in the next commit.

cc
This commit is contained in:
vicrodh committed 2026-05-07 08:57:31 -06:00
1 parent a705e4f297
commit 4bc4213b97
7 files changed
+259

No files matched your search

+3
View File
@@ -2921,6 +2921,7 @@ dependencies = [
"symphonia",
"thiserror 2.0.18",
"tokio",
"zbus",
]
[[package]]
@@ -4387,6 +4388,7 @@ dependencies = [
"signal-hook-registry",
"socket2 0.6.3",
"tokio-macros",
"tracing",
"windows-sys 0.61.2",
]
@@ -5664,6 +5666,7 @@ dependencies = [
"serde_repr",
"sha1",
"static_assertions",
"tokio",
"tracing",
"uds_windows",
"windows-sys 0.52.0",
+4
View File
@@ -41,6 +41,10 @@ dirs = "6"
alsa = "0.10"
# alsa-sys: direct FFI for snd_lib_error_set_handler (custom error routing)
alsa-sys = "0.3"
# zbus for org.freedesktop.ReserveDevice1 D-Bus client (device reservation).
# Pinned to v4 to share the transitive copy already pulled in by
# secret-service/souvlaki and avoid duplicate zbus compiles.
zbus = { version = "4", default-features = false, features = ["tokio"] }
[target.'cfg(target_os = "macos")'.dependencies]
# CoreAudio for device probing, sample rate switching, and (future) hog mode / exclusive mode
@@ -0,0 +1,18 @@
//! D-Bus device reservation via org.freedesktop.ReserveDevice1.
//!
//! Acquires the bus name org.freedesktop.ReserveDevice1.Audio<N> for the
//! card index N, signalling to PulseAudio/PipeWire/WirePlumber that another
//! application owns the device exclusively. Released on Drop.
//!
//! See `qbz-nix-docs/specs/2026-05-07-alsa-exclusive-hardening-design.md`
//! for the full protocol specification and lifetime model.
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "linux")]
pub use linux::*;
#[cfg(not(target_os = "linux"))]
mod stub;
#[cfg(not(target_os = "linux"))]
pub use stub::*;
@@ -0,0 +1,193 @@
//! Linux implementation of `DeviceReservation`.
//!
//! Task 1 ships the public surface, parser helpers, and unit tests for them.
//! The actual zbus client is wired in Task 2 — for now `acquire()` validates
//! the device string and returns a degraded guard so call sites can be
//! plumbed without breaking the build.
use std::fmt;
#[derive(Debug)]
pub struct DeviceReservation {
state: ReservationState,
}
#[derive(Debug)]
#[allow(dead_code)] // `Active` is constructed in Task 2 once the zbus client lands.
enum ReservationState {
Active {
// Filled in Task 2 with the zbus connection + bus name we own.
_placeholder: (),
},
Degraded,
}
impl DeviceReservation {
/// Acquire a reservation for the given ALSA hw: device string.
///
/// Returns `Ok(guard)` on success. On D-Bus unavailability or any other
/// non-fatal failure path, the implementation in Task 2 will return a
/// degraded guard so playback proceeds without reservation. For now the
/// device string is validated and a degraded guard is always returned.
pub fn acquire(hw_device: &str, _app_device_name: &str) -> Result<Self, ReservationError> {
// Validate the device string. Real D-Bus acquisition lands in Task 2.
let _card = parse_card_index(hw_device)?;
Ok(Self {
state: ReservationState::Degraded,
})
}
/// Whether this guard currently holds an active D-Bus reservation.
pub fn is_active(&self) -> bool {
matches!(self.state, ReservationState::Active { .. })
}
}
impl Drop for DeviceReservation {
fn drop(&mut self) {
// Real release lands in Task 2.
}
}
#[derive(Debug)]
pub enum ReservationError {
InvalidDevice(String),
HigherPriorityHolder {
holder_name: String,
holder_priority: i32,
},
DbusError(String),
}
impl fmt::Display for ReservationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidDevice(s) => write!(f, "invalid ALSA device string: {}", s),
Self::HigherPriorityHolder {
holder_name,
holder_priority,
} => write!(
f,
"device reserved by '{}' at priority {}",
holder_name, holder_priority
),
Self::DbusError(s) => write!(f, "D-Bus error: {}", s),
}
}
}
impl std::error::Error for ReservationError {}
/// Parse an ALSA device string and return the kernel card index.
///
/// Accepts: `"hw:0"`, `"hw:0,0"`, `"hw:1,0"`, `"plughw:1,0"`,
/// `"hw:CARD=DacMagic"`, `"hw:CARD=DacMagic,DEV=0"`.
pub(crate) fn parse_card_index(hw_device: &str) -> Result<u32, ReservationError> {
let trimmed = hw_device.trim();
let after_prefix = trimmed
.strip_prefix("hw:")
.or_else(|| trimmed.strip_prefix("plughw:"))
.ok_or_else(|| ReservationError::InvalidDevice(hw_device.to_string()))?;
let card_part = after_prefix.split(',').next().unwrap_or("");
let card_part = card_part.trim();
if card_part.is_empty() {
return Err(ReservationError::InvalidDevice(hw_device.to_string()));
}
if let Some(name) = card_part.strip_prefix("CARD=") {
resolve_card_index_by_name(name)
} else {
card_part
.parse::<u32>()
.map_err(|_| ReservationError::InvalidDevice(hw_device.to_string()))
}
}
/// Resolve a symbolic ALSA card name (e.g., `"DacMagic"`) to its kernel index
/// by iterating over `alsa::card::Iter`.
fn resolve_card_index_by_name(name: &str) -> Result<u32, ReservationError> {
for card in alsa::card::Iter::new() {
let card = card.map_err(|e| ReservationError::DbusError(e.to_string()))?;
let id = card.get_name().unwrap_or_default();
if id == name {
return Ok(card.get_index() as u32);
}
}
Err(ReservationError::InvalidDevice(format!(
"ALSA card '{}' not found",
name
)))
}
/// Format the well-known D-Bus bus name for a given ALSA card index.
#[allow(dead_code)] // Used by the zbus client in Task 2.
pub(crate) fn bus_name_for_card(card_index: u32) -> String {
format!("org.freedesktop.ReserveDevice1.Audio{}", card_index)
}
/// Format the D-Bus object path for a given ALSA card index.
#[allow(dead_code)] // Used by the zbus client in Task 2.
pub(crate) fn object_path_for_card(card_index: u32) -> String {
format!("/org/freedesktop/ReserveDevice1/Audio{}", card_index)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_card_index_basic() {
assert_eq!(parse_card_index("hw:0").unwrap(), 0);
assert_eq!(parse_card_index("hw:1,0").unwrap(), 1);
assert_eq!(parse_card_index("plughw:2,0").unwrap(), 2);
assert_eq!(parse_card_index("hw:99,3").unwrap(), 99);
}
#[test]
fn parse_card_index_rejects_garbage() {
assert!(matches!(
parse_card_index("default"),
Err(ReservationError::InvalidDevice(_))
));
assert!(matches!(
parse_card_index("hw:"),
Err(ReservationError::InvalidDevice(_))
));
assert!(matches!(
parse_card_index(""),
Err(ReservationError::InvalidDevice(_))
));
}
#[test]
fn bus_name_format() {
assert_eq!(
bus_name_for_card(0),
"org.freedesktop.ReserveDevice1.Audio0"
);
assert_eq!(
bus_name_for_card(7),
"org.freedesktop.ReserveDevice1.Audio7"
);
assert_eq!(
bus_name_for_card(99),
"org.freedesktop.ReserveDevice1.Audio99"
);
}
#[test]
fn object_path_format() {
assert_eq!(
object_path_for_card(0),
"/org/freedesktop/ReserveDevice1/Audio0"
);
}
#[test]
fn degraded_guard_reports_inactive() {
let g = DeviceReservation::acquire("hw:0,0", "test").unwrap();
assert!(!g.is_active());
}
}
@@ -0,0 +1,37 @@
//! Non-Linux stub for `DeviceReservation`.
//!
//! The org.freedesktop.ReserveDevice1 protocol is a Linux/D-Bus convention.
//! On macOS and Windows, `acquire()` always succeeds with a degraded guard
//! so that call sites stay portable; `is_active()` always returns `false`.
#[derive(Debug)]
pub struct DeviceReservation;
impl DeviceReservation {
/// Acquire a reservation for the given ALSA hw: device string.
///
/// On non-Linux platforms this always returns a degraded guard (no-op).
pub fn acquire(_hw_device: &str, _app_device_name: &str) -> Result<Self, ReservationError> {
Ok(Self)
}
/// Whether this guard holds an active D-Bus reservation.
///
/// Always `false` on non-Linux platforms.
pub fn is_active(&self) -> bool {
false
}
}
#[derive(Debug)]
pub enum ReservationError {
Unsupported,
}
impl std::fmt::Display for ReservationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "device reservation is not supported on this platform")
}
}
impl std::error::Error for ReservationError {}
+2
View File
@@ -39,6 +39,7 @@ pub mod alsa_direct;
pub mod coreaudio_direct;
pub mod analysis;
pub mod analyzer_tap;
pub mod device_reservation;
pub mod diagnostic;
pub mod dynamic_amplify;
pub mod loudness;
@@ -61,6 +62,7 @@ pub use backend::{
AlsaDirectError, AlsaPlugin, AudioBackend, AudioBackendType, AudioDevice, BackendConfig,
BackendManager, BackendResult, BitPerfectMode,
};
pub use device_reservation::{DeviceReservation, ReservationError};
pub use diagnostic::{AudioDiagnostic, BitDepthResult, DiagnosticSource};
pub use dynamic_amplify::DynamicAmplify;
pub use loudness::{calculate_gain_factor, db_to_linear, extract_replaygain, ReplayGainData};
+2
View File
@@ -4888,6 +4888,7 @@ dependencies = [
"symphonia",
"thiserror 2.0.18",
"tokio",
"zbus 4.4.0",
]
[[package]]
@@ -9293,6 +9294,7 @@ dependencies = [
"serde_repr",
"sha1",
"static_assertions",
"tokio",
"tracing",
"uds_windows",
"windows-sys 0.52.0",