Skip to content

Commit 4365a85

Browse files
committed
fix(rdpsnd-native)!: replace anyhow with typed RdpsndNativeError
Brings ironrdp-rdpsnd-native into line with the workspace convention from ARCHITECTURE.md that libraries provide concrete error types rather than anyhow::Result. Defines RdpsndNativeErrorKind with four variants covering the failure modes in DecodeStream::new (UnsupportedFormat, OpusInit, AudioDevice, StreamBuild). Display and core::error::Error impls are hand-rolled per the post-#1264 pattern in ironrdp-pdu. DecodeStream::new now returns RdpsndNativeResult<Self> instead of anyhow::Result<Self>. Foreign error sources (opus2::Error, cpal::DefaultStreamConfigError, cpal::BuildStreamError) are attached via Error::with_source so the cause chain remains reachable through core::error::Error::source(). anyhow moves from [dependencies] to [dev-dependencies] for the cpal example binary. The internal log of construction failures switches from "{e:#}" to e.report() to preserve cause-chain output now that anyhow's alternate-format chain printing is no longer involved. Signed-off-by: Greg Lamberson <greg@lamco.io>
1 parent 0282d18 commit 4365a85

5 files changed

Lines changed: 82 additions & 19 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ironrdp-rdpsnd-native/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,15 @@ default = ["opus"]
2020
opus = ["dep:opus2", "dep:bytemuck"]
2121

2222
[dependencies]
23-
anyhow = "1"
2423
bytemuck = { version = "1.24", optional = true }
2524
cpal = "0.17"
25+
ironrdp-error = { path = "../ironrdp-error", version = "0.1", features = ["std"] } # public
2626
ironrdp-rdpsnd = { path = "../ironrdp-rdpsnd", version = "0.7" } # public
2727
opus2 = { version = "0.4", optional = true, features = ["bundled"] }
2828
tracing = { version = "0.1", features = ["log"] }
2929

3030
[dev-dependencies]
31+
anyhow = "1"
3132
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
3233

3334
[lints]

crates/ironrdp-rdpsnd-native/src/cpal.rs

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ use std::sync::Arc;
55
use std::sync::mpsc::{self, Receiver, Sender};
66
use std::thread::{self, JoinHandle};
77

8-
use anyhow::{Context as _, bail};
98
use cpal::traits::{DeviceTrait as _, HostTrait as _};
109
use cpal::{SampleFormat, Stream, StreamConfig};
10+
use ironrdp_error::bail;
1111
use ironrdp_rdpsnd::client::RdpsndClientHandler;
1212
use ironrdp_rdpsnd::pdu::{AudioFormat, PitchPdu, VolumePdu, WaveFormat};
1313
use tracing::{debug, error, info, warn};
1414

15+
use crate::error::{RdpsndNativeError, RdpsndNativeErrorKind, RdpsndNativeResult};
16+
1517
#[derive(Debug)]
1618
pub struct RdpsndBackend {
1719
// Unfortunately, Stream is not `Send`, so we move it to a separate thread.
@@ -91,7 +93,7 @@ impl RdpsndClientHandler for RdpsndBackend {
9193
let stream = match DecodeStream::new(&format, rx) {
9294
Ok(stream) => stream,
9395
Err(e) => {
94-
error!(error = format!("{e:#}"));
96+
error!(error = %e.report());
9597
return;
9698
}
9799
};
@@ -138,18 +140,23 @@ pub struct DecodeStream {
138140
}
139141

140142
impl DecodeStream {
141-
pub fn new(rx_format: &AudioFormat, mut rx: Receiver<Vec<u8>>) -> anyhow::Result<Self> {
143+
pub fn new(rx_format: &AudioFormat, mut rx: Receiver<Vec<u8>>) -> RdpsndNativeResult<Self> {
142144
let mut dec_thread = None;
143145
match rx_format.format {
144146
#[cfg(feature = "opus")]
145147
WaveFormat::OPUS => {
146148
let chan = match rx_format.n_channels {
147149
1 => opus2::Channels::Mono,
148150
2 => opus2::Channels::Stereo,
149-
_ => bail!("unsupported #channels for Opus"),
151+
_ => bail!(
152+
"unsupported channel count for Opus",
153+
RdpsndNativeErrorKind::UnsupportedFormat,
154+
),
150155
};
151156
let (dec_tx, dec_rx) = mpsc::channel();
152-
let mut dec = opus2::Decoder::new(rx_format.n_samples_per_sec, chan)?;
157+
let mut dec = opus2::Decoder::new(rx_format.n_samples_per_sec, chan).map_err(|e| {
158+
RdpsndNativeError::new("creating Opus decoder", RdpsndNativeErrorKind::OpusInit).with_source(e)
159+
})?;
153160
dec_thread = Some(thread::spawn(move || {
154161
while let Ok(pkt) = rx.recv() {
155162
let nb_samples = match dec.get_nb_samples(&pkt) {
@@ -181,23 +188,31 @@ impl DecodeStream {
181188
rx = dec_rx;
182189
}
183190
WaveFormat::PCM => {}
184-
_ => bail!("audio format not supported"),
191+
_ => bail!(
192+
"matching server-requested wave format",
193+
RdpsndNativeErrorKind::UnsupportedFormat,
194+
),
185195
}
186196

187197
let sample_format = match rx_format.bits_per_sample {
188198
8 => SampleFormat::U8,
189199
16 => SampleFormat::I16,
190-
_ => {
191-
bail!("only PCM 8/16 bits formats supported");
192-
}
200+
_ => bail!(
201+
"only PCM 8/16 bit formats supported",
202+
RdpsndNativeErrorKind::UnsupportedFormat,
203+
),
193204
};
194205

195206
let host = cpal::default_host();
196-
let device = host.default_output_device().context("no default output device")?;
197-
let _supported_configs_range = device
198-
.supported_output_configs()
199-
.context("no supported output config")?;
200-
let default_config = device.default_output_config()?;
207+
let device = host
208+
.default_output_device()
209+
.ok_or_else(|| RdpsndNativeError::new("no default output device", RdpsndNativeErrorKind::AudioDevice))?;
210+
let _supported_configs_range = device.supported_output_configs().map_err(|e| {
211+
RdpsndNativeError::new("no supported output configs", RdpsndNativeErrorKind::AudioDevice).with_source(e)
212+
})?;
213+
let default_config = device.default_output_config().map_err(|e| {
214+
RdpsndNativeError::new("default output config", RdpsndNativeErrorKind::AudioDevice).with_source(e)
215+
})?;
201216
debug!(?default_config);
202217

203218
let mut rx = RxBuffer::new(rx);
@@ -219,7 +234,9 @@ impl DecodeStream {
219234
|error| error!(%error),
220235
None,
221236
)
222-
.context("failed to setup output stream")?;
237+
.map_err(|e| {
238+
RdpsndNativeError::new("building cpal output stream", RdpsndNativeErrorKind::StreamBuild).with_source(e)
239+
})?;
223240

224241
Ok(Self {
225242
_dec_thread: dec_thread,
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//! Typed error types for `ironrdp-rdpsnd-native`.
2+
3+
/// Categorises failures in `ironrdp-rdpsnd-native` operations.
4+
///
5+
/// Bug-shaped conditions are intentionally absent: misuse of this crate's
6+
/// public API should panic or trip `debug_assert!`, not return `Err`.
7+
#[derive(Debug)]
8+
#[non_exhaustive]
9+
pub enum RdpsndNativeErrorKind {
10+
/// Server requested an audio format outside the supported set (wave
11+
/// format, channel count, or bit depth).
12+
UnsupportedFormat,
13+
/// The Opus decoder failed to initialise. Source carries the underlying
14+
/// `opus2::Error` when available.
15+
OpusInit,
16+
/// No usable audio output device or no supported output configuration
17+
/// for the requested format. Source carries the underlying `cpal` error
18+
/// when available.
19+
AudioDevice,
20+
/// The `cpal` output stream could not be built. Source carries the
21+
/// underlying `cpal::BuildStreamError`.
22+
StreamBuild,
23+
}
24+
25+
impl core::fmt::Display for RdpsndNativeErrorKind {
26+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
27+
match self {
28+
Self::UnsupportedFormat => write!(f, "unsupported audio format"),
29+
Self::OpusInit => write!(f, "Opus decoder initialisation"),
30+
Self::AudioDevice => write!(f, "audio output device"),
31+
Self::StreamBuild => write!(f, "output audio stream build"),
32+
}
33+
}
34+
}
35+
36+
impl core::error::Error for RdpsndNativeErrorKind {}
37+
38+
pub type RdpsndNativeError = ironrdp_error::Error<RdpsndNativeErrorKind>;
39+
pub type RdpsndNativeResult<T> = Result<T, RdpsndNativeError>;
Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
#![cfg_attr(doc, doc = include_str!("../README.md"))]
22
#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")]
3-
4-
#[cfg(test)]
5-
use tracing_subscriber as _;
3+
// `anyhow` and `tracing-subscriber` are dev-deps used only by the `cpal`
4+
// example binary, but `unused_crate_dependencies` still flags them on the
5+
// lib target. The `[lib] test = false` setting makes a `#[cfg(test)]`
6+
// workaround dead code, so the suppression has to apply unconditionally.
7+
#![allow(unused_crate_dependencies)]
68

79
pub mod cpal;
10+
pub mod error;
11+
12+
pub use error::{RdpsndNativeError, RdpsndNativeErrorKind, RdpsndNativeResult};

0 commit comments

Comments
 (0)