1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 11:37:44 +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::ringbuffer::RingBuffer;
#[cfg(unix)]
use std::fs::metadata;
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
#[cfg(unix)]
@ -93,7 +91,7 @@ pub mod options {
pub static FOLLOW_RETRY: &str = "F";
pub static MAX_UNCHANGED_STATS: &str = "max-unchanged-stats";
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)]
@ -156,7 +154,9 @@ impl Settings {
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) {
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) {
starts_with_plus = arg.starts_with('+');
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 {
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());
}
// add '-' to paths
if !settings.paths.contains(&dash) && 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 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 {
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();
// 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() {
// TODO: match against ErrorKind
// if unstable library feature "io_error_more" becomes stable
// TODO: match against ErrorKind if unstable
// library feature "io_error_more" becomes stable
// if let Err(e) = stdin().read(&mut buf) {
// if e.kind() != std::io::ErrorKind::IsADirectory {
// 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 {
files.print_header(Path::new(text::STDIN_HEADER), !first_header);
first_header = false;
@ -371,7 +375,7 @@ fn uu_tail(mut settings: Settings) -> UResult<()> {
if !stdin_is_bad_fd() {
unbounded_tail(&mut reader, &settings)?;
if settings.follow == Some(FollowMode::Descriptor) {
// Insert `stdin` into `files.map`.
// Insert `stdin` into `files.map`
files.insert(
path.to_path_buf(),
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) {
Ok(mut file) => {
if settings.verbose {
@ -405,7 +409,7 @@ fn uu_tail(mut settings: Settings) -> UResult<()> {
}
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);
reader = BufReader::new(file);
} else {
@ -413,12 +417,12 @@ fn uu_tail(mut settings: Settings) -> UResult<()> {
unbounded_tail(&mut reader, &settings)?;
}
if settings.follow.is_some() {
// Insert existing/file `path` into `files.map`.
// Insert existing/file `path` into `files.map`
files.insert(
path.canonicalize()?,
PathData {
reader: Some(Box::new(reader)),
metadata: md,
metadata,
display_name,
},
);
@ -442,12 +446,12 @@ fn uu_tail(mut settings: Settings) -> UResult<()> {
if path.is_relative() {
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(
path.to_path_buf(),
PathData {
reader: None,
metadata: md,
metadata,
display_name,
},
);
@ -644,7 +648,7 @@ fn follow(files: &mut FileHandling, settings: &mut Settings) -> UResult<()> {
// macOS: FSEvents / kqueue
// Windows: ReadDirectoryChangesWatcher
// FreeBSD / NetBSD / OpenBSD / DragonflyBSD: kqueue
// Fallback: polling (default delay is 30 seconds!)
// Fallback: polling every n seconds
// NOTE:
// 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>;
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 {
poll_interval: settings.sleep_sec,
..Default::default()
@ -672,6 +677,7 @@ fn follow(files: &mut FileHandling, settings: &mut Settings) -> UResult<()> {
text::BACKEND
);
settings.exit_code = 1;
settings.use_polling = true;
let config = notify::poll::PollWatcherConfig {
poll_interval: settings.sleep_sec,
..Default::default()
@ -691,8 +697,8 @@ fn follow(files: &mut FileHandling, settings: &mut Settings) -> UResult<()> {
if path.is_tailable() {
// 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
// 2x "inotify_add_watch" and 1x "inotify_rm_watch"
let path = get_path(path, settings);
// for syscalls: 2x "inotify_add_watch" and 1x "inotify_rm_watch"
let path = path.watchable(settings);
watcher
.watch(&path.canonicalize()?, RecursiveMode::NonRecursive)
.unwrap();
@ -706,11 +712,12 @@ fn follow(files: &mut FileHandling, settings: &mut Settings) -> UResult<()> {
.unwrap();
}
} else {
// TODO: [2021-10; jhscheer] do we need to handle non-is_tailable without follow/retry?
todo!();
// TODO: [2022-05; jhscheer] do we need to handle this case?
unimplemented!();
}
}
// TODO: [2021-10; jhscheer]
let mut _event_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) {
for new_path in &orphans {
if new_path.exists() {
let display_name = files.map.get(new_path).unwrap().display_name.to_path_buf();
if new_path.is_file() && files.map.get(new_path).unwrap().metadata.is_none() {
show_error!("{} has appeared; following new file", display_name.quote());
let pd = files.map.get(new_path).unwrap();
let md = new_path.metadata().unwrap();
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() {
files.update_metadata(&new_path_canonical, None);
files.update_metadata(&new_path_canonical, Some(md));
files.update_reader(&new_path_canonical)?;
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
.watch(&new_path, RecursiveMode::NonRecursive)
.unwrap();
} else {
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
// due to timing conflicts with `Notify::PollWatcher`
// e.g. `echo "X1" > missing ; sleep 0.1 ; echo "X" > missing ;`
// this is relevant to pass:
// https://github.com/coreutils/coreutils/blob/e087525091b8f0a15eb2354f71032597d5271599/tests/tail-2/retry.sh#L92
// TODO: [2022-05; jhscheer] still necessary?
if settings.use_polling {
let mut paths = Vec::new();
for path in files.map.keys() {
if path.is_file() {
paths.push(path.to_path_buf());
}
}
for path in &mut paths {
// Poll all watched files manually to not miss changes due to timing
// conflicts with `Notify::PollWatcher`.
// NOTE: This is a workaround because PollWatcher tends to miss events.
// e.g. `echo "X1" > missing ; sleep 0.1 ; echo "X" > missing ;` should trigger a
// truncation event, but PollWatcher doesn't recognize it.
// This is relevant to pass, e.g.: "gnu/tests/tail-2/truncate.sh"
if settings.use_polling && settings.follow.is_some() {
for path in &files
.map
.keys()
.filter(|p| p.is_tailable())
.map(|p| p.to_path_buf())
.collect::<Vec<PathBuf>>()
{
if let Ok(new_md) = path.metadata() {
if let Some(old_md) = &files.map.get(path).unwrap().metadata {
// TODO: [2021-10; jhscheer] reduce duplicate code
let display_name = files.map.get(path).unwrap().display_name.to_path_buf();
if new_md.len() <= old_md.len()
&& new_md.modified()? != old_md.modified()?
&& new_md.is_file()
&& old_md.is_file()
let pd = files.map.get(path).unwrap();
if let Some(old_md) = &pd.metadata {
if old_md.is_tailable()
&& new_md.is_tailable()
&& old_md.got_truncated(&new_md)?
{
show_error!("{}: file truncated", display_name.display());
files.update_metadata(path, None);
show_error!("{}: file truncated", pd.display_name.display());
files.update_metadata(path, Some(new_md));
files.update_reader(path)?;
}
}
@ -785,12 +791,14 @@ fn follow(files: &mut FileHandling, settings: &mut Settings) -> UResult<()> {
_event_counter += 1;
_timeout_counter = 0;
}
match rx_result {
Ok(Ok(event)) => {
// eprintln!("=={:=>3}=====================dbg===", _event_counter);
// dbg!(&event);
// dbg!(files.map.keys());
// dbg!(&files.last);
// dbg!(&orphans);
// eprintln!("=={:=>3}=====================dbg===", _event_counter);
handle_event(&event, files, settings, &mut watcher, &mut orphans)?;
}
@ -859,101 +867,79 @@ fn handle_event(
.unwrap()
.display_name
.to_path_buf();
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::Data(DataChange::Any)) => {
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::File)
| EventKind::Create(CreateKind::Folder)
| EventKind::Create(CreateKind::Any)
| EventKind::Modify(ModifyKind::Data(DataChange::Any))
| EventKind::Modify(ModifyKind::Name(RenameMode::To)) => {
if event_path.is_file() {
if settings.follow.is_some() {
let msg = if (files.map.get(event_path).unwrap().metadata.is_none())
|| (!settings.use_polling && settings.retry) {
format!("{} has appeared", display_name.quote())
} else {
format!("{} has been replaced", display_name.quote())
};
show_error!("{}; following new file", msg);
}
// Since Files are automatically closed when they go out of
// scope, we resume tracking from the start of the file,
// assuming it has been truncated to 0. This mimics GNU's `tail`
// behavior and is the usual truncation operation for log files.
// files.update_metadata(event_path, None);
files.update_reader(event_path)?;
if settings.follow == Some(FollowMode::Name) && settings.retry {
// TODO: [2021-10; jhscheer] add test for this
// Path has appeared, it's not an orphan any more.
orphans.retain(|path| path != event_path);
}
} else {
// If the path pointed to a file and now points to something else:
let md = &files.map.get(event_path).unwrap().metadata;
if md.is_none() || md.as_ref().unwrap().is_file() {
let msg = "has been replaced with an untailable file";
if let Ok(new_md) = event_path.metadata() {
let is_tailable = new_md.is_tailable();
let pd = files.map.get(event_path).unwrap();
if let Some(old_md) = &pd.metadata {
if is_tailable {
// We resume tracking from the start of the file,
// assuming it has been truncated to 0. This mimics GNU's `tail`
// behavior and is the usual truncation operation for log files.
if !old_md.is_tailable() {
show_error!( "{} has become accessible", display_name.quote());
files.update_reader(event_path)?;
} else if pd.reader.is_none() {
show_error!( "{} has appeared; following new file", display_name.quote());
files.update_reader(event_path)?;
} else if event.kind == EventKind::Modify(ModifyKind::Name(RenameMode::To)) {
show_error!( "{} has been replaced; following new file", display_name.quote());
files.update_reader(event_path)?;
} else if old_md.got_truncated(&new_md)? {
show_error!("{}: file truncated", display_name.display());
files.update_reader(event_path)?;
}
} else if !is_tailable && old_md.is_tailable() {
if pd.reader.is_some() {
files.reset_reader(event_path);
} else {
show_error!(
"{} 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) {
show_error!(
"{} {}; giving up on this name",
display_name.quote(),
msg
"{} has been replaced with an untailable file; giving up on this name",
display_name.quote()
);
let _ = watcher.unwatch(event_path);
files.map.remove(event_path).unwrap();
if files.map.is_empty() {
crash!(1, "{}", text::NO_FILES_REMAINING);
}
} else if settings.follow == Some(FollowMode::Name) {
// TODO: [2021-10; jhscheer] add test for this
files.update_metadata(event_path, None);
show_error!("{} {}", display_name.quote(), msg);
} else {
show_error!(
"{} has been replaced with an untailable file",
display_name.quote()
);
}
}
files.update_metadata(event_path, Some(new_md));
}
}
EventKind::Remove(RemoveKind::File) | EventKind::Remove(RemoveKind::Any)
// | EventKind::Modify(ModifyKind::Name(RenameMode::Any))
| EventKind::Modify(ModifyKind::Name(RenameMode::From)) => {
EventKind::Remove(RemoveKind::File)
| EventKind::Remove(RemoveKind::Any)
// | EventKind::Modify(ModifyKind::Name(RenameMode::Any))
| EventKind::Modify(ModifyKind::Name(RenameMode::From)) => {
if settings.follow == Some(FollowMode::Name) {
if settings.retry {
if let Some(old_md) = &files.map.get(event_path).unwrap().metadata {
if old_md.is_file() {
if let Some(old_md) = &files.map.get_mut(event_path).unwrap().metadata {
if old_md.is_tailable() {
show_error!(
"{} has become inaccessible: {}",
display_name.quote(),
@ -968,33 +954,31 @@ fn handle_event(
text::BACKEND
);
orphans.push(event_path.to_path_buf());
let _ = watcher.unwatch(event_path);
}
let _ = watcher.unwatch(event_path);
} else {
show_error!("{}: {}", display_name.display(), text::NO_SUCH_FILE);
if !files.files_remaining() && settings.use_polling {
crash!(1, "{}", text::NO_FILES_REMAINING);
}
}
// Update `files.map` to indicate that `event_path`
// is not an existing file anymore.
files.map.insert(
event_path.to_path_buf(),
PathData {
reader: None,
metadata: None,
display_name,
},
);
files.reset_reader(event_path);
} else if settings.follow == Some(FollowMode::Descriptor) && settings.retry {
// --retry only effective for the initial open
let _ = watcher.unwatch(event_path);
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)) => {
// NOTE: For `tail -f a`, keep tracking additions to b after `mv a b`
// (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:
// $ tail -f file_a ---disable-inotify
// $ mv file_a file_b
// $ echo A >> file_b
// $ echo A >> file_a
// The last append to file_a is printed, however this shouldn't be because
// after the "mv" tail should only follow "file_b".
// TODO: [2022-05; jhscheer] add test for this bug
if settings.follow == Some(FollowMode::Descriptor) {
let new_path = event.paths.last().unwrap().canonicalize()?;
// Open new file and seek to End:
let mut file = File::open(&new_path)?;
file.seek(SeekFrom::End(0))?;
// Add new reader and remove old reader:
// Add new reader but keep old display name
files.map.insert(
new_path.to_owned(),
PathData {
@ -1023,13 +1009,14 @@ fn handle_event(
display_name, // mimic GNU's tail and show old name in header
},
);
// Remove old reader
files.map.remove(event_path).unwrap();
if files.last.as_ref().unwrap() == event_path {
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 new_path = get_path(&new_path, settings);
let new_path = new_path.watchable(settings);
watcher
.watch(
&new_path.canonicalize()?,
@ -1045,34 +1032,12 @@ fn handle_event(
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
/// and the display_name (header_name) of files that are being followed.
struct PathData {
reader: Option<Box<dyn BufRead>>,
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.
@ -1086,17 +1051,20 @@ struct FileHandling {
}
impl FileHandling {
/// Insert new `PathData` into the HashMap
fn insert(&mut self, k: PathBuf, v: PathData) -> Option<PathData> {
self.last = Some(k.to_owned());
self.map.insert(k, v)
}
/// Return true if there is only stdin remaining
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?
&& (self.map.contains_key(Path::new(text::DASH)))
// || 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 {
for path in self.map.keys() {
if path.is_tailable() || path.is_stdin() {
@ -1106,27 +1074,35 @@ impl FileHandling {
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<()> {
assert!(self.map.contains_key(path));
if let Some(pd) = self.map.get_mut(path) {
let new_reader = BufReader::new(File::open(&path)?);
pd.reader = Some(Box::new(new_reader));
}
// BUG:
// If it's not necessary to reopen a file, GNU's tail calls seek to offset 0.
// 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(())
}
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));
if let Some(pd) = self.map.get_mut(path) {
if let Some(md) = md {
pd.metadata = Some(md);
} else {
pd.metadata = path.metadata().ok();
}
}
self.map.get_mut(path).unwrap().metadata = if metadata.is_some() {
metadata
} else {
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> {
assert!(self.map.contains_key(path));
let mut read_some = false;
@ -1145,6 +1121,7 @@ impl FileHandling {
Ok(read_some)
}
/// Print `buffer` to stdout
fn print_file(&self, buffer: &[u8]) -> UResult<()> {
let mut stdout = stdout();
stdout
@ -1153,6 +1130,7 @@ impl FileHandling {
Ok(())
}
/// Read new data from `path` and print it to stdout
fn tail_file(&mut self, path: &Path, verbose: bool) -> UResult<bool> {
let mut buffer = vec![];
let read_some = self.read_file(path, &mut buffer)?;
@ -1168,17 +1146,17 @@ impl FileHandling {
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 {
if verbose {
if let Some(ref last) = self.last {
if let Ok(path) = path.canonicalize() {
return !last.eq(&path);
}
return !last.eq(&path);
}
}
false
}
/// Print header for `path` to stdout
fn print_header(&self, path: &Path, needs_newline: bool) {
println!(
"{}==> {} <==",
@ -1187,6 +1165,7 @@ impl FileHandling {
);
}
/// Wrapper for `PathData::display_name`
fn display_name(&self, path: &Path) -> String {
if let Some(path) = self.map.get(path) {
path.display_name.display().to_string()
@ -1415,12 +1394,6 @@ fn unbounded_tail<T: Read>(reader: &mut BufReader<T>, settings: &Settings) -> UR
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> {
let mut size_string = src.trim();
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))
}
fn get_block_size(md: &Metadata) -> u64 {
#[cfg(unix)]
{
md.blocks()
}
#[cfg(not(unix))]
{
md.len()
}
}
pub fn stdin_is_pipe_or_fifo() -> bool {
#[cfg(unix)]
{
@ -1473,34 +1435,106 @@ pub fn stdin_is_bad_fd() -> bool {
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_orphan(&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 {
self.eq(Self::new(text::DASH))
|| self.eq(Self::new(text::DEV_STDIN))
|| self.eq(Self::new(text::STDIN_HEADER))
}
/// Return true if `path` does not have an existing parent directory
fn is_orphan(&self) -> bool {
!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 {
#[cfg(unix)]
{
// TODO: [2021-10; jhscheer] what about fifos?
self.is_file()
|| self.exists() && {
let ft = metadata(self).unwrap().file_type();
ft.is_char_device() || ft.is_fifo()
}
}
#[cfg(not(unix))]
{
self.is_file()
self.is_file() || self.exists() && self.metadata().unwrap().is_tailable()
}
/// Wrapper for `path` to use for `notify::Watcher::watch`.
/// Will return a "watchable" parent directory if necessary.
/// Will panic if parent directory cannot be watched.
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.
// 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 = 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()
}
}
}