Skip to main content

datui_lib/loading/
tee.rs

1//! `--tee FILE`: standard input recorded to a file the user keeps, while it is viewed.
2//!
3//! The copy itself is a [`crate::loading::follow::Spool`] writing to FILE rather than to a
4//! temporary file; this module creates FILE and, when the stream ends, fills in what
5//! a producer writing to a pipe could not: a WAV file's sizes. `--tee -` passes the
6//! stream on to standard output instead (`pass_stdout_on`).
7
8use std::fs::File;
9use std::io::{Read, Seek, SeekFrom, Write};
10use std::path::Path;
11
12/// Create `path` for the copy, refusing one that is there unless `force`. Never
13/// claimed for removal: the file is the user's, and stays whatever happens.
14pub(crate) fn create(path: &Path, force: bool) -> Result<File, String> {
15    let mut options = std::fs::OpenOptions::new();
16    options.read(true).write(true);
17    if force {
18        options.create(true).truncate(true);
19    } else {
20        options.create_new(true);
21    }
22    options.open(path).map_err(|e| match e.kind() {
23        std::io::ErrorKind::AlreadyExists => refusal(path),
24        _ => format!("Could not create {}: {e}", path.display()),
25    })
26}
27
28/// What is said of a FILE that is there already.
29pub fn refusal(path: &Path) -> String {
30    format!(
31        "{} is there already; --force overwrites it.",
32        path.display()
33    )
34}
35
36/// Why `--tee -` cannot pass the stream on when standard output is the terminal.
37const STDOUT_IS_THE_SCREEN: &str = "--tee - passes standard input on to standard output, \
38     which is the screen here: send it on to a pipe or a file, as in: \
39     some_logger | datui -f --tee - | gzip > run1.csv.gz";
40
41/// `--tee -`: keep standard output for the stream, and point the process's standard
42/// output at the terminal, where the screen is drawn. Everything that draws or writes
43/// to standard output, Ratatui and Crossterm among them, then reaches the terminal
44/// with no change of its own. Returns the stream's standard output.
45#[cfg(unix)]
46pub(crate) fn pass_stdout_on() -> Result<File, String> {
47    use std::io::IsTerminal;
48    use std::os::fd::{AsRawFd, FromRawFd};
49    if std::io::stdout().is_terminal() {
50        return Err(STDOUT_IS_THE_SCREEN.to_string());
51    }
52    let tty = std::fs::OpenOptions::new()
53        .read(true)
54        .write(true)
55        .open("/dev/tty")
56        .map_err(|e| format!("--tee - draws on the terminal, /dev/tty, which did not open: {e}"))?;
57    // Close-on-exec, so a program datui starts does not hold the pipe open after the
58    // stream ends.
59    // SAFETY: plain fd calls; their results are checked.
60    let kept = unsafe { libc::fcntl(libc::STDOUT_FILENO, libc::F_DUPFD_CLOEXEC, 3) };
61    if kept < 0 {
62        return Err(format!(
63            "Could not keep standard output: {}",
64            std::io::Error::last_os_error()
65        ));
66    }
67    // SAFETY: `kept` was just opened and is owned by nothing else.
68    let kept = unsafe { File::from_raw_fd(kept) };
69    // SAFETY: both fds are open; dup2 replaces fd 1 atomically.
70    if unsafe { libc::dup2(tty.as_raw_fd(), libc::STDOUT_FILENO) } < 0 {
71        return Err(format!(
72            "Could not draw on the terminal: {}",
73            std::io::Error::last_os_error()
74        ));
75    }
76    Ok(kept)
77}
78
79/// As on Unix, with the console's screen buffer, `CONOUT$`, as standard output. Rust's
80/// standard output asks Windows for the handle on every write, and Crossterm sizes and
81/// sets up the console through `CONOUT$` and `CONIN$` itself.
82#[cfg(windows)]
83pub(crate) fn pass_stdout_on() -> Result<File, String> {
84    use std::io::IsTerminal;
85    use std::os::windows::io::{FromRawHandle, IntoRawHandle};
86    use windows_sys::Win32::System::Console::{GetStdHandle, STD_OUTPUT_HANDLE, SetStdHandle};
87    if std::io::stdout().is_terminal() {
88        return Err(STDOUT_IS_THE_SCREEN.to_string());
89    }
90    let console = std::fs::OpenOptions::new()
91        .read(true)
92        .write(true)
93        .open("CONOUT$")
94        .map_err(|e| format!("--tee - draws on the console, which did not open: {e}"))?;
95    // SAFETY: GetStdHandle takes no pointers.
96    let kept = unsafe { GetStdHandle(STD_OUTPUT_HANDLE) };
97    if kept.is_null() || kept == windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE {
98        return Err(
99            "--tee - passes the stream on to standard output, and there is none.".to_string(),
100        );
101    }
102    // The console handle stays open for the life of the process, as standard output.
103    let console = console.into_raw_handle();
104    // SAFETY: a live handle, owned from here by the process's standard output.
105    if unsafe { SetStdHandle(STD_OUTPUT_HANDLE, console) } == 0 {
106        return Err(format!(
107            "Could not draw on the console: {}",
108            std::io::Error::last_os_error()
109        ));
110    }
111    // SAFETY: the old standard output handle is no longer standard output; this is now
112    // its one owner.
113    Ok(unsafe { File::from_raw_handle(kept) })
114}
115
116#[cfg(not(any(unix, windows)))]
117pub(crate) fn pass_stdout_on() -> Result<File, String> {
118    Err("--tee - is not supported on this platform.".to_string())
119}
120
121/// What [`fix_wav_sizes`] did.
122#[derive(Debug, PartialEq, Eq)]
123pub(crate) enum Fixed {
124    /// Not a WAV file, or its sizes were right already.
125    Nothing,
126    /// The RIFF and `data` sizes now say how long the file is.
127    Riff,
128    /// Over 4 GB: the header is RF64's, its sizes in the `ds64` chunk that took the
129    /// place of the `JUNK` the producer reserved.
130    Rf64,
131    /// Over 4 GB with no room reserved for RF64's sizes: they are left as written.
132    TooLong,
133}
134
135/// Fill in the sizes of the WAV file `file` holds, which a producer writing to a pipe
136/// could not seek back to: the RIFF size and the `data` chunk's, 0 or 0xFFFFFFFF while
137/// it streamed. Over 4 GB the header becomes RF64's when the producer reserved room for
138/// it with a `JUNK` chunk first, as Broadcast WAV writers do.
139pub(crate) fn fix_wav_sizes(file: &mut File) -> std::io::Result<Fixed> {
140    let len = file.metadata()?.len();
141    let mut head = [0u8; 12];
142    file.seek(SeekFrom::Start(0))?;
143    if len < 12 || file.read_exact(&mut head).is_err() {
144        return Ok(Fixed::Nothing);
145    }
146    let rf64 = &head[0..4] == b"RF64";
147    if !(&head[0..4] == b"RIFF" || rf64) || &head[8..12] != b"WAVE" {
148        return Ok(Fixed::Nothing);
149    }
150    // Walk the chunks to `data`, the last a streaming producer writes.
151    let mut at = 12u64;
152    let mut junk = None;
153    let mut ds64 = None;
154    let data = loop {
155        if at + 8 > len {
156            return Ok(Fixed::Nothing);
157        }
158        let mut chunk = [0u8; 8];
159        file.seek(SeekFrom::Start(at))?;
160        file.read_exact(&mut chunk)?;
161        let size = u32::from_le_bytes([chunk[4], chunk[5], chunk[6], chunk[7]]) as u64;
162        match &chunk[0..4] {
163            b"data" => break at,
164            b"JUNK" if at == 12 => junk = Some(size),
165            b"ds64" => ds64 = Some(at),
166            _ => {}
167        }
168        // Chunks are padded to an even length.
169        at += 8 + size + (size & 1);
170    };
171    let data_size = len - (data + 8);
172    let riff_size = len - 8;
173    if let Some(ds64) = ds64.filter(|_| rf64) {
174        // RF64 already: its sizes live in `ds64`.
175        file.seek(SeekFrom::Start(ds64 + 8))?;
176        file.write_all(&riff_size.to_le_bytes())?;
177        file.write_all(&data_size.to_le_bytes())?;
178        return Ok(Fixed::Rf64);
179    }
180    if riff_size <= u32::MAX as u64 {
181        if read_u32(file, 4)? as u64 == riff_size && read_u32(file, data + 4)? as u64 == data_size {
182            return Ok(Fixed::Nothing);
183        }
184        write_u32(file, 4, riff_size as u32)?;
185        write_u32(file, data + 4, data_size as u32)?;
186        return Ok(Fixed::Riff);
187    }
188    // A ds64 chunk is 28 bytes: the RIFF size, the data size, the sample count and an
189    // empty table. It takes the reserved JUNK's place, size and all.
190    match junk {
191        Some(size) if size >= 28 => {
192            file.seek(SeekFrom::Start(0))?;
193            file.write_all(b"RF64")?;
194            file.write_all(&u32::MAX.to_le_bytes())?;
195            file.seek(SeekFrom::Start(12))?;
196            file.write_all(b"ds64")?;
197            file.write_all(&(size as u32).to_le_bytes())?;
198            file.write_all(&riff_size.to_le_bytes())?;
199            file.write_all(&data_size.to_le_bytes())?;
200            // The sample count is the fact chunk's business, which PCM has none of.
201            file.write_all(&0u64.to_le_bytes())?;
202            file.write_all(&0u32.to_le_bytes())?;
203            write_u32(file, data + 4, u32::MAX)?;
204            Ok(Fixed::Rf64)
205        }
206        _ => Ok(Fixed::TooLong),
207    }
208}
209
210fn read_u32(file: &mut File, at: u64) -> std::io::Result<u32> {
211    let mut b = [0u8; 4];
212    file.seek(SeekFrom::Start(at))?;
213    file.read_exact(&mut b)?;
214    Ok(u32::from_le_bytes(b))
215}
216
217fn write_u32(file: &mut File, at: u64, value: u32) -> std::io::Result<()> {
218    file.seek(SeekFrom::Start(at))?;
219    file.write_all(&value.to_le_bytes())
220}
221
222#[cfg(test)]
223mod tests;