diff --git a/src/uu/tail/src/args.rs b/src/uu/tail/src/args.rs index f2ef918f3..27e69b994 100644 --- a/src/uu/tail/src/args.rs +++ b/src/uu/tail/src/args.rs @@ -113,6 +113,18 @@ pub enum FollowMode { Name, } +#[derive(Debug)] +pub enum VerificationResult { + Ok, + CannotFollowStdinByName, + NoOutput, +} + +pub enum CheckResult { + Ok, + NoPidSupport, +} + #[derive(Debug, Default)] pub struct Settings { pub follow: Option, @@ -149,10 +161,6 @@ impl Settings { settings.retry = matches.get_flag(options::RETRY) || matches.get_flag(options::FOLLOW_RETRY); - if settings.retry && settings.follow.is_none() { - show_warning!("--retry ignored; --retry is useful only when following"); - } - if let Some(s) = matches.get_one::(options::SLEEP_INT) { settings.sleep_sec = match s.parse::() { Ok(s) => Duration::from_secs_f32(s), @@ -195,13 +203,6 @@ impl Settings { )); } settings.pid = pid; - if settings.follow.is_none() { - show_warning!("PID ignored; --pid=PID is useful only when following"); - } - if !platform::supports_pid_checks(settings.pid) { - show_warning!("--pid=PID is not supported on this system"); - settings.pid = 0; - } } Err(e) => { return Err(USimpleError::new( @@ -214,16 +215,6 @@ impl Settings { settings.mode = FilterMode::from(matches)?; - // Mimic GNU's tail for -[nc]0 without -f and exit immediately - if settings.follow.is_none() - && matches!( - settings.mode, - FilterMode::Lines(Signum::MinusZero, _) | FilterMode::Bytes(Signum::MinusZero) - ) - { - std::process::exit(0) - } - let mut inputs: VecDeque = matches .get_many::(options::ARG_FILES) .map(|v| v.map(|string| Input::from(string.clone())).collect()) @@ -243,6 +234,54 @@ impl Settings { Ok(settings) } + + pub fn has_only_stdin(&self) -> bool { + self.inputs.iter().all(|input| input.is_stdin()) + } + + pub fn has_stdin(&self) -> bool { + self.inputs.iter().any(|input| input.is_stdin()) + } + + pub fn check_warnings(&self) -> CheckResult { + if self.retry { + if self.follow.is_none() { + show_warning!("--retry ignored; --retry is useful only when following"); + } else if self.follow == Some(FollowMode::Descriptor) { + show_warning!("--retry only effective for the initial open"); + } + } + + if self.pid != 0 { + if self.follow.is_none() { + show_warning!("PID ignored; --pid=PID is useful only when following"); + } else if !platform::supports_pid_checks(self.pid) { + show_warning!("--pid=PID is not supported on this system"); + return CheckResult::NoPidSupport; + } + } + + CheckResult::Ok + } + + pub fn verify(&self) -> VerificationResult { + // Mimic GNU's tail for `tail -F` + if self.inputs.iter().any(|i| i.is_stdin()) && self.follow == Some(FollowMode::Name) { + return VerificationResult::CannotFollowStdinByName; + } + + // Mimic GNU's tail for -[nc]0 without -f and exit immediately + if self.follow.is_none() + && matches!( + self.mode, + FilterMode::Lines(Signum::MinusZero, _) | FilterMode::Bytes(Signum::MinusZero) + ) + { + return VerificationResult::NoOutput; + } + + VerificationResult::Ok + } } pub fn arg_iterate<'a>( @@ -298,19 +337,6 @@ fn parse_num(src: &str) -> Result { }) } -pub fn stdin_is_pipe_or_fifo() -> bool { - #[cfg(unix)] - { - platform::stdin_is_pipe_or_fifo() - } - #[cfg(windows)] - { - winapi_util::file::typ(winapi_util::HandleRef::stdin()) - .map(|t| t.is_disk() || t.is_pipe()) - .unwrap_or(false) - } -} - pub fn parse_args(args: impl uucore::Args) -> UResult { let matches = uu_app().try_get_matches_from(arg_iterate(args)?)?; Settings::from(&matches) diff --git a/src/uu/tail/src/chunks.rs b/src/uu/tail/src/chunks.rs index acfc69a30..7ad2e153b 100644 --- a/src/uu/tail/src/chunks.rs +++ b/src/uu/tail/src/chunks.rs @@ -7,7 +7,9 @@ //! or at the end of piped stdin with [`LinesChunk`] or [`BytesChunk`]. //! //! Use [`ReverseChunks::new`] to create a new iterator over chunks of bytes from the file. + // spell-checker:ignore (ToDO) filehandle BUFSIZ + use std::collections::VecDeque; use std::fs::File; use std::io::{BufRead, Read, Seek, SeekFrom, Write}; diff --git a/src/uu/tail/src/follow/files.rs b/src/uu/tail/src/follow/files.rs index 556defd1f..8686e73f4 100644 --- a/src/uu/tail/src/follow/files.rs +++ b/src/uu/tail/src/follow/files.rs @@ -13,7 +13,6 @@ use std::collections::hash_map::Keys; use std::collections::HashMap; use std::fs::{File, Metadata}; use std::io::{stdout, BufRead, BufReader, BufWriter}; - use std::path::{Path, PathBuf}; use uucore::error::UResult; diff --git a/src/uu/tail/src/follow/mod.rs b/src/uu/tail/src/follow/mod.rs index 4bb2798d1..e31eb54d1 100644 --- a/src/uu/tail/src/follow/mod.rs +++ b/src/uu/tail/src/follow/mod.rs @@ -6,4 +6,4 @@ mod files; mod watch; -pub use watch::{follow, WatcherService}; +pub use watch::{follow, Observer}; diff --git a/src/uu/tail/src/follow/watch.rs b/src/uu/tail/src/follow/watch.rs index dd8728c45..c21da2fae 100644 --- a/src/uu/tail/src/follow/watch.rs +++ b/src/uu/tail/src/follow/watch.rs @@ -13,8 +13,7 @@ use notify::{RecommendedWatcher, RecursiveMode, Watcher, WatcherKind}; use std::collections::VecDeque; use std::io::BufRead; use std::path::{Path, PathBuf}; -use std::sync::mpsc; -use std::sync::mpsc::{channel, Receiver}; +use std::sync::mpsc::{self, channel, Receiver}; use uucore::display::Quotable; use uucore::error::{set_exit_code, UResult, USimpleError}; use uucore::show_error; @@ -81,7 +80,7 @@ impl WatcherRx { } } -pub struct WatcherService { +pub struct Observer { /// Whether --retry was given on the command line pub retry: bool, @@ -92,17 +91,21 @@ pub struct WatcherService { /// platform specific event driven method. Since `use_polling` is subject to /// change during runtime it is moved out of [`Settings`]. pub use_polling: bool, + pub watcher_rx: Option, pub orphans: Vec, pub files: FileHandling, + + pub pid: platform::Pid, } -impl WatcherService { +impl Observer { pub fn new( retry: bool, follow: Option, use_polling: bool, files: FileHandling, + pid: platform::Pid, ) -> Self { Self { retry, @@ -111,6 +114,7 @@ impl WatcherService { watcher_rx: None, orphans: Vec::new(), files, + pid, } } @@ -120,6 +124,7 @@ impl WatcherService { settings.follow, settings.use_polling, FileHandling::from(settings), + settings.pid, ) } @@ -460,14 +465,12 @@ impl WatcherService { } } -pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResult<()> { - if watcher_service.files.no_files_remaining(settings) - && !watcher_service.files.only_stdin_remaining() - { +pub fn follow(mut observer: Observer, settings: &Settings) -> UResult<()> { + if observer.files.no_files_remaining(settings) && !observer.files.only_stdin_remaining() { return Err(USimpleError::new(1, text::NO_FILES_REMAINING.to_string())); } - let mut process = platform::ProcessChecker::new(settings.pid); + let mut process = platform::ProcessChecker::new(observer.pid); let mut _event_counter = 0; let mut _timeout_counter = 0; @@ -478,7 +481,7 @@ pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResu // If `--pid=p`, tail checks whether process p // is alive at least every `--sleep-interval=N` seconds - if settings.follow.is_some() && settings.pid != 0 && process.is_dead() { + if settings.follow.is_some() && observer.pid != 0 && process.is_dead() { // p is dead, tail will also terminate break; } @@ -487,22 +490,20 @@ pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResu // If a path becomes an orphan during runtime, it will be added to orphans. // To be able to differentiate between the cases of test_retry8 and test_retry9, // here paths will not be removed from orphans if the path becomes available. - if watcher_service.follow_name_retry() { - for new_path in &watcher_service.orphans { + if observer.follow_name_retry() { + for new_path in &observer.orphans { if new_path.exists() { - let pd = watcher_service.files.get(new_path); + let pd = observer.files.get(new_path); 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() ); - watcher_service.files.update_metadata(new_path, Some(md)); - watcher_service.files.update_reader(new_path)?; - _read_some = watcher_service - .files - .tail_file(new_path, settings.verbose)?; - watcher_service + observer.files.update_metadata(new_path, Some(md)); + observer.files.update_reader(new_path)?; + _read_some = observer.files.tail_file(new_path, settings.verbose)?; + observer .watcher_rx .as_mut() .unwrap() @@ -514,7 +515,7 @@ pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResu // With -f, sleep for approximately N seconds (default 1.0) between iterations; // We wake up if Notify sends an Event or if we wait more than `sleep_sec`. - let rx_result = watcher_service + let rx_result = observer .watcher_rx .as_mut() .unwrap() @@ -529,9 +530,9 @@ pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResu match rx_result { Ok(Ok(event)) => { if let Some(event_path) = event.paths.first() { - if watcher_service.files.contains_key(event_path) { + if observer.files.contains_key(event_path) { // Handle Event if it is about a path that we are monitoring - paths = watcher_service.handle_event(&event, settings)?; + paths = observer.handle_event(&event, settings)?; } } } @@ -540,8 +541,8 @@ pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResu paths, })) if e.kind() == std::io::ErrorKind::NotFound => { if let Some(event_path) = paths.first() { - if watcher_service.files.contains_key(event_path) { - let _ = watcher_service + if observer.files.contains_key(event_path) { + let _ = observer .watcher_rx .as_mut() .unwrap() @@ -566,16 +567,16 @@ pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResu Err(e) => return Err(USimpleError::new(1, format!("RecvTimeoutError: {}", e))), } - if watcher_service.use_polling && settings.follow.is_some() { + if observer.use_polling && settings.follow.is_some() { // Consider all files to potentially have new content. // This is a workaround because `Notify::PollWatcher` // does not recognize the "renaming" of files. - paths = watcher_service.files.keys().cloned().collect::>(); + paths = observer.files.keys().cloned().collect::>(); } // main print loop for path in &paths { - _read_some = watcher_service.files.tail_file(path, settings.verbose)?; + _read_some = observer.files.tail_file(path, settings.verbose)?; } if _timeout_counter == settings.max_unchanged_stats { diff --git a/src/uu/tail/src/paths.rs b/src/uu/tail/src/paths.rs index 203a23817..03656d036 100644 --- a/src/uu/tail/src/paths.rs +++ b/src/uu/tail/src/paths.rs @@ -5,24 +5,14 @@ // spell-checker:ignore tailable seekable stdlib (stdlib) -#[cfg(unix)] -use std::os::unix::fs::{FileTypeExt, MetadataExt}; - -use std::collections::VecDeque; +use crate::text; use std::fs::{File, Metadata}; use std::io::{Seek, SeekFrom}; +#[cfg(unix)] +use std::os::unix::fs::{FileTypeExt, MetadataExt}; use std::path::{Path, PathBuf}; - use uucore::error::UResult; -use crate::args::Settings; -use crate::text; - -// * This file is part of the uutils coreutils package. -// * -// * For the full copyright and license information, please view the LICENSE -// * file that was distributed with this source code. - #[derive(Debug, Clone)] pub enum InputKind { File(PathBuf), @@ -36,6 +26,7 @@ pub struct Input { } impl Input { + // TODO: from &str may be the better choice pub fn from(string: String) -> Self { let kind = if string == text::DASH { InputKind::Stdin @@ -132,44 +123,6 @@ impl HeaderPrinter { } } } - -#[derive(Debug, Clone)] -pub struct InputService { - pub inputs: VecDeque, - pub presume_input_pipe: bool, - pub header_printer: HeaderPrinter, -} - -impl InputService { - pub fn new(verbose: bool, presume_input_pipe: bool, inputs: VecDeque) -> Self { - Self { - inputs, - presume_input_pipe, - header_printer: HeaderPrinter::new(verbose, true), - } - } - - pub fn from(settings: &Settings) -> Self { - Self::new( - settings.verbose, - settings.presume_input_pipe, - settings.inputs.clone(), - ) - } - - pub fn has_stdin(&mut self) -> bool { - self.inputs.iter().any(|input| input.is_stdin()) - } - - pub fn has_only_stdin(&self) -> bool { - self.inputs.iter().all(|input| input.is_stdin()) - } - - pub fn print_header(&mut self, input: &Input) { - self.header_printer.print_input(input); - } -} - pub trait FileExtTail { #[allow(clippy::wrong_self_convention)] fn is_seekable(&mut self, current_offset: u64) -> bool; @@ -228,9 +181,11 @@ impl MetadataExtTail for Metadata { } #[cfg(windows)] { + // TODO: `file_index` requires unstable library feature `windows_by_handle` // use std::os::windows::prelude::*; // if let Some(self_id) = self.file_index() { // if let Some(other_id) = other.file_index() { + // // TODO: not sure this is the equivalent of comparing inode numbers // // return self_id.eq(&other_id); // } diff --git a/src/uu/tail/src/platform/mod.rs b/src/uu/tail/src/platform/mod.rs index 17660731d..e5ae8b8d8 100644 --- a/src/uu/tail/src/platform/mod.rs +++ b/src/uu/tail/src/platform/mod.rs @@ -11,7 +11,6 @@ #[cfg(unix)] pub use self::unix::{ //stdin_is_bad_fd, stdin_is_pipe_or_fifo, supports_pid_checks, Pid, ProcessChecker, - stdin_is_pipe_or_fifo, supports_pid_checks, Pid, ProcessChecker, diff --git a/src/uu/tail/src/platform/unix.rs b/src/uu/tail/src/platform/unix.rs index 2627cc252..ed34b2cf9 100644 --- a/src/uu/tail/src/platform/unix.rs +++ b/src/uu/tail/src/platform/unix.rs @@ -11,8 +11,6 @@ // spell-checker:ignore (ToDO) stdlib, ISCHR, GETFD // spell-checker:ignore (options) EPERM, ENOSYS -use libc::S_IFCHR; -use nix::sys::stat::fstat; use std::io::Error; pub type Pid = libc::pid_t; @@ -45,13 +43,6 @@ pub fn supports_pid_checks(pid: self::Pid) -> bool { fn get_errno() -> i32 { Error::last_os_error().raw_os_error().unwrap() } -#[inline] -pub fn stdin_is_pipe_or_fifo() -> bool { - // IFCHR means the file (stdin) is a character input device, which is the case of a terminal. - // We just need to check if stdin is not a character device here, because we are not interested - // in the type of stdin itself. - fstat(libc::STDIN_FILENO).map_or(false, |file| file.st_mode as libc::mode_t & S_IFCHR == 0) -} //pub fn stdin_is_bad_fd() -> bool { // FIXME: Detect a closed file descriptor, e.g.: `tail <&-` diff --git a/src/uu/tail/src/tail.rs b/src/uu/tail/src/tail.rs index af56c9813..771284c5c 100644 --- a/src/uu/tail/src/tail.rs +++ b/src/uu/tail/src/tail.rs @@ -24,58 +24,61 @@ mod paths; mod platform; pub mod text; +pub use args::uu_app; +use args::{parse_args, FilterMode, Settings, Signum}; +use chunks::ReverseChunks; +use follow::Observer; +use paths::{FileExtTail, HeaderPrinter, Input, InputKind, MetadataExtTail}; use same_file::Handle; use std::cmp::Ordering; use std::fs::File; use std::io::{self, stdin, stdout, BufRead, BufReader, BufWriter, Read, Seek, SeekFrom, Write}; use std::path::{Path, PathBuf}; -use uucore::{show, show_error, show_warning}; - use uucore::display::Quotable; use uucore::error::{get_exit_code, set_exit_code, FromIo, UError, UResult, USimpleError}; - -pub use args::uu_app; -use args::{parse_args, FilterMode, Settings, Signum}; -use chunks::ReverseChunks; -use follow::WatcherService; -use paths::{FileExtTail, Input, InputKind, InputService, MetadataExtTail}; +use uucore::{show, show_error}; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let settings = parse_args(args)?; - uu_tail(&settings) -} -fn uu_tail(settings: &Settings) -> UResult<()> { - // Mimic GNU's tail for `tail -F` and exit immediately - let mut input_service = InputService::from(settings); - let mut watcher_service = WatcherService::from(settings); + let mut observer = Observer::from(&settings); - if input_service.has_stdin() && watcher_service.follow_name() { - return Err(USimpleError::new( - 1, - format!("cannot follow {} by name", text::DASH.quote()), - )); + match settings.check_warnings() { + args::CheckResult::NoPidSupport => observer.pid = 0, + args::CheckResult::Ok => {} } - watcher_service.start(settings)?; + match settings.verify() { + args::VerificationResult::CannotFollowStdinByName => { + return Err(USimpleError::new( + 1, + format!("cannot follow {} by name", text::DASH.quote()), + )) + } + // Exit early if we do not output anything. Note, that this may break a pipe + // when tail is on the receiving side. + args::VerificationResult::NoOutput => return Ok(()), + args::VerificationResult::Ok => {} + } + + uu_tail(&settings, observer) +} + +fn uu_tail(settings: &Settings, mut observer: Observer) -> UResult<()> { + let mut printer = HeaderPrinter::new(settings.verbose, true); + + observer.start(settings)?; // Do an initial tail print of each path's content. // Add `path` and `reader` to `files` map if `--follow` is selected. - for input in &input_service.inputs.clone() { + for input in &settings.inputs.clone() { match input.kind() { InputKind::File(path) if cfg!(not(unix)) || path != &PathBuf::from(text::DEV_STDIN) => { - tail_file( - settings, - &mut input_service, - input, - path, - &mut watcher_service, - 0, - )?; + tail_file(settings, &mut printer, input, path, &mut observer, 0)?; } // File points to /dev/stdin here InputKind::File(_) | InputKind::Stdin => { - tail_stdin(settings, &mut input_service, input, &mut watcher_service)?; + tail_stdin(settings, &mut printer, input, &mut observer)?; } } } @@ -90,9 +93,8 @@ fn uu_tail(settings: &Settings) -> UResult<()> { the input file is not a FIFO, pipe, or regular file, it is unspecified whether or not the -f option shall be ignored. */ - - if !input_service.has_only_stdin() { - follow::follow(watcher_service, settings)?; + if !settings.has_only_stdin() { + follow::follow(observer, settings)?; } } @@ -105,16 +107,12 @@ fn uu_tail(settings: &Settings) -> UResult<()> { fn tail_file( settings: &Settings, - input_service: &mut InputService, + header_printer: &mut HeaderPrinter, input: &Input, path: &Path, - watcher_service: &mut WatcherService, + observer: &mut Observer, offset: u64, ) -> UResult<()> { - if watcher_service.follow_descriptor_retry() { - show_warning!("--retry only effective for the initial open"); - } - if !path.exists() { set_exit_code(1); show_error!( @@ -122,11 +120,11 @@ fn tail_file( input.display_name, text::NO_SUCH_FILE ); - watcher_service.add_bad_path(path, input.display_name.as_str(), false)?; + observer.add_bad_path(path, input.display_name.as_str(), false)?; } else if path.is_dir() { set_exit_code(1); - input_service.print_header(input); + header_printer.print_input(input); let err_msg = "Is a directory".to_string(); show_error!("error reading '{}': {}", input.display_name, err_msg); @@ -142,16 +140,16 @@ fn tail_file( msg ); } - if !(watcher_service.follow_name_retry()) { + if !(observer.follow_name_retry()) { // skip directory if not retry return Ok(()); } - watcher_service.add_bad_path(path, input.display_name.as_str(), false)?; + observer.add_bad_path(path, input.display_name.as_str(), false)?; } else if input.is_tailable() { let metadata = path.metadata().ok(); match File::open(path) { Ok(mut file) => { - input_service.print_header(input); + header_printer.print_input(input); let mut reader; if !settings.presume_input_pipe && file.is_seekable(if input.is_stdin() { offset } else { 0 }) @@ -163,7 +161,7 @@ fn tail_file( reader = BufReader::new(file); unbounded_tail(&mut reader, settings)?; } - watcher_service.add_path( + observer.add_path( path, input.display_name.as_str(), Some(Box::new(reader)), @@ -171,20 +169,20 @@ fn tail_file( )?; } Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => { - watcher_service.add_bad_path(path, input.display_name.as_str(), false)?; + observer.add_bad_path(path, input.display_name.as_str(), false)?; show!(e.map_err_context(|| { format!("cannot open '{}' for reading", input.display_name) })); } Err(e) => { - watcher_service.add_bad_path(path, input.display_name.as_str(), false)?; + observer.add_bad_path(path, input.display_name.as_str(), false)?; return Err(e.map_err_context(|| { format!("cannot open '{}' for reading", input.display_name) })); } } } else { - watcher_service.add_bad_path(path, input.display_name.as_str(), false)?; + observer.add_bad_path(path, input.display_name.as_str(), false)?; } Ok(()) @@ -192,9 +190,9 @@ fn tail_file( fn tail_stdin( settings: &Settings, - input_service: &mut InputService, + header_printer: &mut HeaderPrinter, input: &Input, - watcher_service: &mut WatcherService, + observer: &mut Observer, ) -> UResult<()> { match input.resolve() { // fifo @@ -211,24 +209,20 @@ fn tail_stdin( } tail_file( settings, - input_service, + header_printer, input, &path, - watcher_service, + observer, stdin_offset, )?; } // pipe None => { - input_service.print_header(input); + header_printer.print_input(input); if !paths::stdin_is_bad_fd() { let mut reader = BufReader::new(stdin()); unbounded_tail(&mut reader, settings)?; - watcher_service.add_stdin( - input.display_name.as_str(), - Some(Box::new(reader)), - true, - )?; + observer.add_stdin(input.display_name.as_str(), Some(Box::new(reader)), true)?; } else { set_exit_code(1); show_error!( @@ -417,7 +411,7 @@ fn unbounded_tail(reader: &mut BufReader, settings: &Settings) -> UR FilterMode::Lines(Signum::Negative(count), sep) => { let mut chunks = chunks::LinesChunkBuffer::new(*sep, *count); chunks.fill(reader)?; - chunks.print(writer)?; + chunks.print(&mut writer)?; } FilterMode::Lines(Signum::PlusZero | Signum::Positive(1), _) => { io::copy(reader, &mut writer)?; @@ -441,7 +435,7 @@ fn unbounded_tail(reader: &mut BufReader, settings: &Settings) -> UR FilterMode::Bytes(Signum::Negative(count)) => { let mut chunks = chunks::BytesChunkBuffer::new(*count); chunks.fill(reader)?; - chunks.print(writer)?; + chunks.print(&mut writer)?; } FilterMode::Bytes(Signum::PlusZero | Signum::Positive(1)) => { io::copy(reader, &mut writer)?; diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index e59dcf3ea..c763204ad 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -11,6 +11,7 @@ extern crate tail; use crate::common::random::*; use crate::common::util::*; +use pretty_assertions::assert_eq; use rand::distributions::Alphanumeric; use std::char::from_digit; use std::io::Write;