Skip to content

Commit 1eb53f3

Browse files
committed
fix(sound): suppress ALSA stderr errors on headless systems
On Linux systems without audio hardware (Docker containers, headless servers), ALSA prints error messages like 'ALSA lib confmisc.c: cannot find card 0' directly to stderr before Rust error handling can catch them. This fix temporarily redirects stderr to /dev/null during audio initialization on Linux to suppress these noisy messages while preserving the graceful fallback to terminal bell notifications. The fix: - Only applies on Linux (where ALSA is used) - Falls back to normal behavior if stderr redirection fails - Properly restores stderr after initialization - Has no effect on non-Linux platforms (macOS, Windows)
1 parent ac6398e commit 1eb53f3

File tree

1 file changed

+95
-8
lines changed

1 file changed

+95
-8
lines changed

src/cortex-tui/src/sound.rs

Lines changed: 95 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
//!
1010
//! On platforms without audio support (e.g., musl builds), falls back to
1111
//! terminal bell notifications.
12+
//!
13+
//! On Linux, ALSA error messages (e.g., "cannot find card 0") are suppressed
14+
//! during audio initialization to avoid noisy output on headless systems.
1215
1316
#[cfg(feature = "audio")]
1417
use std::io::Cursor;
@@ -44,6 +47,96 @@ const COMPLETE_WAV: &[u8] = include_bytes!("sounds/complete.wav");
4447
#[cfg(feature = "audio")]
4548
const APPROVAL_WAV: &[u8] = include_bytes!("sounds/approval.wav");
4649

50+
/// Try to create audio output stream, suppressing ALSA errors on Linux.
51+
///
52+
/// On Linux, ALSA prints error messages directly to stderr when no audio
53+
/// hardware is available (e.g., "ALSA lib confmisc.c: cannot find card 0").
54+
/// This function suppresses those messages by temporarily redirecting stderr
55+
/// to /dev/null during initialization.
56+
#[cfg(all(feature = "audio", target_os = "linux"))]
57+
fn try_create_output_stream() -> Option<(rodio::OutputStream, rodio::OutputStreamHandle)> {
58+
use std::os::unix::io::AsRawFd;
59+
60+
// Open /dev/null for redirecting stderr
61+
let dev_null = match std::fs::File::open("/dev/null") {
62+
Ok(f) => f,
63+
Err(_) => {
64+
// Can't open /dev/null, try without suppression
65+
return match rodio::OutputStream::try_default() {
66+
Ok((stream, handle)) => Some((stream, handle)),
67+
Err(e) => {
68+
tracing::debug!("Failed to initialize audio output: {}", e);
69+
None
70+
}
71+
};
72+
}
73+
};
74+
75+
// Save the original stderr file descriptor
76+
// SAFETY: dup is safe to call with a valid file descriptor (2 = stderr)
77+
let original_stderr = unsafe { libc::dup(2) };
78+
if original_stderr == -1 {
79+
// dup failed, try without suppression
80+
return match rodio::OutputStream::try_default() {
81+
Ok((stream, handle)) => Some((stream, handle)),
82+
Err(e) => {
83+
tracing::debug!("Failed to initialize audio output: {}", e);
84+
None
85+
}
86+
};
87+
}
88+
89+
// Redirect stderr to /dev/null
90+
// SAFETY: dup2 is safe with valid file descriptors
91+
let redirect_result = unsafe { libc::dup2(dev_null.as_raw_fd(), 2) };
92+
drop(dev_null); // Close our handle to /dev/null
93+
94+
if redirect_result == -1 {
95+
// dup2 failed, restore and try without suppression
96+
// SAFETY: close is safe with a valid file descriptor
97+
unsafe { libc::close(original_stderr) };
98+
return match rodio::OutputStream::try_default() {
99+
Ok((stream, handle)) => Some((stream, handle)),
100+
Err(e) => {
101+
tracing::debug!("Failed to initialize audio output: {}", e);
102+
None
103+
}
104+
};
105+
}
106+
107+
// Try to create the audio output stream (ALSA errors will go to /dev/null)
108+
let result = rodio::OutputStream::try_default();
109+
110+
// Restore the original stderr
111+
// SAFETY: dup2 and close are safe with valid file descriptors
112+
unsafe {
113+
libc::dup2(original_stderr, 2);
114+
libc::close(original_stderr);
115+
}
116+
117+
match result {
118+
Ok((stream, handle)) => Some((stream, handle)),
119+
Err(e) => {
120+
tracing::debug!("Failed to initialize audio output: {}", e);
121+
None
122+
}
123+
}
124+
}
125+
126+
/// Try to create audio output stream (non-Linux platforms).
127+
///
128+
/// On non-Linux platforms, ALSA is not used, so no stderr suppression is needed.
129+
#[cfg(all(feature = "audio", not(target_os = "linux")))]
130+
fn try_create_output_stream() -> Option<(rodio::OutputStream, rodio::OutputStreamHandle)> {
131+
match rodio::OutputStream::try_default() {
132+
Ok((stream, handle)) => Some((stream, handle)),
133+
Err(e) => {
134+
tracing::debug!("Failed to initialize audio output: {}", e);
135+
None
136+
}
137+
}
138+
}
139+
47140
/// Initialize the global sound system.
48141
/// Spawns a dedicated audio thread that owns the OutputStream.
49142
/// Should be called once at application startup.
@@ -67,14 +160,8 @@ pub fn init() {
67160
thread::Builder::new()
68161
.name("cortex-audio".to_string())
69162
.spawn(move || {
70-
// Try to create audio output
71-
let output = match rodio::OutputStream::try_default() {
72-
Ok((stream, handle)) => Some((stream, handle)),
73-
Err(e) => {
74-
tracing::debug!("Failed to initialize audio output: {}", e);
75-
None
76-
}
77-
};
163+
// Try to create audio output (with ALSA error suppression on Linux)
164+
let output = try_create_output_stream();
78165

79166
// Process sound requests
80167
while let Ok(sound_type) = rx.recv() {

0 commit comments

Comments
 (0)