1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 19:47:45 +00:00

tail: refactor and fixes to pass more GNU test-suite checks

* add fixes to pass:
    - tail-2/F-vs-rename.sh
    - tail-2/follow-name.sh
    - tail-2/inotify-hash-abuse.sh
    - tail-2/inotify-only-regular.sh
    - tail-2/retry.sh
* add/improve documentation
This commit is contained in:
Jan Scheer 2022-06-02 17:11:51 +02:00
parent 94fe42634b
commit 70fed83305
No known key found for this signature in database
GPG key ID: C62AD4C29E2B9828

View file

@ -45,8 +45,6 @@ use uucore::lines::lines;
use uucore::parse_size::{parse_size, ParseSizeError}; use uucore::parse_size::{parse_size, ParseSizeError};
use uucore::ringbuffer::RingBuffer; use uucore::ringbuffer::RingBuffer;
#[cfg(unix)]
use std::fs::metadata;
#[cfg(unix)] #[cfg(unix)]
use std::os::unix::fs::MetadataExt; use std::os::unix::fs::MetadataExt;
#[cfg(unix)] #[cfg(unix)]
@ -93,7 +91,7 @@ pub mod options {
pub static FOLLOW_RETRY: &str = "F"; pub static FOLLOW_RETRY: &str = "F";
pub static MAX_UNCHANGED_STATS: &str = "max-unchanged-stats"; pub static MAX_UNCHANGED_STATS: &str = "max-unchanged-stats";
pub static ARG_FILES: &str = "files"; pub static ARG_FILES: &str = "files";
pub static PRESUME_INPUT_PIPE: &str = "-presume-input-pipe"; pub static PRESUME_INPUT_PIPE: &str = "-presume-input-pipe"; // NOTE: three hyphens is correct
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
@ -156,7 +154,9 @@ impl Settings {
Err(_) => return Err(format!("invalid number of seconds: {}", s.quote())), Err(_) => return Err(format!("invalid number of seconds: {}", s.quote())),
} }
} }
settings.sleep_sec /= 100; // NOTE: value decreased to pass timing sensitive GNU tests // NOTE: Value decreased to accommodate for discrepancies. Divisor chosen
// empirically in order to pass timing sensitive GNU test-suite checks.
settings.sleep_sec /= 100;
if let Some(s) = matches.value_of(options::MAX_UNCHANGED_STATS) { if let Some(s) = matches.value_of(options::MAX_UNCHANGED_STATS) {
settings.max_unchanged_stats = match s.parse::<u32>() { settings.max_unchanged_stats = match s.parse::<u32>() {
@ -187,7 +187,7 @@ impl Settings {
} }
} }
let mut starts_with_plus = false; let mut starts_with_plus = false; // support for legacy format (+0)
let mode_and_beginning = if let Some(arg) = matches.value_of(options::BYTES) { let mode_and_beginning = if let Some(arg) = matches.value_of(options::BYTES) {
starts_with_plus = arg.starts_with('+'); starts_with_plus = arg.starts_with('+');
match parse_num(arg) { match parse_num(arg) {
@ -255,7 +255,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
} }
}; };
// skip expansive fstat check if PRESUME_INPUT_PIPE is selected // skip expensive call to fstat if PRESUME_INPUT_PIPE is selected
if !args.stdin_is_pipe_or_fifo { if !args.stdin_is_pipe_or_fifo {
args.stdin_is_pipe_or_fifo = stdin_is_pipe_or_fifo(); args.stdin_is_pipe_or_fifo = stdin_is_pipe_or_fifo();
} }
@ -273,6 +273,7 @@ fn uu_tail(mut settings: Settings) -> UResult<()> {
crash!(1, "cannot follow {} by name", text::DASH.quote()); crash!(1, "cannot follow {} by name", text::DASH.quote());
} }
// add '-' to paths
if !settings.paths.contains(&dash) && settings.stdin_is_pipe_or_fifo if !settings.paths.contains(&dash) && settings.stdin_is_pipe_or_fifo
|| settings.paths.is_empty() && !settings.stdin_is_pipe_or_fifo || settings.paths.is_empty() && !settings.stdin_is_pipe_or_fifo
{ {
@ -301,10 +302,13 @@ fn uu_tail(mut settings: Settings) -> UResult<()> {
} }
} }
// TODO: is there a better way to check for a readable stdin?
let mut buf = [0; 0]; // empty buffer to check if stdin().read().is_err() let mut buf = [0; 0]; // empty buffer to check if stdin().read().is_err()
let stdin_read_possible = settings.stdin_is_pipe_or_fifo && stdin().read(&mut buf).is_ok(); let stdin_read_possible = settings.stdin_is_pipe_or_fifo && stdin().read(&mut buf).is_ok();
if !path.is_stdin() && !path.is_tailable() { let path_is_tailable = path.is_tailable();
if !path.is_stdin() && !path_is_tailable {
if settings.follow == Some(FollowMode::Descriptor) && settings.retry { if settings.follow == Some(FollowMode::Descriptor) && settings.retry {
show_warning!("--retry only effective for the initial open"); show_warning!("--retry only effective for the initial open");
} }
@ -324,10 +328,10 @@ fn uu_tail(mut settings: Settings) -> UResult<()> {
let err_msg = "Is a directory".to_string(); let err_msg = "Is a directory".to_string();
// NOTE: On macOS path.is_dir() can be false for directories // NOTE: On macOS path.is_dir() can be false for directories
// if it was a redirect, e.g. ` tail < DIR` // if it was a redirect, e.g. `$ tail < DIR`
if !path.is_dir() { if !path.is_dir() {
// TODO: match against ErrorKind // TODO: match against ErrorKind if unstable
// if unstable library feature "io_error_more" becomes stable // library feature "io_error_more" becomes stable
// if let Err(e) = stdin().read(&mut buf) { // if let Err(e) = stdin().read(&mut buf) {
// if e.kind() != std::io::ErrorKind::IsADirectory { // if e.kind() != std::io::ErrorKind::IsADirectory {
// err_msg = e.message.to_string(); // err_msg = e.message.to_string();
@ -359,9 +363,9 @@ fn uu_tail(mut settings: Settings) -> UResult<()> {
} }
} }
let md = path.metadata().ok(); let metadata = path.metadata().ok();
if display_name.is_stdin() && path.is_tailable() { if display_name.is_stdin() && path_is_tailable {
if settings.verbose { if settings.verbose {
files.print_header(Path::new(text::STDIN_HEADER), !first_header); files.print_header(Path::new(text::STDIN_HEADER), !first_header);
first_header = false; first_header = false;
@ -371,7 +375,7 @@ fn uu_tail(mut settings: Settings) -> UResult<()> {
if !stdin_is_bad_fd() { if !stdin_is_bad_fd() {
unbounded_tail(&mut reader, &settings)?; unbounded_tail(&mut reader, &settings)?;
if settings.follow == Some(FollowMode::Descriptor) { if settings.follow == Some(FollowMode::Descriptor) {
// Insert `stdin` into `files.map`. // Insert `stdin` into `files.map`
files.insert( files.insert(
path.to_path_buf(), path.to_path_buf(),
PathData { PathData {
@ -396,7 +400,7 @@ fn uu_tail(mut settings: Settings) -> UResult<()> {
); );
} }
} }
} else if path.is_tailable() { } else if path_is_tailable {
match File::open(&path) { match File::open(&path) {
Ok(mut file) => { Ok(mut file) => {
if settings.verbose { if settings.verbose {
@ -405,7 +409,7 @@ fn uu_tail(mut settings: Settings) -> UResult<()> {
} }
let mut reader; let mut reader;
if is_seekable(&mut file) && get_block_size(md.as_ref().unwrap()) > 0 { if file.is_seekable() && metadata.as_ref().unwrap().get_block_size() > 0 {
bounded_tail(&mut file, &settings); bounded_tail(&mut file, &settings);
reader = BufReader::new(file); reader = BufReader::new(file);
} else { } else {
@ -413,12 +417,12 @@ fn uu_tail(mut settings: Settings) -> UResult<()> {
unbounded_tail(&mut reader, &settings)?; unbounded_tail(&mut reader, &settings)?;
} }
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.insert( files.insert(
path.canonicalize()?, path.canonicalize()?,
PathData { PathData {
reader: Some(Box::new(reader)), reader: Some(Box::new(reader)),
metadata: md, metadata,
display_name, display_name,
}, },
); );
@ -442,12 +446,12 @@ fn uu_tail(mut settings: Settings) -> UResult<()> {
if path.is_relative() { if path.is_relative() {
path = std::env::current_dir()?.join(&path); path = std::env::current_dir()?.join(&path);
} }
// Insert non-is_tailable() paths into `files.map`. // Insert non-is_tailable() paths into `files.map`
files.insert( files.insert(
path.to_path_buf(), path.to_path_buf(),
PathData { PathData {
reader: None, reader: None,
metadata: md, metadata,
display_name, display_name,
}, },
); );
@ -644,7 +648,7 @@ fn follow(files: &mut FileHandling, settings: &mut Settings) -> UResult<()> {
// macOS: FSEvents / kqueue // macOS: FSEvents / kqueue
// Windows: ReadDirectoryChangesWatcher // Windows: ReadDirectoryChangesWatcher
// FreeBSD / NetBSD / OpenBSD / DragonflyBSD: kqueue // FreeBSD / NetBSD / OpenBSD / DragonflyBSD: kqueue
// Fallback: polling (default delay is 30 seconds!) // Fallback: polling every n seconds
// NOTE: // NOTE:
// We force the use of kqueue with: features=["macos_kqueue"]. // We force the use of kqueue with: features=["macos_kqueue"].
@ -654,6 +658,7 @@ fn follow(files: &mut FileHandling, settings: &mut Settings) -> UResult<()> {
let mut watcher: Box<dyn Watcher>; let mut watcher: Box<dyn Watcher>;
if settings.use_polling || RecommendedWatcher::kind() == WatcherKind::PollWatcher { if settings.use_polling || RecommendedWatcher::kind() == WatcherKind::PollWatcher {
settings.use_polling = true; // We have to use polling because there's no supported backend
let config = notify::poll::PollWatcherConfig { let config = notify::poll::PollWatcherConfig {
poll_interval: settings.sleep_sec, poll_interval: settings.sleep_sec,
..Default::default() ..Default::default()
@ -672,6 +677,7 @@ fn follow(files: &mut FileHandling, settings: &mut Settings) -> UResult<()> {
text::BACKEND text::BACKEND
); );
settings.exit_code = 1; settings.exit_code = 1;
settings.use_polling = true;
let config = notify::poll::PollWatcherConfig { let config = notify::poll::PollWatcherConfig {
poll_interval: settings.sleep_sec, poll_interval: settings.sleep_sec,
..Default::default() ..Default::default()
@ -691,8 +697,8 @@ fn follow(files: &mut FileHandling, settings: &mut Settings) -> UResult<()> {
if path.is_tailable() { if path.is_tailable() {
// TODO: [2022-05; jhscheer] also add `file` (not just parent) to please // TODO: [2022-05; jhscheer] also add `file` (not just parent) to please
// "gnu/tests/tail-2/inotify-rotate-resourced.sh" because it is looking for // "gnu/tests/tail-2/inotify-rotate-resourced.sh" because it is looking for
// 2x "inotify_add_watch" and 1x "inotify_rm_watch" // for syscalls: 2x "inotify_add_watch" and 1x "inotify_rm_watch"
let path = get_path(path, settings); let path = path.watchable(settings);
watcher watcher
.watch(&path.canonicalize()?, RecursiveMode::NonRecursive) .watch(&path.canonicalize()?, RecursiveMode::NonRecursive)
.unwrap(); .unwrap();
@ -706,11 +712,12 @@ fn follow(files: &mut FileHandling, settings: &mut Settings) -> UResult<()> {
.unwrap(); .unwrap();
} }
} else { } else {
// TODO: [2021-10; jhscheer] do we need to handle non-is_tailable without follow/retry? // TODO: [2022-05; jhscheer] do we need to handle this case?
todo!(); unimplemented!();
} }
} }
// TODO: [2021-10; jhscheer]
let mut _event_counter = 0; let mut _event_counter = 0;
let mut _timeout_counter = 0; let mut _timeout_counter = 0;
@ -725,53 +732,52 @@ fn follow(files: &mut FileHandling, settings: &mut Settings) -> UResult<()> {
if settings.retry && settings.follow == Some(FollowMode::Name) { if settings.retry && settings.follow == Some(FollowMode::Name) {
for new_path in &orphans { for new_path in &orphans {
if new_path.exists() { if new_path.exists() {
let display_name = files.map.get(new_path).unwrap().display_name.to_path_buf(); let pd = files.map.get(new_path).unwrap();
if new_path.is_file() && files.map.get(new_path).unwrap().metadata.is_none() { let md = new_path.metadata().unwrap();
show_error!("{} has appeared; following new file", display_name.quote()); if md.is_tailable() && pd.reader.is_none() {
show_error!(
"{} has appeared; following new file",
pd.display_name.quote()
);
if let Ok(new_path_canonical) = new_path.canonicalize() { if let Ok(new_path_canonical) = new_path.canonicalize() {
files.update_metadata(&new_path_canonical, None); files.update_metadata(&new_path_canonical, Some(md));
files.update_reader(&new_path_canonical)?; files.update_reader(&new_path_canonical)?;
read_some = files.tail_file(&new_path_canonical, settings.verbose)?; read_some = files.tail_file(&new_path_canonical, settings.verbose)?;
let new_path = get_path(&new_path_canonical, settings); let new_path = new_path_canonical.watchable(settings);
watcher watcher
.watch(&new_path, RecursiveMode::NonRecursive) .watch(&new_path, RecursiveMode::NonRecursive)
.unwrap(); .unwrap();
} else { } else {
unreachable!(); unreachable!();
} }
} else if new_path.is_dir() {
// TODO: [2021-10; jhscheer] does is_dir() need handling?
todo!();
} }
} }
} }
} }
// Poll all watched files manually to not miss changes // Poll all watched files manually to not miss changes due to timing
// due to timing conflicts with `Notify::PollWatcher` // conflicts with `Notify::PollWatcher`.
// e.g. `echo "X1" > missing ; sleep 0.1 ; echo "X" > missing ;` // NOTE: This is a workaround because PollWatcher tends to miss events.
// this is relevant to pass: // e.g. `echo "X1" > missing ; sleep 0.1 ; echo "X" > missing ;` should trigger a
// https://github.com/coreutils/coreutils/blob/e087525091b8f0a15eb2354f71032597d5271599/tests/tail-2/retry.sh#L92 // truncation event, but PollWatcher doesn't recognize it.
// TODO: [2022-05; jhscheer] still necessary? // This is relevant to pass, e.g.: "gnu/tests/tail-2/truncate.sh"
if settings.use_polling { if settings.use_polling && settings.follow.is_some() {
let mut paths = Vec::new(); for path in &files
for path in files.map.keys() { .map
if path.is_file() { .keys()
paths.push(path.to_path_buf()); .filter(|p| p.is_tailable())
} .map(|p| p.to_path_buf())
} .collect::<Vec<PathBuf>>()
for path in &mut paths { {
if let Ok(new_md) = path.metadata() { if let Ok(new_md) = path.metadata() {
if let Some(old_md) = &files.map.get(path).unwrap().metadata { let pd = files.map.get(path).unwrap();
// TODO: [2021-10; jhscheer] reduce duplicate code if let Some(old_md) = &pd.metadata {
let display_name = files.map.get(path).unwrap().display_name.to_path_buf(); if old_md.is_tailable()
if new_md.len() <= old_md.len() && new_md.is_tailable()
&& new_md.modified()? != old_md.modified()? && old_md.got_truncated(&new_md)?
&& new_md.is_file()
&& old_md.is_file()
{ {
show_error!("{}: file truncated", display_name.display()); show_error!("{}: file truncated", pd.display_name.display());
files.update_metadata(path, None); files.update_metadata(path, Some(new_md));
files.update_reader(path)?; files.update_reader(path)?;
} }
} }
@ -785,12 +791,14 @@ fn follow(files: &mut FileHandling, settings: &mut Settings) -> UResult<()> {
_event_counter += 1; _event_counter += 1;
_timeout_counter = 0; _timeout_counter = 0;
} }
match rx_result { match rx_result {
Ok(Ok(event)) => { Ok(Ok(event)) => {
// eprintln!("=={:=>3}=====================dbg===", _event_counter); // eprintln!("=={:=>3}=====================dbg===", _event_counter);
// dbg!(&event); // dbg!(&event);
// dbg!(files.map.keys()); // dbg!(files.map.keys());
// dbg!(&files.last); // dbg!(&files.last);
// dbg!(&orphans);
// eprintln!("=={:=>3}=====================dbg===", _event_counter); // eprintln!("=={:=>3}=====================dbg===", _event_counter);
handle_event(&event, files, settings, &mut watcher, &mut orphans)?; handle_event(&event, files, settings, &mut watcher, &mut orphans)?;
} }
@ -859,101 +867,79 @@ fn handle_event(
.unwrap() .unwrap()
.display_name .display_name
.to_path_buf(); .to_path_buf();
match event.kind { match event.kind {
EventKind::Access(AccessKind::Close(AccessMode::Write)) EventKind::Modify(ModifyKind::Metadata(MetadataKind::Any))
| EventKind::Modify(ModifyKind::Metadata(MetadataKind::Any)) // | EventKind::Access(AccessKind::Close(AccessMode::Write))
| EventKind::Modify(ModifyKind::Metadata(MetadataKind::WriteTime)) | EventKind::Modify(ModifyKind::Metadata(MetadataKind::WriteTime))
| EventKind::Modify(ModifyKind::Data(DataChange::Any)) => { | EventKind::Create(CreateKind::File)
if let Ok(new_md) = event_path.metadata() {
if let Some(old_md) = &files.map.get(event_path).unwrap().metadata {
if new_md.is_file() && !old_md.is_file() {
show_error!(
"{} has appeared; following new file",
display_name.quote()
);
files.update_metadata(event_path, None);
files.update_reader(event_path)?;
} else if !new_md.is_file() && old_md.is_file() {
show_error!(
"{} has been replaced with an untailable file",
display_name.quote()
);
files.map.insert(
event_path.to_path_buf(),
PathData {
reader: None,
metadata: None,
display_name,
},
);
files.update_metadata(event_path, None);
} else if new_md.len() <= old_md.len()
&& new_md.modified()? != old_md.modified()?
{
show_error!("{}: file truncated", display_name.display());
files.update_metadata(event_path, None);
files.update_reader(event_path)?;
}
}
}
}
EventKind::Create(CreateKind::File)
| EventKind::Create(CreateKind::Folder) | EventKind::Create(CreateKind::Folder)
| EventKind::Create(CreateKind::Any) | EventKind::Create(CreateKind::Any)
| EventKind::Modify(ModifyKind::Data(DataChange::Any))
| EventKind::Modify(ModifyKind::Name(RenameMode::To)) => { | EventKind::Modify(ModifyKind::Name(RenameMode::To)) => {
if event_path.is_file() { if let Ok(new_md) = event_path.metadata() {
if settings.follow.is_some() { let is_tailable = new_md.is_tailable();
let msg = if (files.map.get(event_path).unwrap().metadata.is_none()) let pd = files.map.get(event_path).unwrap();
|| (!settings.use_polling && settings.retry) { if let Some(old_md) = &pd.metadata {
format!("{} has appeared", display_name.quote()) if is_tailable {
} else { // We resume tracking from the start of the file,
format!("{} has been replaced", display_name.quote()) // assuming it has been truncated to 0. This mimics GNU's `tail`
}; // behavior and is the usual truncation operation for log files.
show_error!("{}; following new file", msg); if !old_md.is_tailable() {
} show_error!( "{} has become accessible", display_name.quote());
files.update_reader(event_path)?;
// Since Files are automatically closed when they go out of } else if pd.reader.is_none() {
// scope, we resume tracking from the start of the file, show_error!( "{} has appeared; following new file", display_name.quote());
// assuming it has been truncated to 0. This mimics GNU's `tail` files.update_reader(event_path)?;
// behavior and is the usual truncation operation for log files. } else if event.kind == EventKind::Modify(ModifyKind::Name(RenameMode::To)) {
// files.update_metadata(event_path, None); show_error!( "{} has been replaced; following new file", display_name.quote());
files.update_reader(event_path)?; files.update_reader(event_path)?;
if settings.follow == Some(FollowMode::Name) && settings.retry { } else if old_md.got_truncated(&new_md)? {
// TODO: [2021-10; jhscheer] add test for this show_error!("{}: file truncated", display_name.display());
// Path has appeared, it's not an orphan any more. files.update_reader(event_path)?;
orphans.retain(|path| path != event_path); }
} } else if !is_tailable && old_md.is_tailable() {
} else { if pd.reader.is_some() {
// If the path pointed to a file and now points to something else: files.reset_reader(event_path);
let md = &files.map.get(event_path).unwrap().metadata; } else {
if md.is_none() || md.as_ref().unwrap().is_file() { show_error!(
let msg = "has been replaced with an untailable file"; "{} has been replaced with an untailable file",
display_name.quote()
);
}
}
} else if is_tailable {
show_error!( "{} has appeared; following new file", display_name.quote());
files.update_reader(event_path)?;
} else if settings.retry {
if settings.follow == Some(FollowMode::Descriptor) { if settings.follow == Some(FollowMode::Descriptor) {
show_error!( show_error!(
"{} {}; giving up on this name", "{} has been replaced with an untailable file; giving up on this name",
display_name.quote(), display_name.quote()
msg
); );
let _ = watcher.unwatch(event_path); let _ = watcher.unwatch(event_path);
files.map.remove(event_path).unwrap(); files.map.remove(event_path).unwrap();
if files.map.is_empty() { if files.map.is_empty() {
crash!(1, "{}", text::NO_FILES_REMAINING); crash!(1, "{}", text::NO_FILES_REMAINING);
} }
} else if settings.follow == Some(FollowMode::Name) { } else {
// TODO: [2021-10; jhscheer] add test for this show_error!(
files.update_metadata(event_path, None); "{} has been replaced with an untailable file",
show_error!("{} {}", display_name.quote(), msg); display_name.quote()
);
} }
} }
files.update_metadata(event_path, Some(new_md));
} }
} }
EventKind::Remove(RemoveKind::File) | EventKind::Remove(RemoveKind::Any) EventKind::Remove(RemoveKind::File)
// | EventKind::Modify(ModifyKind::Name(RenameMode::Any)) | EventKind::Remove(RemoveKind::Any)
| EventKind::Modify(ModifyKind::Name(RenameMode::From)) => { // | EventKind::Modify(ModifyKind::Name(RenameMode::Any))
| EventKind::Modify(ModifyKind::Name(RenameMode::From)) => {
if settings.follow == Some(FollowMode::Name) { if settings.follow == Some(FollowMode::Name) {
if settings.retry { if settings.retry {
if let Some(old_md) = &files.map.get(event_path).unwrap().metadata { if let Some(old_md) = &files.map.get_mut(event_path).unwrap().metadata {
if old_md.is_file() { if old_md.is_tailable() {
show_error!( show_error!(
"{} has become inaccessible: {}", "{} has become inaccessible: {}",
display_name.quote(), display_name.quote(),
@ -968,33 +954,31 @@ fn handle_event(
text::BACKEND text::BACKEND
); );
orphans.push(event_path.to_path_buf()); orphans.push(event_path.to_path_buf());
let _ = watcher.unwatch(event_path);
} }
let _ = watcher.unwatch(event_path);
} else { } else {
show_error!("{}: {}", display_name.display(), text::NO_SUCH_FILE); show_error!("{}: {}", display_name.display(), text::NO_SUCH_FILE);
if !files.files_remaining() && settings.use_polling { if !files.files_remaining() && settings.use_polling {
crash!(1, "{}", text::NO_FILES_REMAINING); crash!(1, "{}", text::NO_FILES_REMAINING);
} }
} }
// Update `files.map` to indicate that `event_path` files.reset_reader(event_path);
// is not an existing file anymore.
files.map.insert(
event_path.to_path_buf(),
PathData {
reader: None,
metadata: None,
display_name,
},
);
} else if settings.follow == Some(FollowMode::Descriptor) && settings.retry { } else if settings.follow == Some(FollowMode::Descriptor) && settings.retry {
// --retry only effective for the initial open // --retry only effective for the initial open
let _ = watcher.unwatch(event_path); let _ = watcher.unwatch(event_path);
files.map.remove(event_path).unwrap(); files.map.remove(event_path).unwrap();
} else if settings.use_polling && event.kind == EventKind::Remove(RemoveKind::Any) {
// BUG:
// The watched file was removed. Since we're using Polling, this
// could be a rename. We can't tell because `notify::PollWatcher` doesn't
// recognize renames properly.
// Ideally we want to call seek to offset 0 on the file handle.
// But because we only have access to `PathData::reader` as `BufRead`,
// we cannot seek to 0 with `BufReader::seek_relative`.
// Also because we don't have the new name, we cannot work around this
// by simply reopening the file.
} }
} }
// EventKind::Modify(ModifyKind::Name(RenameMode::Any))
// | EventKind::Modify(ModifyKind::Name(RenameMode::From)) => {
// }
EventKind::Modify(ModifyKind::Name(RenameMode::Both)) => { EventKind::Modify(ModifyKind::Name(RenameMode::Both)) => {
// NOTE: For `tail -f a`, keep tracking additions to b after `mv a b` // NOTE: For `tail -f a`, keep tracking additions to b after `mv a b`
// (gnu/tests/tail-2/descriptor-vs-rename.sh) // (gnu/tests/tail-2/descriptor-vs-rename.sh)
@ -1005,16 +989,18 @@ fn handle_event(
// BUG: As a result, there's a bug if polling is used: // BUG: As a result, there's a bug if polling is used:
// $ tail -f file_a ---disable-inotify // $ tail -f file_a ---disable-inotify
// $ mv file_a file_b // $ mv file_a file_b
// $ echo A >> file_b
// $ echo A >> file_a // $ echo A >> file_a
// The last append to file_a is printed, however this shouldn't be because // The last append to file_a is printed, however this shouldn't be because
// after the "mv" tail should only follow "file_b". // after the "mv" tail should only follow "file_b".
// TODO: [2022-05; jhscheer] add test for this bug
if settings.follow == Some(FollowMode::Descriptor) { if settings.follow == Some(FollowMode::Descriptor) {
let new_path = event.paths.last().unwrap().canonicalize()?; let new_path = event.paths.last().unwrap().canonicalize()?;
// Open new file and seek to End: // Open new file and seek to End:
let mut file = File::open(&new_path)?; let mut file = File::open(&new_path)?;
file.seek(SeekFrom::End(0))?; file.seek(SeekFrom::End(0))?;
// Add new reader and remove old reader: // Add new reader but keep old display name
files.map.insert( files.map.insert(
new_path.to_owned(), new_path.to_owned(),
PathData { PathData {
@ -1023,13 +1009,14 @@ fn handle_event(
display_name, // mimic GNU's tail and show old name in header display_name, // mimic GNU's tail and show old name in header
}, },
); );
// Remove old reader
files.map.remove(event_path).unwrap(); files.map.remove(event_path).unwrap();
if files.last.as_ref().unwrap() == event_path { if files.last.as_ref().unwrap() == event_path {
files.last = Some(new_path.to_owned()); files.last = Some(new_path.to_owned());
} }
// Unwatch old path and watch new path: // Unwatch old path and watch new path
let _ = watcher.unwatch(event_path); let _ = watcher.unwatch(event_path);
let new_path = get_path(&new_path, settings); let new_path = new_path.watchable(settings);
watcher watcher
.watch( .watch(
&new_path.canonicalize()?, &new_path.canonicalize()?,
@ -1045,34 +1032,12 @@ fn handle_event(
Ok(()) Ok(())
} }
fn get_path(path: &Path, settings: &Settings) -> PathBuf {
if cfg!(target_os = "linux") || settings.use_polling {
// NOTE: Using the parent directory here instead of the file is a workaround.
// On Linux the watcher can crash for rename/delete/move operations if a file is watched directly.
// This workaround follows the recommendation of the notify crate authors:
// > On some platforms, if the `path` is renamed or removed while being watched, behavior may
// > be unexpected. See discussions in [#165] and [#166]. If less surprising behavior is wanted
// > one may non-recursively watch the _parent_ directory as well and manage related events.
let parent = path
.parent()
.unwrap_or_else(|| crash!(1, "cannot watch parent directory of {}", path.display()));
// TODO: [2021-10; jhscheer] add test for this - "cannot watch parent directory"
if parent.is_dir() {
parent.to_path_buf()
} else {
PathBuf::from(".")
}
} else {
path.to_path_buf()
}
}
/// Data structure to keep a handle on the BufReader, Metadata /// Data structure to keep a handle on the BufReader, Metadata
/// and the display_name (header_name) of files that are being followed. /// and the display_name (header_name) of files that are being followed.
struct PathData { struct PathData {
reader: Option<Box<dyn BufRead>>, reader: Option<Box<dyn BufRead>>,
metadata: Option<Metadata>, metadata: Option<Metadata>,
display_name: PathBuf, // the path the user provided, used for headers display_name: PathBuf, // the path as provided by user input, used for headers
} }
/// Data structure to keep a handle on files to follow. /// Data structure to keep a handle on files to follow.
@ -1086,17 +1051,20 @@ struct FileHandling {
} }
impl FileHandling { impl FileHandling {
/// Insert new `PathData` into the HashMap
fn insert(&mut self, k: PathBuf, v: PathData) -> Option<PathData> { fn insert(&mut self, k: PathBuf, v: PathData) -> Option<PathData> {
self.last = Some(k.to_owned()); self.last = Some(k.to_owned());
self.map.insert(k, v) self.map.insert(k, v)
} }
/// Return true if there is only stdin remaining
fn only_stdin_remaining(&self) -> bool { fn only_stdin_remaining(&self) -> bool {
self.map.len() == 1 self.map.len() == 1
&& (self.map.contains_key(Path::new(text::DASH)) && (self.map.contains_key(Path::new(text::DASH)))
|| self.map.contains_key(Path::new(text::DEV_STDIN))) // TODO: still needed? // || self.map.contains_key(Path::new(text::DEV_STDIN))) // TODO: still needed?
} }
/// Return true if there is at least one "tailable" path (or stdin) remaining
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() || path.is_stdin() { if path.is_tailable() || path.is_stdin() {
@ -1106,27 +1074,35 @@ impl FileHandling {
false false
} }
/// Set `reader` to None to indicate that `path` is not an existing file anymore.
fn reset_reader(&mut self, path: &Path) {
assert!(self.map.contains_key(path));
self.map.get_mut(path).unwrap().reader = None;
}
/// Reopen the file at the monitored `path`
fn update_reader(&mut self, path: &Path) -> UResult<()> { fn update_reader(&mut self, path: &Path) -> UResult<()> {
assert!(self.map.contains_key(path)); assert!(self.map.contains_key(path));
if let Some(pd) = self.map.get_mut(path) { // BUG:
let new_reader = BufReader::new(File::open(&path)?); // If it's not necessary to reopen a file, GNU's tail calls seek to offset 0.
pd.reader = Some(Box::new(new_reader)); // However we can't call seek here because `BufRead` does not implement `Seek`.
} // As a workaround we always reopen the file even though this might not always
// be necessary.
self.map.get_mut(path).unwrap().reader = Some(Box::new(BufReader::new(File::open(&path)?)));
Ok(()) Ok(())
} }
fn update_metadata(&mut self, path: &Path, md: Option<Metadata>) { /// Reload metadata from `path`, or `metadata`
fn update_metadata(&mut self, path: &Path, metadata: Option<Metadata>) {
assert!(self.map.contains_key(path)); assert!(self.map.contains_key(path));
if let Some(pd) = self.map.get_mut(path) { self.map.get_mut(path).unwrap().metadata = if metadata.is_some() {
if let Some(md) = md { metadata
pd.metadata = Some(md); } else {
} else { path.metadata().ok()
pd.metadata = path.metadata().ok(); };
}
}
} }
// This reads from the current seek position forward. /// Read `path` from the current seek position forward
fn read_file(&mut self, path: &Path, buffer: &mut Vec<u8>) -> UResult<bool> { fn read_file(&mut self, path: &Path, buffer: &mut Vec<u8>) -> UResult<bool> {
assert!(self.map.contains_key(path)); assert!(self.map.contains_key(path));
let mut read_some = false; let mut read_some = false;
@ -1145,6 +1121,7 @@ impl FileHandling {
Ok(read_some) Ok(read_some)
} }
/// Print `buffer` to stdout
fn print_file(&self, buffer: &[u8]) -> UResult<()> { fn print_file(&self, buffer: &[u8]) -> UResult<()> {
let mut stdout = stdout(); let mut stdout = stdout();
stdout stdout
@ -1153,6 +1130,7 @@ impl FileHandling {
Ok(()) Ok(())
} }
/// Read new data from `path` and print it to stdout
fn tail_file(&mut self, path: &Path, verbose: bool) -> UResult<bool> { fn tail_file(&mut self, path: &Path, verbose: bool) -> UResult<bool> {
let mut buffer = vec![]; let mut buffer = vec![];
let read_some = self.read_file(path, &mut buffer)?; let read_some = self.read_file(path, &mut buffer)?;
@ -1168,17 +1146,17 @@ impl FileHandling {
Ok(read_some) Ok(read_some)
} }
/// Decide if printing `path` needs a header based on when it was last printed
fn needs_header(&self, path: &Path, verbose: bool) -> bool { fn needs_header(&self, path: &Path, verbose: bool) -> bool {
if verbose { if verbose {
if let Some(ref last) = self.last { if let Some(ref last) = self.last {
if let Ok(path) = path.canonicalize() { return !last.eq(&path);
return !last.eq(&path);
}
} }
} }
false false
} }
/// Print header for `path` to stdout
fn print_header(&self, path: &Path, needs_newline: bool) { fn print_header(&self, path: &Path, needs_newline: bool) {
println!( println!(
"{}==> {} <==", "{}==> {} <==",
@ -1187,6 +1165,7 @@ impl FileHandling {
); );
} }
/// Wrapper for `PathData::display_name`
fn display_name(&self, path: &Path) -> String { fn display_name(&self, path: &Path) -> String {
if let Some(path) = self.map.get(path) { if let Some(path) = self.map.get(path) {
path.display_name.display().to_string() path.display_name.display().to_string()
@ -1415,12 +1394,6 @@ fn unbounded_tail<T: Read>(reader: &mut BufReader<T>, settings: &Settings) -> UR
Ok(()) Ok(())
} }
fn is_seekable<T: Seek>(file: &mut T) -> bool {
file.seek(SeekFrom::Current(0)).is_ok()
&& file.seek(SeekFrom::End(0)).is_ok()
&& file.seek(SeekFrom::Start(0)).is_ok()
}
fn parse_num(src: &str) -> Result<(u64, bool), ParseSizeError> { fn parse_num(src: &str) -> Result<(u64, bool), ParseSizeError> {
let mut size_string = src.trim(); let mut size_string = src.trim();
let mut starting_with = false; let mut starting_with = false;
@ -1440,17 +1413,6 @@ fn parse_num(src: &str) -> Result<(u64, bool), ParseSizeError> {
parse_size(size_string).map(|n| (n, starting_with)) parse_size(size_string).map(|n| (n, starting_with))
} }
fn get_block_size(md: &Metadata) -> u64 {
#[cfg(unix)]
{
md.blocks()
}
#[cfg(not(unix))]
{
md.len()
}
}
pub fn stdin_is_pipe_or_fifo() -> bool { pub fn stdin_is_pipe_or_fifo() -> bool {
#[cfg(unix)] #[cfg(unix)]
{ {
@ -1473,34 +1435,106 @@ pub fn stdin_is_bad_fd() -> bool {
false false
} }
trait PathExt { trait FileExtTail {
fn is_seekable(&mut self) -> bool;
}
impl FileExtTail for File {
fn is_seekable(&mut self) -> bool {
self.seek(SeekFrom::Current(0)).is_ok()
&& self.seek(SeekFrom::End(0)).is_ok()
&& self.seek(SeekFrom::Start(0)).is_ok()
}
}
trait MetadataExtTail {
fn is_tailable(&self) -> bool;
fn got_truncated(
&self,
other: &Metadata,
) -> Result<bool, Box<(dyn uucore::error::UError + 'static)>>;
fn get_block_size(&self) -> u64;
}
impl MetadataExtTail for Metadata {
fn is_tailable(&self) -> bool {
let ft = self.file_type();
#[cfg(unix)]
{
ft.is_file() || ft.is_char_device() || ft.is_fifo()
}
#[cfg(not(unix))]
{
ft.is_file()
}
}
/// Return true if the file was modified and is now shorter
fn got_truncated(
&self,
other: &Metadata,
) -> Result<bool, Box<(dyn uucore::error::UError + 'static)>> {
Ok(other.len() < self.len() && other.modified()? != self.modified()?)
}
fn get_block_size(&self) -> u64 {
#[cfg(unix)]
{
self.blocks()
}
#[cfg(not(unix))]
{
self.len()
}
}
}
trait PathExtTail {
fn is_stdin(&self) -> bool; fn is_stdin(&self) -> bool;
fn is_orphan(&self) -> bool; fn is_orphan(&self) -> bool;
fn is_tailable(&self) -> bool; fn is_tailable(&self) -> bool;
fn watchable(&self, settings: &Settings) -> PathBuf;
} }
impl PathExt for Path { impl PathExtTail for Path {
fn is_stdin(&self) -> bool { fn is_stdin(&self) -> bool {
self.eq(Self::new(text::DASH)) self.eq(Self::new(text::DASH))
|| self.eq(Self::new(text::DEV_STDIN)) || self.eq(Self::new(text::DEV_STDIN))
|| self.eq(Self::new(text::STDIN_HEADER)) || self.eq(Self::new(text::STDIN_HEADER))
} }
/// Return true if `path` does not have an existing parent directory
fn is_orphan(&self) -> bool { fn is_orphan(&self) -> bool {
!matches!(self.parent(), Some(parent) if parent.is_dir()) !matches!(self.parent(), Some(parent) if parent.is_dir())
} }
/// Return true if `path` is is a file type that can be tailed
fn is_tailable(&self) -> bool { fn is_tailable(&self) -> bool {
#[cfg(unix)] self.is_file() || self.exists() && self.metadata().unwrap().is_tailable()
{ }
// TODO: [2021-10; jhscheer] what about fifos?
self.is_file() /// Wrapper for `path` to use for `notify::Watcher::watch`.
|| self.exists() && { /// Will return a "watchable" parent directory if necessary.
let ft = metadata(self).unwrap().file_type(); /// Will panic if parent directory cannot be watched.
ft.is_char_device() || ft.is_fifo() fn watchable(&self, settings: &Settings) -> PathBuf {
} if cfg!(target_os = "linux") || settings.use_polling {
} // NOTE: Using the parent directory here instead of the file is a workaround.
#[cfg(not(unix))] // On Linux the watcher can crash for rename/delete/move operations if a file is watched directly.
{ // This workaround follows the recommendation of the notify crate authors:
self.is_file() // > On some platforms, if the `path` is renamed or removed while being watched, behavior may
// > be unexpected. See discussions in [#165] and [#166]. If less surprising behavior is wanted
// > one may non-recursively watch the _parent_ directory as well and manage related events.
let parent = self.parent().unwrap_or_else(|| {
crash!(1, "cannot watch parent directory of {}", self.display())
});
// TODO: [2021-10; jhscheer] add test for this - "cannot watch parent directory"
if parent.is_dir() {
parent.to_path_buf()
} else {
PathBuf::from(".")
}
} else {
self.to_path_buf()
} }
} }
} }