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;
#[cfg(unix)]
use crate::platform::stdin_is_pipe_or_fifo;
use crate::platform::{stdin_is_bad_fd, stdin_is_pipe_or_fifo};
#[cfg(unix)]
use std::fs::metadata;
#[cfg(unix)]
@ -65,9 +65,13 @@ const ABOUT: &str = "\
const USAGE: &str = "{} [FLAG]... [FILE]...";
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_SUCH_FILE: &str = "No such file or directory";
#[cfg(unix)]
pub static BAD_FD: &str = "Bad file descriptor";
#[cfg(target_os = "linux")]
pub static BACKEND: &str = "inotify";
#[cfg(all(unix, not(target_os = "linux")))]
@ -116,18 +120,18 @@ enum FollowMode {
#[derive(Debug, Default)]
struct Settings {
mode: FilterMode,
sleep_sec: Duration,
max_unchanged_stats: u32,
beginning: bool,
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,
verbose: bool,
retry: bool,
pid: platform::Pid,
paths: Vec<PathBuf>,
presume_input_pipe: bool,
return_code: i32,
stdin_is_pipe_or_fifo: bool,
}
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)
.map(|v| v.map(PathBuf::from).collect())
.unwrap_or_else(|| vec![PathBuf::from("-")]);
// Filter out non tailable paths depending on `FollowMode`.
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();
.unwrap_or_default();
// .unwrap_or_else(|| {
// vec![PathBuf::from("-")] // always follow stdin
// });
settings.verbose = (matches.is_present(options::verbosity::VERBOSE)
|| settings.paths.len() > 1)
@ -280,16 +241,41 @@ impl Settings {
#[uucore::main]
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,
Err(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)
}
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 files = FileHandling {
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.
// Add `path` to `files` map if `--follow` is selected.
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();
if path.is_stdin() || settings.presume_input_pipe {
if display_name.is_stdin() {
if settings.verbose {
if !first_header {
println!();
}
Path::new(text::STDIN_STR).print_header();
Path::new(text::STDIN_HEADER).print_header();
}
let mut reader = BufReader::new(stdin());
unbounded_tail(&mut reader, &settings)?;
// Don't follow stdin since there are no checks for pipes/FIFOs
//
// 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() {
if !stdin_is_bad_fd() {
unbounded_tail(&mut reader, &settings)?;
if settings.follow == Some(FollowMode::Descriptor) {
// Insert `stdin` into `files.map`.
files.map.insert(
PathBuf::from(text::STDIN_STR),
files.insert(
path.to_path_buf(),
PathData {
reader: Some(Box::new(reader)),
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() {
if settings.verbose {
@ -359,44 +397,60 @@ fn uu_tail(mut settings: Settings) -> UResult<()> {
}
if settings.follow.is_some() {
// Insert existing/file `path` into `files.map`.
files.map.insert(
files.insert(
path.canonicalize().unwrap(),
PathData {
reader: Some(Box::new(reader)),
metadata: md,
display_name: path.to_owned(),
display_name,
},
);
files.last = Some(path.canonicalize().unwrap());
}
} 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`.
let key = if path.is_relative() {
std::env::current_dir().unwrap().join(path)
} else {
path.to_path_buf()
};
files.map.insert(
key.to_path_buf(),
files.insert(
path.to_path_buf(),
PathData {
reader: None,
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() {
/*
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 {
show_error!("{}", text::NO_FILES_REMAINING);
} else {
if !files.only_stdin_remaining() {
show_error!("{}", text::NO_FILES_REMAINING);
}
} else if !(settings.stdin_is_pipe_or_fifo && settings.paths.len() == 1) {
follow(&mut files, &mut settings)?;
}
}
if settings.return_code > 0 {
#[cfg(unix)]
if stdin_is_bad_fd() {
show_error!("-: {}", text::BAD_FD);
}
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!)
// NOTE:
// We force the use of kqueue with: features=["macos_kqueue"],
// because macOS only `kqueue` is suitable for our use case since `FSEvents` waits until
// file close util it delivers a modify event. See:
// We force the use of kqueue with: features=["macos_kqueue"].
// On macOS only `kqueue` is suitable for our use case because `FSEvents`
// waits for file close util it delivers a modify event. See:
// https://github.com/notify-rs/notify/issues/240
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`.
let mut orphans = Vec::new();
for path in files.map.keys() {
if path.is_file() {
if path.is_tailable() {
let path = get_path(path, settings);
watcher
.watch(&path.canonicalize().unwrap(), RecursiveMode::NonRecursive)
@ -616,7 +670,8 @@ fn follow(files: &mut FileHandling, settings: &mut Settings) -> UResult<()> {
.unwrap();
}
} 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();
// Open new file and seek to End:
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:
files.map.insert(
new_path.to_owned(),
@ -1006,9 +1061,20 @@ struct 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 {
for path in self.map.keys() {
if path.is_tailable() {
if path.is_tailable() || path.is_stdin() {
return true;
}
}
@ -1349,7 +1415,9 @@ trait PathExt {
impl PathExt for Path {
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) {
println!("==> {} <==", self.display());