113 lines
4.5 KiB
Rust
113 lines
4.5 KiB
Rust
//! Keeping foreign writes off the TUI's screen.
|
|
//!
|
|
//! `tracing` goes to a log file precisely because the terminal belongs to the
|
|
//! interface — but that only covers what *we* log. File descriptor 2 still
|
|
//! points at the terminal, and plenty in this process never goes through
|
|
//! `tracing`:
|
|
//!
|
|
//! - ALSA prints from C (`ALSA lib pcm.c:…: underrun occurred`), which the
|
|
//! bundled `cbd` sees because the audio stack shares its process.
|
|
//! - the default panic hook, and anything a dependency decides to
|
|
//! `eprintln!`.
|
|
//!
|
|
//! Those bytes land wherever the cursor is, scroll the terminal by a line and
|
|
//! leave the whole layout looking shifted — the queue appearing to bleed into
|
|
//! the now-playing pane. Because ratatui repaints only the cells that changed,
|
|
//! the damage persists until something forces a full redraw. Worse, the
|
|
//! message is then *lost*: it never reaches the log, so afterwards there is no
|
|
//! record of the underrun that caused it.
|
|
//!
|
|
//! So the fix is one `dup2`: point fd 2 at a file next to the log before the
|
|
//! alternate screen is entered. The diagnostics are kept, just not on screen.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
/// The directory both binaries log into: `crabidy/` under the state dir,
|
|
/// falling back to the cache dir and then to the temp dir.
|
|
///
|
|
/// Shared so the log file and the captured stderr always land together.
|
|
pub fn log_dir() -> PathBuf {
|
|
dirs::state_dir()
|
|
.or_else(dirs::cache_dir)
|
|
.unwrap_or_else(std::env::temp_dir)
|
|
.join("crabidy")
|
|
}
|
|
|
|
/// Points file descriptor 2 at `path` (appending), so writes that bypass
|
|
/// `tracing` are recorded instead of scribbling on the interface.
|
|
///
|
|
/// Call once, before the terminal is put into raw mode. Errors are returned
|
|
/// rather than handled: a client that cannot open its log file should still
|
|
/// start — it just keeps the noisy stderr it has always had.
|
|
#[cfg(unix)]
|
|
pub fn capture_into(path: &Path) -> std::io::Result<()> {
|
|
use std::os::fd::AsRawFd;
|
|
|
|
let file = std::fs::OpenOptions::new()
|
|
.create(true)
|
|
.append(true)
|
|
.open(path)?;
|
|
// `dup2` makes fd 2 a second reference to this file, so dropping `file`
|
|
// (and its own descriptor) at the end of this function leaves stderr
|
|
// pointing at it.
|
|
if unsafe { libc::dup2(file.as_raw_fd(), libc::STDERR_FILENO) } == -1 {
|
|
return Err(std::io::Error::last_os_error());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Nothing to do off Unix: there is no `dup2`, and the crabidy clients are
|
|
/// terminal programs on Linux and macOS.
|
|
#[cfg(not(unix))]
|
|
pub fn capture_into(_path: &Path) -> std::io::Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
/// The redirect must actually move the process's stderr, and appending
|
|
/// must not truncate what an earlier run wrote.
|
|
///
|
|
/// The write goes to the descriptor directly rather than through
|
|
/// `eprintln!`: that is what a C library does (the case this exists for),
|
|
/// and the test harness intercepts the macro but not the descriptor.
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn captured_stderr_reaches_the_file_and_appends() {
|
|
use std::io::Write;
|
|
|
|
let dir = tempfile::tempdir().expect("tempdir");
|
|
let path = dir.path().join("cbd.stderr.log");
|
|
std::fs::write(&path, "earlier run\n").expect("seed");
|
|
|
|
// Keep the real stderr so the rest of the suite still has one. Briefly
|
|
// process-wide, so a parallel test writing to fd 2 in this window would
|
|
// land in the file too — harmless here, and the alternative is a child
|
|
// process for one assertion.
|
|
let saved = unsafe { libc::dup(libc::STDERR_FILENO) };
|
|
assert!(saved >= 0, "could not save stderr");
|
|
capture_into(&path).expect("redirect");
|
|
let wrote = std::io::stderr().write_all(b"underrun occurred\n");
|
|
let flushed = std::io::stderr().flush();
|
|
let restored = unsafe { libc::dup2(saved, libc::STDERR_FILENO) };
|
|
assert!(restored >= 0, "could not restore stderr");
|
|
unsafe { libc::close(saved) };
|
|
wrote.expect("write to the redirected stderr");
|
|
flushed.expect("flush");
|
|
|
|
let captured = std::fs::read_to_string(&path).expect("read back");
|
|
assert!(captured.starts_with("earlier run\n"), "{captured:?}");
|
|
assert!(captured.contains("underrun occurred"), "{captured:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn the_log_dir_is_under_crabidy() {
|
|
assert_eq!(
|
|
log_dir().file_name().and_then(|n| n.to_str()),
|
|
Some("crabidy")
|
|
);
|
|
}
|
|
}
|