Skip to content

fix(sound): suppress ALSA stderr errors on headless systems#116

Merged
echobt merged 2 commits intomainfrom
fix/suppress-alsa-stderr-messages
Feb 5, 2026
Merged

fix(sound): suppress ALSA stderr errors on headless systems#116
echobt merged 2 commits intomainfrom
fix/suppress-alsa-stderr-messages

Conversation

@echobt
Copy link
Contributor

@echobt echobt commented Feb 5, 2026

Problem

When starting the cortex CLI on Linux systems without audio hardware (Docker containers, headless servers, CI environments), ALSA prints error messages directly to stderr:

ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory

These messages are printed by the ALSA C library before Rust's error handling can catch the failure, making it impossible to suppress them through normal Rust error handling.

Solution

This PR implements temporary stderr redirection on Linux during audio initialization:

  1. Save the original stderr file descriptor using dup(2)
  2. Redirect stderr to /dev/null using dup2()
  3. Call rodio::OutputStream::try_default() (ALSA errors go to /dev/null)
  4. Restore the original stderr using dup2() and close the saved fd

The fix:

  • Only applies on Linux (where ALSA is used) - no changes on macOS/Windows
  • Gracefully degrades if stderr redirection fails
  • Properly restores stderr after initialization
  • Preserves existing behavior - terminal bell fallback still works when audio is unavailable

Testing

  • Code compiles with cargo check -p cortex-tui
  • Code compiles without audio feature: cargo check -p cortex-tui --no-default-features
  • Tested on headless Linux system (Docker container)

Changes

  • src/cortex-tui/src/sound.rs: Added try_create_output_stream() function that wraps audio initialization with ALSA stderr suppression on Linux

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)
@greptile-apps
Copy link

greptile-apps bot commented Feb 5, 2026

Greptile Overview

Greptile Summary

This PR reduces noisy ALSA messages on headless Linux by wrapping rodio::OutputStream::try_default() in a Linux-only helper (try_create_output_stream) that temporarily redirects process stderr (fd 2) to /dev/null during audio initialization, then restores it. Non-Linux platforms keep the prior behavior, and failures still fall back to the existing terminal-bell path when audio can’t be initialized.

Confidence Score: 3/5

  • This PR is close to mergeable but has correctness risks around global stderr manipulation.
  • Core approach is reasonable and scoped to Linux, but it temporarily redirects the process-wide stderr in a background thread and does not check restore-call return values, so stderr can be inadvertently suppressed beyond audio init or unrelated diagnostics can be lost under concurrency.
  • src/cortex-tui/src/sound.rs

Important Files Changed

Filename Overview
src/cortex-tui/src/sound.rs Adds try_create_output_stream() that redirects stderr to /dev/null on Linux to suppress ALSA noise during rodio::OutputStream::try_default(). Main concerns: restoration calls ignore dup2/close return values, and redirection is process-wide which can drop unrelated stderr output in other threads.

Sequence Diagram

sequenceDiagram
    participant Main as cortex-tui main
    participant Sound as sound::init()
    participant Thr as audio thread
    participant OS as Linux kernel (fd table)
    participant ALSA as ALSA/rodio backend

    Main->>Sound: init()
    Sound->>Thr: spawn "cortex-audio"
    Thr->>OS: open("/dev/null")
    Thr->>OS: dup(2) (save stderr)
    Thr->>OS: dup2(dev_null, 2) (redirect stderr)
    Thr->>ALSA: rodio::OutputStream::try_default()
    ALSA-->>OS: write(2, "ALSA lib ...")
    OS-->>OS: discard to /dev/null
    Thr->>OS: dup2(saved, 2) (restore stderr)
    Thr->>OS: close(saved)
    loop sound requests
        Sound-->>Thr: SyncSender::try_send(SoundType)
        Thr->>ALSA: play_wav_internal(handle, wav)
    end
Loading

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 file reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 107 to 115
// Try to create the audio output stream (ALSA errors will go to /dev/null)
let result = rodio::OutputStream::try_default();

// Restore the original stderr
// SAFETY: dup2 and close are safe with valid file descriptors
unsafe {
libc::dup2(original_stderr, 2);
libc::close(original_stderr);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stderr restoration unchecked

After redirecting stderr, the restore path ignores return values from libc::dup2(original_stderr, 2) and libc::close(original_stderr) (src/cortex-tui/src/sound.rs:112-115). If either fails (e.g., dup2 gets EBADF/EINTR), stderr can remain redirected for the rest of the process, which is a behavior change outside audio init. At minimum, check dup2’s return value and, on failure, log (and avoid closing original_stderr if you still need it).

Also appears in the redirect path where dup2(dev_null, 2) failure is handled but the restore attempt isn’t re-validated.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/cortex-tui/src/sound.rs
Line: 107:115

Comment:
**Stderr restoration unchecked**

After redirecting stderr, the restore path ignores return values from `libc::dup2(original_stderr, 2)` and `libc::close(original_stderr)` (`src/cortex-tui/src/sound.rs:112-115`). If either fails (e.g., `dup2` gets `EBADF`/`EINTR`), stderr can remain redirected for the rest of the process, which is a behavior change outside audio init. At minimum, check `dup2`’s return value and, on failure, log (and avoid closing `original_stderr` if you still need it).

Also appears in the redirect path where `dup2(dev_null, 2)` failure is handled but the restore attempt isn’t re-validated.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +89 to +104
// Redirect stderr to /dev/null
// SAFETY: dup2 is safe with valid file descriptors
let redirect_result = unsafe { libc::dup2(dev_null.as_raw_fd(), 2) };
drop(dev_null); // Close our handle to /dev/null

if redirect_result == -1 {
// dup2 failed, restore and try without suppression
// SAFETY: close is safe with a valid file descriptor
unsafe { libc::close(original_stderr) };
return match rodio::OutputStream::try_default() {
Ok((stream, handle)) => Some((stream, handle)),
Err(e) => {
tracing::debug!("Failed to initialize audio output: {}", e);
None
}
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Global stderr redirection race

This temporarily redirects the process-wide fd 2 in the audio thread (src/cortex-tui/src/sound.rs:89-104). In a multi-threaded CLI, any concurrent writes to stderr (including from other libraries) during this window will be dropped, which can hide unrelated errors. To avoid losing diagnostics, consider performing the redirection as early as possible (before starting other threads) or gating it behind a mutex/global guard so only one thread can run it while other stderr writes are paused (or document the trade-off explicitly if you’re accepting it).

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/cortex-tui/src/sound.rs
Line: 89:104

Comment:
**Global stderr redirection race**

This temporarily redirects the *process-wide* fd 2 in the audio thread (`src/cortex-tui/src/sound.rs:89-104`). In a multi-threaded CLI, any concurrent writes to stderr (including from other libraries) during this window will be dropped, which can hide unrelated errors. To avoid losing diagnostics, consider performing the redirection as early as possible (before starting other threads) or gating it behind a mutex/global guard so only one thread can run it while other stderr writes are paused (or document the trade-off explicitly if you’re accepting it).

How can I resolve this? If you propose a fix, please make it concise.

…-off

Address Greptile code review feedback:

1. Check dup2 return value during stderr restoration and log errors
   via tracing::error if restoration fails. Only close original_stderr
   if dup2 succeeds, preserving it for potential debugging.

2. Add comprehensive documentation about the thread safety implications
   of temporarily redirecting process-wide stderr during ALSA init.
@echobt echobt merged commit 9a49c1b into main Feb 5, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant