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

tail: implement --follow=name

This implements `--follow=name` for create/move/delete events.
Under the hood crate `notify` provides a cross-platform notification
library.
This commit is contained in:
Jan Scheer 2021-09-27 23:08:37 +02:00
parent a9066e2d0c
commit c70b7a0501
No known key found for this signature in database
GPG key ID: C62AD4C29E2B9828

View file

@ -70,11 +70,17 @@ enum FilterMode {
Lines(usize, u8), // (number of lines, delimiter) Lines(usize, u8), // (number of lines, delimiter)
} }
#[derive(Debug, PartialEq)]
enum FollowMode {
Descriptor,
Name,
}
struct Settings { struct Settings {
mode: FilterMode, mode: FilterMode,
sleep_sec: Duration, sleep_sec: Duration,
beginning: bool, beginning: bool,
follow: bool, follow: Option<FollowMode>,
force_polling: bool, force_polling: bool,
pid: platform::Pid, pid: platform::Pid,
} }
@ -85,7 +91,7 @@ impl Default for Settings {
mode: FilterMode::Lines(10, b'\n'), mode: FilterMode::Lines(10, b'\n'),
sleep_sec: Duration::from_secs_f32(1.0), sleep_sec: Duration::from_secs_f32(1.0),
beginning: false, beginning: false,
follow: false, follow: None,
force_polling: false, force_polling: false,
pid: 0, pid: 0,
} }
@ -100,7 +106,13 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
let matches = app.get_matches_from(args); let matches = app.get_matches_from(args);
settings.follow = matches.is_present(options::FOLLOW); settings.follow = if matches.occurrences_of(options::FOLLOW) == 0 {
None
} else if matches.value_of(options::FOLLOW) == Some("name") {
Some(FollowMode::Name)
} else {
Some(FollowMode::Descriptor)
};
if let Some(s) = matches.value_of(options::SLEEP_INT) { if let Some(s) = matches.value_of(options::SLEEP_INT) {
settings.sleep_sec = match s.parse::<f32>() { settings.sleep_sec = match s.parse::<f32>() {
@ -113,7 +125,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
if let Ok(pid) = pid_str.parse() { if let Ok(pid) = pid_str.parse() {
settings.pid = pid; settings.pid = pid;
if pid != 0 { if pid != 0 {
if !settings.follow { if settings.follow.is_none() {
show_warning!("PID ignored; --pid=PID is useful only when following"); show_warning!("PID ignored; --pid=PID is useful only when following");
} }
@ -192,7 +204,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
not the -f option shall be ignored. not the -f option shall be ignored.
*/ */
if settings.follow && !stdin_is_pipe_or_fifo() { if settings.follow.is_some() && !stdin_is_pipe_or_fifo() {
readers.push((Box::new(reader), &stdin_string)); readers.push((Box::new(reader), &stdin_string));
} }
} }
@ -218,22 +230,22 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
let md = file.metadata().unwrap(); let md = file.metadata().unwrap();
if is_seekable(&mut file) && get_block_size(&md) > 0 { if is_seekable(&mut file) && get_block_size(&md) > 0 {
bounded_tail(&mut file, &settings); bounded_tail(&mut file, &settings);
if settings.follow { if settings.follow.is_some() {
let reader = BufReader::new(file); let reader = BufReader::new(file);
readers.push((Box::new(reader), filename)); readers.push((Box::new(reader), filename));
} }
} else { } else {
let mut reader = BufReader::new(file); let mut reader = BufReader::new(file);
unbounded_tail(&mut reader, &settings); unbounded_tail(&mut reader, &settings);
if settings.follow { if settings.follow.is_some() {
readers.push((Box::new(reader), filename)); readers.push((Box::new(reader), filename));
} }
} }
} }
} }
if settings.follow { if settings.follow.is_some() {
follow(&mut readers[..], &settings); follow(&mut readers, &settings);
} }
return_code return_code
@ -257,6 +269,12 @@ pub fn uu_app() -> App<'static, 'static> {
Arg::with_name(options::FOLLOW) Arg::with_name(options::FOLLOW)
.short("f") .short("f")
.long(options::FOLLOW) .long(options::FOLLOW)
.default_value("descriptor")
.takes_value(true)
.min_values(0)
.max_values(1)
.require_equals(true)
.possible_values(&["descriptor", "name"])
.help("Print the file as it grows"), .help("Print the file as it grows"),
) )
.arg( .arg(
@ -315,13 +333,13 @@ pub fn uu_app() -> App<'static, 'static> {
) )
} }
fn follow<T: BufRead>(readers: &mut [(T, &PathBuf)], settings: &Settings) { fn follow(readers: &mut Vec<(Box<dyn BufRead>, &PathBuf)>, settings: &Settings) {
assert!(settings.follow); assert!(settings.follow.is_some());
if readers.is_empty() { if readers.is_empty() {
return; return;
} }
let mut last = readers.len() - 1; let last = readers.len() - 1;
let mut read_some = false; let mut read_some = false;
let mut process = platform::ProcessChecker::new(settings.pid); let mut process = platform::ProcessChecker::new(settings.pid);
@ -330,7 +348,7 @@ fn follow<T: BufRead>(readers: &mut [(T, &PathBuf)], settings: &Settings) {
let (tx, rx) = channel(); let (tx, rx) = channel();
let mut watcher: Box<dyn Watcher>; let mut watcher: Box<dyn Watcher>;
if dbg!(settings.force_polling) { if settings.force_polling {
watcher = Box::new( watcher = Box::new(
notify::PollWatcher::with_delay(Arc::new(Mutex::new(tx)), settings.sleep_sec).unwrap(), notify::PollWatcher::with_delay(Arc::new(Mutex::new(tx)), settings.sleep_sec).unwrap(),
); );
@ -339,38 +357,99 @@ fn follow<T: BufRead>(readers: &mut [(T, &PathBuf)], settings: &Settings) {
}; };
for (_, path) in readers.iter() { for (_, path) in readers.iter() {
// NOTE: Using the parent directory here instead of the file is a workaround.
// On Linux (other OSs not tested yet) the watcher can crash for rename/delete/move
// operations if a file is watched directly.
// This is the recommendation of the notify crate authors:
// > On some platforms, if the `path` is renamed or removed while being watched, behaviour may
// > be unexpected. See discussions in [#165] and [#166]. If less surprising behaviour is wanted
// > one may non-recursively watch the _parent_ directory as well and manage related events.
let parent = path.parent().unwrap(); // This should never be `None` if `path.is_file()`
let path = if parent.is_dir() {
parent
} else {
Path::new(".")
};
watcher.watch(path, RecursiveMode::NonRecursive).unwrap(); watcher.watch(path, RecursiveMode::NonRecursive).unwrap();
} }
loop { loop {
// std::thread::sleep(settings.sleep_sec); match rx.recv() {
let _result = rx.recv(); Ok(Ok(event)) => {
// TODO: // println!("\n{:?}", event);
// match rx.recv() { if settings.follow == Some(FollowMode::Name) {
// Ok(event) => println!("\n{:?}", event), use notify::event::*;
// Err(e) => println!("watch error: {:?}", e), for (i, (reader, path)) in readers.iter_mut().enumerate() {
// } if let Some(event_path) = event.paths.first() {
if path.ends_with(
event_path
.file_name()
.unwrap_or_else(|| std::ffi::OsStr::new("")),
) {
match event.kind {
// notify::EventKind::Any => {}
// EventKind::Access(AccessKind::Close(AccessMode::Write)) => {}
EventKind::Create(CreateKind::File)
| EventKind::Modify(ModifyKind::Name(RenameMode::To)) => {
// This triggers for e.g.:
// Create: cp log.bak log.dat
// Rename: mv log.bak log.dat
let pid_is_dead = !read_some && settings.pid != 0 && process.is_dead(); let msg = if settings.force_polling {
read_some = false; format!(
"{} has been replaced; following new file",
path.quote()
)
} else {
format!(
"{} has appeared; following new file",
path.quote()
)
};
show_error!("{}", 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, which is the usual
// truncation operation for log files.
for (i, (reader, filename)) in readers.iter_mut().enumerate() { // Open file again and then print it from the beginning.
// Print all new content since the last pass let new_reader =
loop { Box::new(BufReader::new(File::open(&path).unwrap()));
let mut datum = String::new(); let _ = std::mem::replace(reader, new_reader);
match reader.read_line(&mut datum) { read_some =
Ok(0) => break, print_file((i, &mut (reader, path)), last, read_some);
Ok(_) => { }
read_some = true; // EventKind::Modify(ModifyKind::Metadata(_)) => {}
if i != last { // EventKind::Modify(ModifyKind::Name(RenameMode::From)) => {}
println!("\n==> {} <==", filename.display()); // EventKind::Modify(ModifyKind::Name(RenameMode::To)) => {}
last = i; EventKind::Remove(RemoveKind::File)
| EventKind::Modify(ModifyKind::Name(RenameMode::From)) => {
// This triggers for e.g.:
// Create: cp log.dat log.bak
// Rename: mv log.dat log.bak
if !settings.force_polling {
show_error!(
"{}: No such file or directory",
path.display()
);
}
}
// notify::EventKind::Other => {}
_ => {} // println!("{:?}", event.kind),
}
}
} }
print!("{}", datum);
} }
Err(err) => panic!("{}", err),
} }
} }
Err(e) => println!("{:?}", e),
_ => print!("UnknownError"),
}
let pid_is_dead = !read_some && settings.pid != 0 && process.is_dead();
for reader_i in readers.iter_mut().enumerate() {
read_some = print_file(reader_i, last, read_some);
} }
if pid_is_dead { if pid_is_dead {
@ -379,6 +458,31 @@ fn follow<T: BufRead>(readers: &mut [(T, &PathBuf)], settings: &Settings) {
} }
} }
// Print all new content since the last pass
fn print_file<T: BufRead>(
reader_i: (usize, &mut (T, &PathBuf)),
mut last: usize,
mut read_some: bool,
) -> bool {
let (i, (reader, filename)) = reader_i;
loop {
let mut datum = String::new();
match reader.read_line(&mut datum) {
Ok(0) => break,
Ok(_) => {
read_some = true;
if i != last {
println!("\n==> {} <==", filename.display());
last = i;
}
print!("{}", datum);
}
Err(err) => panic!("{}", err),
}
}
read_some
}
/// Iterate over bytes in the file, in reverse, until we find the /// Iterate over bytes in the file, in reverse, until we find the
/// `num_delimiters` instance of `delimiter`. The `file` is left seek'd to the /// `num_delimiters` instance of `delimiter`. The `file` is left seek'd to the
/// position just after that delimiter. /// position just after that delimiter.