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:
parent
a9066e2d0c
commit
c70b7a0501
1 changed files with 139 additions and 35 deletions
|
@ -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.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue