1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 11:37:44 +00:00

tail: implement follow for stdin (pipe, fifo, and redirects)

* implement behavior to pass `gnu/tests/tail-2/follow-stdin.sh`
* add stdin redirect using the same /dev/stdin-workaround used by uu_stat
* refactor
This commit is contained in:
Jan Scheer 2022-05-16 22:27:41 +02:00
parent ede73745f5
commit 90cef98a14
No known key found for this signature in database
GPG key ID: C62AD4C29E2B9828

View file

@ -47,7 +47,7 @@ use uucore::parse_size::{parse_size, ParseSizeError};
use uucore::ringbuffer::RingBuffer; use uucore::ringbuffer::RingBuffer;
#[cfg(unix)] #[cfg(unix)]
use crate::platform::stdin_is_pipe_or_fifo; use crate::platform::{stdin_is_bad_fd, stdin_is_pipe_or_fifo};
#[cfg(unix)] #[cfg(unix)]
use std::fs::metadata; use std::fs::metadata;
#[cfg(unix)] #[cfg(unix)]
@ -65,9 +65,13 @@ const ABOUT: &str = "\
const USAGE: &str = "{} [FLAG]... [FILE]..."; const USAGE: &str = "{} [FLAG]... [FILE]...";
pub mod text { pub mod text {
pub static STDIN_STR: &str = "standard input"; pub static DASH: &str = "-";
pub static DEV_STDIN: &str = "/dev/stdin";
pub static STDIN_HEADER: &str = "standard input";
pub static NO_FILES_REMAINING: &str = "no files remaining"; pub static NO_FILES_REMAINING: &str = "no files remaining";
pub static NO_SUCH_FILE: &str = "No such file or directory"; pub static NO_SUCH_FILE: &str = "No such file or directory";
#[cfg(unix)]
pub static BAD_FD: &str = "Bad file descriptor";
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub static BACKEND: &str = "inotify"; pub static BACKEND: &str = "inotify";
#[cfg(all(unix, not(target_os = "linux")))] #[cfg(all(unix, not(target_os = "linux")))]
@ -116,18 +120,18 @@ enum FollowMode {
#[derive(Debug, Default)] #[derive(Debug, Default)]
struct Settings { struct Settings {
mode: FilterMode,
sleep_sec: Duration,
max_unchanged_stats: u32,
beginning: bool, beginning: bool,
follow: Option<FollowMode>, follow: Option<FollowMode>,
max_unchanged_stats: u32,
mode: FilterMode,
paths: VecDeque<PathBuf>,
pid: platform::Pid,
retry: bool,
return_code: i32,
sleep_sec: Duration,
use_polling: bool, use_polling: bool,
verbose: bool, verbose: bool,
retry: bool, stdin_is_pipe_or_fifo: bool,
pid: platform::Pid,
paths: Vec<PathBuf>,
presume_input_pipe: bool,
return_code: i32,
} }
impl Settings { impl Settings {
@ -217,58 +221,15 @@ impl Settings {
} }
} }
settings.presume_input_pipe = matches.is_present(options::PRESUME_INPUT_PIPE); settings.stdin_is_pipe_or_fifo = matches.is_present(options::PRESUME_INPUT_PIPE);
let mut paths = matches settings.paths = matches
.values_of(options::ARG_FILES) .values_of(options::ARG_FILES)
.map(|v| v.map(PathBuf::from).collect()) .map(|v| v.map(PathBuf::from).collect())
.unwrap_or_else(|| vec![PathBuf::from("-")]); .unwrap_or_default();
// .unwrap_or_else(|| {
// Filter out non tailable paths depending on `FollowMode`. // vec![PathBuf::from("-")] // always follow stdin
paths.retain(|path| { // });
if !path.is_stdin() {
if !(path.is_tailable()) {
if settings.follow == Some(FollowMode::Descriptor) && settings.retry {
show_warning!("--retry only effective for the initial open");
}
if !path.exists() {
settings.return_code = 1;
show_error!(
"cannot open {} for reading: {}",
path.quote(),
text::NO_SUCH_FILE
);
} else if path.is_dir() {
settings.return_code = 1;
show_error!("error reading {}: Is a directory", path.quote());
if settings.follow.is_some() && settings.retry {
let msg = if !settings.retry {
"; giving up on this name"
} else {
""
};
show_error!(
"{}: cannot follow end of this type of file{}",
path.display(),
msg
);
}
} else {
// TODO: [2021-10; jhscheer] how to handle block device, socket, fifo?
todo!();
}
}
} else if settings.follow == Some(FollowMode::Name) {
// Mimic GNU's tail; Exit immediately even though there might be other valid files.
crash!(1, "cannot follow '-' by name");
}
if settings.follow == Some(FollowMode::Name) && settings.retry {
true
} else {
!path.is_dir() || path.is_stdin()
}
});
settings.paths = paths.clone();
settings.verbose = (matches.is_present(options::verbosity::VERBOSE) settings.verbose = (matches.is_present(options::verbosity::VERBOSE)
|| settings.paths.len() > 1) || settings.paths.len() > 1)
@ -280,16 +241,41 @@ impl Settings {
#[uucore::main] #[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let args = match Settings::get_from(args) { let mut args = match Settings::get_from(args) {
Ok(o) => o, Ok(o) => o,
Err(s) => { Err(s) => {
return Err(USimpleError::new(1, s)); return Err(USimpleError::new(1, s));
} }
}; };
// skip expansive fstat check if PRESUME_INPUT_PIPE is selected
if !args.stdin_is_pipe_or_fifo {
// FIXME windows has GetFileType which can determine if the file is a pipe/FIFO
// so this check can also be performed
if cfg!(unix) {
args.stdin_is_pipe_or_fifo = stdin_is_pipe_or_fifo();
}
}
uu_tail(args) uu_tail(args)
} }
fn uu_tail(mut settings: Settings) -> UResult<()> { fn uu_tail(mut settings: Settings) -> UResult<()> {
let dash = PathBuf::from(text::DASH);
// Mimic GNU's tail for `tail -F` and exit immediately
if (settings.paths.is_empty() || settings.paths.contains(&dash))
&& settings.follow == Some(FollowMode::Name)
{
crash!(1, "cannot follow {} by name", text::DASH.quote());
}
if !settings.paths.contains(&dash) && settings.stdin_is_pipe_or_fifo
|| settings.paths.is_empty() && !settings.stdin_is_pipe_or_fifo
{
settings.paths.push_front(dash);
}
let mut first_header = true; let mut first_header = true;
let mut files = FileHandling { let mut files = FileHandling {
map: HashMap::with_capacity(settings.paths.len()), map: HashMap::with_capacity(settings.paths.len()),
@ -299,45 +285,97 @@ fn uu_tail(mut settings: Settings) -> UResult<()> {
// Do an initial tail print of each path's content. // Do an initial tail print of each path's content.
// Add `path` to `files` map if `--follow` is selected. // Add `path` to `files` map if `--follow` is selected.
for path in &settings.paths { for path in &settings.paths {
let mut path = path.to_path_buf();
let mut display_name = path.to_path_buf();
// Workaround to handle redirects, e.g. `touch f && tail -f - < f`
if cfg!(unix) && path.is_stdin() {
display_name = PathBuf::from(text::STDIN_HEADER);
if let Ok(p) = Path::new(text::DEV_STDIN).canonicalize() {
path = p.to_owned();
} else {
path = PathBuf::from(text::DEV_STDIN);
}
}
if path.is_stdin() && settings.follow == Some(FollowMode::Name) {
// TODO: still needed?
// Mimic GNU's tail; Exit immediately even though there might be other valid files.
crash!(1, "cannot follow {} by name", text::DASH.quote());
} else if !path.is_stdin() && !path.is_tailable() {
if settings.follow == Some(FollowMode::Descriptor) && settings.retry {
show_warning!("--retry only effective for the initial open");
}
if !path.exists() {
settings.return_code = 1;
show_error!(
"cannot open {} for reading: {}",
display_name.quote(),
text::NO_SUCH_FILE
);
} else if path.is_dir() {
settings.return_code = 1;
show_error!("error reading {}: Is a directory", display_name.quote());
if settings.follow.is_some() {
let msg = if !settings.retry {
"; giving up on this name"
} else {
""
};
show_error!(
"{}: cannot follow end of this type of file{}",
display_name.display(),
msg
);
}
if !(settings.follow == Some(FollowMode::Name) && settings.retry) {
// skip directory if not retry
continue;
}
} else {
// TODO: [2021-10; jhscheer] how to handle block device, socket, fifo?
todo!();
}
}
let md = path.metadata().ok(); let md = path.metadata().ok();
if path.is_stdin() || settings.presume_input_pipe {
if display_name.is_stdin() {
if settings.verbose { if settings.verbose {
if !first_header { if !first_header {
println!(); println!();
} }
Path::new(text::STDIN_STR).print_header(); Path::new(text::STDIN_HEADER).print_header();
} }
let mut reader = BufReader::new(stdin()); let mut reader = BufReader::new(stdin());
unbounded_tail(&mut reader, &settings)?; if !stdin_is_bad_fd() {
unbounded_tail(&mut reader, &settings)?;
// Don't follow stdin since there are no checks for pipes/FIFOs if settings.follow == Some(FollowMode::Descriptor) {
//
// FIXME windows has GetFileType which can determine if the file is a pipe/FIFO
// so this check can also be performed
#[cfg(unix)]
{
/*
POSIX specification regarding tail -f
If the input file is a regular file or if the file operand specifies a FIFO, do not
terminate after the last line of the input file has been copied, but read and copy
further bytes from the input file when they become available. If no file operand is
specified and standard input is a pipe or FIFO, the -f option shall be ignored. If
the input file is not a FIFO, pipe, or regular file, it is unspecified whether or
not the -f option shall be ignored.
*/
if settings.follow == Some(FollowMode::Descriptor) && !stdin_is_pipe_or_fifo() {
// Insert `stdin` into `files.map`. // Insert `stdin` into `files.map`.
files.map.insert( files.insert(
PathBuf::from(text::STDIN_STR), path.to_path_buf(),
PathData { PathData {
reader: Some(Box::new(reader)), reader: Some(Box::new(reader)),
metadata: None, metadata: None,
display_name: PathBuf::from(text::STDIN_STR), display_name: PathBuf::from(text::STDIN_HEADER),
}, },
); );
} }
} else {
settings.return_code = 1;
show_error!(
"cannot fstat {}: {}",
text::STDIN_HEADER.quote(),
text::BAD_FD
);
if settings.follow.is_some() {
show_error!(
"error reading {}: {}",
text::STDIN_HEADER.quote(),
text::BAD_FD
);
}
} }
} else if path.is_tailable() { } else if path.is_tailable() {
if settings.verbose { if settings.verbose {
@ -359,44 +397,60 @@ fn uu_tail(mut settings: Settings) -> UResult<()> {
} }
if settings.follow.is_some() { if settings.follow.is_some() {
// Insert existing/file `path` into `files.map`. // Insert existing/file `path` into `files.map`.
files.map.insert( files.insert(
path.canonicalize().unwrap(), path.canonicalize().unwrap(),
PathData { PathData {
reader: Some(Box::new(reader)), reader: Some(Box::new(reader)),
metadata: md, metadata: md,
display_name: path.to_owned(), display_name,
}, },
); );
files.last = Some(path.canonicalize().unwrap());
} }
} else if settings.retry && settings.follow.is_some() { } else if settings.retry && settings.follow.is_some() {
if path.is_relative() {
path = std::env::current_dir().unwrap().join(&path);
}
// Insert non-is_tailable() paths into `files.map`. // Insert non-is_tailable() paths into `files.map`.
let key = if path.is_relative() { files.insert(
std::env::current_dir().unwrap().join(path) path.to_path_buf(),
} else {
path.to_path_buf()
};
files.map.insert(
key.to_path_buf(),
PathData { PathData {
reader: None, reader: None,
metadata: md, metadata: md,
display_name: path.to_path_buf(), display_name,
}, },
); );
files.last = Some(key);
} }
} }
// dbg!(files.map.is_empty(), files.files_remaining(), files.only_stdin_remaining());
// for k in files.map.keys() {
// dbg!(k);
// }
if settings.follow.is_some() { if settings.follow.is_some() {
/*
POSIX specification regarding tail -f
If the input file is a regular file or if the file operand specifies a FIFO, do not
terminate after the last line of the input file has been copied, but read and copy
further bytes from the input file when they become available. If no file operand is
specified and standard input is a pipe or FIFO, the -f option shall be ignored. If
the input file is not a FIFO, pipe, or regular file, it is unspecified whether or
not the -f option shall be ignored.
*/
if files.map.is_empty() || !files.files_remaining() && !settings.retry { if files.map.is_empty() || !files.files_remaining() && !settings.retry {
show_error!("{}", text::NO_FILES_REMAINING); if !files.only_stdin_remaining() {
} else { show_error!("{}", text::NO_FILES_REMAINING);
}
} else if !(settings.stdin_is_pipe_or_fifo && settings.paths.len() == 1) {
follow(&mut files, &mut settings)?; follow(&mut files, &mut settings)?;
} }
} }
if settings.return_code > 0 { if settings.return_code > 0 {
#[cfg(unix)]
if stdin_is_bad_fd() {
show_error!("-: {}", text::BAD_FD);
}
return Err(USimpleError::new(settings.return_code, "")); return Err(USimpleError::new(settings.return_code, ""));
} }
@ -565,9 +619,9 @@ fn follow(files: &mut FileHandling, settings: &mut Settings) -> UResult<()> {
// Fallback: polling (default delay is 30 seconds!) // Fallback: polling (default delay is 30 seconds!)
// NOTE: // NOTE:
// We force the use of kqueue with: features=["macos_kqueue"], // We force the use of kqueue with: features=["macos_kqueue"].
// because macOS only `kqueue` is suitable for our use case since `FSEvents` waits until // On macOS only `kqueue` is suitable for our use case because `FSEvents`
// file close util it delivers a modify event. See: // waits for file close util it delivers a modify event. See:
// https://github.com/notify-rs/notify/issues/240 // https://github.com/notify-rs/notify/issues/240
let mut watcher: Box<dyn Watcher>; let mut watcher: Box<dyn Watcher>;
@ -600,7 +654,7 @@ fn follow(files: &mut FileHandling, settings: &mut Settings) -> UResult<()> {
// If there is no parent, add `path` to `orphans`. // If there is no parent, add `path` to `orphans`.
let mut orphans = Vec::new(); let mut orphans = Vec::new();
for path in files.map.keys() { for path in files.map.keys() {
if path.is_file() { if path.is_tailable() {
let path = get_path(path, settings); let path = get_path(path, settings);
watcher watcher
.watch(&path.canonicalize().unwrap(), RecursiveMode::NonRecursive) .watch(&path.canonicalize().unwrap(), RecursiveMode::NonRecursive)
@ -616,7 +670,8 @@ fn follow(files: &mut FileHandling, settings: &mut Settings) -> UResult<()> {
.unwrap(); .unwrap();
} }
} else { } else {
// TODO: [2021-10; jhscheer] does this case need handling? // TODO: [2021-10; jhscheer] do we need to handle non-is_tailable without follow/retry?
todo!();
} }
} }
@ -934,7 +989,7 @@ fn handle_event(
let new_path = event.paths.last().unwrap().canonicalize().unwrap(); let new_path = event.paths.last().unwrap().canonicalize().unwrap();
// Open new file and seek to End: // Open new file and seek to End:
let mut file = File::open(&new_path).unwrap(); let mut file = File::open(&new_path).unwrap();
let _ = file.seek(SeekFrom::End(0)); file.seek(SeekFrom::End(0)).unwrap();
// Add new reader and remove old reader: // Add new reader and remove old reader:
files.map.insert( files.map.insert(
new_path.to_owned(), new_path.to_owned(),
@ -1006,9 +1061,20 @@ struct FileHandling {
} }
impl FileHandling { impl FileHandling {
fn insert(&mut self, k: PathBuf, v: PathData) -> Option<PathData> {
self.last = Some(k.to_owned());
self.map.insert(k, v)
}
fn only_stdin_remaining(&self) -> bool {
self.map.len() == 1
&& (self.map.contains_key(Path::new(text::DASH))
|| self.map.contains_key(Path::new(text::DEV_STDIN))) // TODO: still needed?
}
fn files_remaining(&self) -> bool { fn files_remaining(&self) -> bool {
for path in self.map.keys() { for path in self.map.keys() {
if path.is_tailable() { if path.is_tailable() || path.is_stdin() {
return true; return true;
} }
} }
@ -1349,7 +1415,9 @@ trait PathExt {
impl PathExt for Path { impl PathExt for Path {
fn is_stdin(&self) -> bool { fn is_stdin(&self) -> bool {
self.to_str() == Some("-") self.eq(Path::new(text::DASH))
|| self.eq(Path::new(text::DEV_STDIN))
|| self.eq(Path::new(text::STDIN_HEADER))
} }
fn print_header(&self) { fn print_header(&self) {
println!("==> {} <==", self.display()); println!("==> {} <==", self.display());