mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-28 11:37:44 +00:00
tail: implement handling of truncate event for --follow=name
This commit is contained in:
parent
d9cd28fab6
commit
e935d40480
2 changed files with 182 additions and 87 deletions
|
@ -171,7 +171,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
|
|
||||||
let mut files_count = paths.len();
|
let mut files_count = paths.len();
|
||||||
let mut first_header = true;
|
let mut first_header = true;
|
||||||
let mut readers: Vec<(Box<dyn BufRead>, &PathBuf)> = Vec::new();
|
let mut readers: Vec<(Box<dyn BufRead>, &PathBuf, Option<Metadata>)> = Vec::new();
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
let stdin_string = PathBuf::from("standard input");
|
let stdin_string = PathBuf::from("standard input");
|
||||||
|
@ -205,7 +205,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if settings.follow.is_some() && !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, None));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -227,18 +227,18 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
}
|
}
|
||||||
first_header = false;
|
first_header = false;
|
||||||
let mut file = File::open(&path).unwrap();
|
let mut file = File::open(&path).unwrap();
|
||||||
let md = file.metadata().unwrap();
|
let md = file.metadata().ok();
|
||||||
if is_seekable(&mut file) && get_block_size(&md) > 0 {
|
if is_seekable(&mut file) && get_block_size(md.as_ref().unwrap()) > 0 {
|
||||||
bounded_tail(&mut file, &settings);
|
bounded_tail(&mut file, &settings);
|
||||||
if settings.follow.is_some() {
|
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, md));
|
||||||
}
|
}
|
||||||
} 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.is_some() {
|
if settings.follow.is_some() {
|
||||||
readers.push((Box::new(reader), filename));
|
readers.push((Box::new(reader), filename, md));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -333,14 +333,12 @@ pub fn uu_app() -> App<'static, 'static> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn follow(readers: &mut Vec<(Box<dyn BufRead>, &PathBuf)>, settings: &Settings) {
|
fn follow(readers: &mut Vec<(Box<dyn BufRead>, &PathBuf, Option<Metadata>)>, settings: &Settings) {
|
||||||
assert!(settings.follow.is_some());
|
assert!(settings.follow.is_some());
|
||||||
if readers.is_empty() {
|
if readers.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let last = readers.len() - 1;
|
|
||||||
let mut read_some = false;
|
|
||||||
let mut process = platform::ProcessChecker::new(settings.pid);
|
let mut process = platform::ProcessChecker::new(settings.pid);
|
||||||
|
|
||||||
use notify::{RecursiveMode, Watcher};
|
use notify::{RecursiveMode, Watcher};
|
||||||
|
@ -349,14 +347,23 @@ fn follow(readers: &mut Vec<(Box<dyn BufRead>, &PathBuf)>, settings: &Settings)
|
||||||
|
|
||||||
let mut watcher: Box<dyn Watcher>;
|
let mut watcher: Box<dyn Watcher>;
|
||||||
if settings.force_polling {
|
if settings.force_polling {
|
||||||
|
// Polling based Watcher implementation
|
||||||
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(),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
// Watcher is implemented per platform using the best implementation available on that
|
||||||
|
// platform. In addition to such event driven implementations, a polling implementation
|
||||||
|
// is also provided that should work on any platform.
|
||||||
|
// Linux / Android: inotify
|
||||||
|
// macOS: FSEvents
|
||||||
|
// Windows: ReadDirectoryChangesW
|
||||||
|
// FreeBSD / NetBSD / OpenBSD / DragonflyBSD: kqueue
|
||||||
|
// Fallback: polling (default delay is 30 seconds!)
|
||||||
watcher = Box::new(notify::RecommendedWatcher::new(tx).unwrap());
|
watcher = Box::new(notify::RecommendedWatcher::new(tx).unwrap());
|
||||||
};
|
};
|
||||||
|
|
||||||
for (_, path) in readers.iter() {
|
for (_, path, _) in readers.iter() {
|
||||||
// NOTE: Using the parent directory here instead of the file is a workaround.
|
// 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
|
// On Linux (other OSs not tested yet) the watcher can crash for rename/delete/move
|
||||||
// operations if a file is watched directly.
|
// operations if a file is watched directly.
|
||||||
|
@ -373,100 +380,122 @@ fn follow(readers: &mut Vec<(Box<dyn BufRead>, &PathBuf)>, settings: &Settings)
|
||||||
watcher.watch(path, RecursiveMode::NonRecursive).unwrap();
|
watcher.watch(path, RecursiveMode::NonRecursive).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut read_some;
|
||||||
|
let last = readers.len() - 1;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
read_some = false;
|
||||||
match rx.recv() {
|
match rx.recv() {
|
||||||
Ok(Ok(event)) => {
|
Ok(Ok(event)) => {
|
||||||
// println!("\n{:?}", event);
|
// println!("\n{:?}\n", event);
|
||||||
if settings.follow == Some(FollowMode::Name) {
|
if settings.follow == Some(FollowMode::Name) {
|
||||||
use notify::event::*;
|
handle_event(event, readers, settings, last);
|
||||||
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::Create(CreateKind::Any)
|
|
||||||
| EventKind::Modify(ModifyKind::Name(RenameMode::To)) => {
|
|
||||||
// This triggers for e.g.:
|
|
||||||
// Create: cp log.bak log.dat
|
|
||||||
// Rename: mv log.bak log.dat
|
|
||||||
|
|
||||||
let msg = if settings.force_polling {
|
|
||||||
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.
|
|
||||||
|
|
||||||
// Open file again and then print it from the beginning.
|
|
||||||
let new_reader =
|
|
||||||
Box::new(BufReader::new(File::open(&path).unwrap()));
|
|
||||||
let _ = std::mem::replace(reader, new_reader);
|
|
||||||
read_some =
|
|
||||||
print_file((i, &mut (reader, path)), last, read_some);
|
|
||||||
}
|
|
||||||
// EventKind::Modify(ModifyKind::Metadata(_)) => {}
|
|
||||||
// EventKind::Modify(ModifyKind::Name(RenameMode::From)) => {}
|
|
||||||
// EventKind::Modify(ModifyKind::Name(RenameMode::To)) => {}
|
|
||||||
EventKind::Remove(RemoveKind::File)
|
|
||||||
| EventKind::Remove(RemoveKind::Any)
|
|
||||||
| 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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => println!("{:?}", e),
|
Err(e) => eprintln!("{:?}", e),
|
||||||
_ => print!("UnknownError"),
|
_ => eprintln!("UnknownError"),
|
||||||
}
|
}
|
||||||
|
|
||||||
let pid_is_dead = !read_some && settings.pid != 0 && process.is_dead();
|
|
||||||
|
|
||||||
for reader_i in readers.iter_mut().enumerate() {
|
for reader_i in readers.iter_mut().enumerate() {
|
||||||
read_some = print_file(reader_i, last, read_some);
|
read_some = print_file(reader_i, last);
|
||||||
}
|
}
|
||||||
|
|
||||||
if pid_is_dead {
|
if !read_some && settings.pid != 0 && process.is_dead() {
|
||||||
|
// pid is dead
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print all new content since the last pass
|
fn handle_event(
|
||||||
fn print_file<T: BufRead>(
|
event: notify::Event,
|
||||||
reader_i: (usize, &mut (T, &PathBuf)),
|
readers: &mut Vec<(Box<dyn BufRead>, &PathBuf, Option<Metadata>)>,
|
||||||
mut last: usize,
|
settings: &Settings,
|
||||||
mut read_some: bool,
|
last: usize,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let (i, (reader, filename)) = reader_i;
|
let mut read_some = false;
|
||||||
|
use notify::event::*;
|
||||||
|
for (i, (reader, path, metadata)) 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::Modify(ModifyKind::Data(DataChange::Any)) => {
|
||||||
|
// This triggers for e.g.:
|
||||||
|
// head log.dat > log.dat
|
||||||
|
if let Ok(new_md) = path.metadata() {
|
||||||
|
if let Some(old_md) = metadata {
|
||||||
|
if new_md.len() < old_md.len() {
|
||||||
|
show_error!("{}: file truncated", path.display());
|
||||||
|
// Update Metadata, open file again and print from beginning.
|
||||||
|
let _ = std::mem::replace(metadata, Some(new_md));
|
||||||
|
let new_reader = BufReader::new(File::open(&path).unwrap());
|
||||||
|
// let _ = new_reader.seek(SeekFrom::End(0));
|
||||||
|
let _ = std::mem::replace(reader, Box::new(new_reader));
|
||||||
|
read_some = print_file((i, &mut (reader, path, None)), last);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EventKind::Create(CreateKind::File)
|
||||||
|
| EventKind::Create(CreateKind::Any)
|
||||||
|
| EventKind::Modify(ModifyKind::Name(RenameMode::To)) => {
|
||||||
|
// This triggers for e.g.:
|
||||||
|
// Create: cp log.bak log.dat
|
||||||
|
// Rename: mv log.bak log.dat
|
||||||
|
|
||||||
|
let msg = if settings.force_polling {
|
||||||
|
format!("{} has been replaced", path.quote())
|
||||||
|
} else {
|
||||||
|
format!("{} has appeared", path.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, which is the usual
|
||||||
|
// truncation operation for log files.
|
||||||
|
|
||||||
|
// Open file again and then print it from the beginning.
|
||||||
|
let new_reader = BufReader::new(File::open(&path).unwrap());
|
||||||
|
let _ = std::mem::replace(reader, Box::new(new_reader));
|
||||||
|
read_some = print_file((i, &mut (reader, path, None)), last);
|
||||||
|
}
|
||||||
|
// EventKind::Modify(ModifyKind::Metadata(_)) => {}
|
||||||
|
// EventKind::Modify(ModifyKind::Name(RenameMode::From)) => {}
|
||||||
|
// EventKind::Modify(ModifyKind::Name(RenameMode::To)) => {}
|
||||||
|
EventKind::Remove(RemoveKind::File)
|
||||||
|
| EventKind::Remove(RemoveKind::Any)
|
||||||
|
| 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
read_some
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print all new content since the last pass.
|
||||||
|
// This prints from the current seek position forward.
|
||||||
|
// `last` determines if a header needs to be printed.
|
||||||
|
fn print_file<T: BufRead>(
|
||||||
|
reader_i: (usize, &mut (T, &PathBuf, Option<Metadata>)),
|
||||||
|
mut last: usize,
|
||||||
|
) -> bool {
|
||||||
|
let mut read_some = false;
|
||||||
|
let (i, (reader, filename, _)) = reader_i;
|
||||||
loop {
|
loop {
|
||||||
let mut datum = String::new();
|
let mut datum = String::new();
|
||||||
match reader.read_line(&mut datum) {
|
match reader.read_line(&mut datum) {
|
||||||
|
|
|
@ -105,6 +105,30 @@ fn test_follow_multiple() {
|
||||||
child.kill().unwrap();
|
child.kill().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_follow_name_multiple() {
|
||||||
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
let mut child = ucmd
|
||||||
|
.arg("--follow=name")
|
||||||
|
.arg(FOOBAR_TXT)
|
||||||
|
.arg(FOOBAR_2_TXT)
|
||||||
|
.run_no_wait();
|
||||||
|
|
||||||
|
let expected = at.read("foobar_follow_multiple.expected");
|
||||||
|
assert_eq!(read_size(&mut child, expected.len()), expected);
|
||||||
|
|
||||||
|
let first_append = "trois\n";
|
||||||
|
at.append(FOOBAR_2_TXT, first_append);
|
||||||
|
assert_eq!(read_size(&mut child, first_append.len()), first_append);
|
||||||
|
|
||||||
|
let second_append = "twenty\nthirty\n";
|
||||||
|
let expected = at.read("foobar_follow_multiple_appended.expected");
|
||||||
|
at.append(FOOBAR_TXT, second_append);
|
||||||
|
assert_eq!(read_size(&mut child, expected.len()), expected);
|
||||||
|
|
||||||
|
child.kill().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_follow_stdin() {
|
fn test_follow_stdin() {
|
||||||
new_ucmd!()
|
new_ucmd!()
|
||||||
|
@ -521,6 +545,48 @@ fn test_follow_name_create() {
|
||||||
assert_eq!(buf_stderr, expected_stderr);
|
assert_eq!(buf_stderr, expected_stderr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_follow_name_truncate() {
|
||||||
|
// This test triggers a truncate event while `tail --follow=name logfile` is running.
|
||||||
|
// cp logfile backup && head logfile > logfile && sleep 1 && cp backup logfile
|
||||||
|
|
||||||
|
let ts = TestScenario::new(util_name!());
|
||||||
|
let at = &ts.fixtures;
|
||||||
|
|
||||||
|
let source = FOLLOW_NAME_TXT;
|
||||||
|
let source_canonical = &at.plus(source);
|
||||||
|
let backup = at.plus_as_string("backup");
|
||||||
|
|
||||||
|
let expected_stdout = at.read(FOLLOW_NAME_EXP);
|
||||||
|
let expected_stderr = format!("{}: {}: file truncated\n", ts.util_name, source);
|
||||||
|
|
||||||
|
let args = ["--follow=name", source];
|
||||||
|
let mut p = ts.ucmd().args(&args).run_no_wait();
|
||||||
|
|
||||||
|
let delay = 10;
|
||||||
|
|
||||||
|
std::fs::copy(&source_canonical, &backup).unwrap();
|
||||||
|
sleep(Duration::from_millis(delay));
|
||||||
|
|
||||||
|
let _ = std::fs::File::create(source_canonical).unwrap(); // trigger truncate
|
||||||
|
sleep(Duration::from_millis(delay));
|
||||||
|
|
||||||
|
std::fs::copy(&backup, &source_canonical).unwrap();
|
||||||
|
sleep(Duration::from_millis(delay));
|
||||||
|
|
||||||
|
p.kill().unwrap();
|
||||||
|
|
||||||
|
let mut buf_stdout = String::new();
|
||||||
|
let mut p_stdout = p.stdout.take().unwrap();
|
||||||
|
p_stdout.read_to_string(&mut buf_stdout).unwrap();
|
||||||
|
assert_eq!(buf_stdout, expected_stdout);
|
||||||
|
|
||||||
|
let mut buf_stderr = String::new();
|
||||||
|
let mut p_stderr = p.stderr.take().unwrap();
|
||||||
|
p_stderr.read_to_string(&mut buf_stderr).unwrap();
|
||||||
|
assert_eq!(buf_stderr, expected_stderr);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_follow_name_create_polling() {
|
fn test_follow_name_create_polling() {
|
||||||
// This test triggers a remove/create event while `tail --follow=name --disable-inotify logfile` is running.
|
// This test triggers a remove/create event while `tail --follow=name --disable-inotify logfile` is running.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue