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

Merge pull request #4135 from Joining7943/refactor-tail-check-warnings

`tail`: Refactor handling of warnings and early exits
This commit is contained in:
Terts Diepraam 2022-12-14 13:21:54 +01:00 committed by GitHub
commit e4bed1c087
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 1283 additions and 257 deletions

1
Cargo.lock generated
View file

@ -2985,6 +2985,7 @@ dependencies = [
name = "uu_tail" name = "uu_tail"
version = "0.0.16" version = "0.0.16"
dependencies = [ dependencies = [
"atty",
"clap", "clap",
"libc", "libc",
"memchr", "memchr",

View file

@ -22,6 +22,7 @@ memchr = "2.5.0"
notify = { version = "=5.0.0", features=["macos_kqueue"]} notify = { version = "=5.0.0", features=["macos_kqueue"]}
uucore = { version=">=0.0.16", package="uucore", path="../../uucore", features=["ringbuffer", "lines"] } uucore = { version=">=0.0.16", package="uucore", path="../../uucore", features=["ringbuffer", "lines"] }
same-file = "1.0.6" same-file = "1.0.6"
atty = "0.2"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.42.0", default-features = false, features = ["Win32_System_Threading", "Win32_Foundation"] } windows-sys = { version = "0.42.0", default-features = false, features = ["Win32_System_Threading", "Win32_Foundation"] }

View file

@ -7,8 +7,10 @@
use crate::paths::Input; use crate::paths::Input;
use crate::{parse, platform, Quotable}; use crate::{parse, platform, Quotable};
use atty::Stream;
use clap::crate_version; use clap::crate_version;
use clap::{parser::ValueSource, Arg, ArgAction, ArgMatches, Command}; use clap::{parser::ValueSource, Arg, ArgAction, ArgMatches, Command};
use same_file::Handle;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::ffi::OsString; use std::ffi::OsString;
use std::time::Duration; use std::time::Duration;
@ -113,6 +115,13 @@ pub enum FollowMode {
Name, Name,
} }
#[derive(Debug)]
pub enum VerificationResult {
Ok,
CannotFollowStdinByName,
NoOutput,
}
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Settings { pub struct Settings {
pub follow: Option<FollowMode>, pub follow: Option<FollowMode>,
@ -149,10 +158,6 @@ impl Settings {
settings.retry = settings.retry =
matches.get_flag(options::RETRY) || matches.get_flag(options::FOLLOW_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::<String>(options::SLEEP_INT) { if let Some(s) = matches.get_one::<String>(options::SLEEP_INT) {
settings.sleep_sec = match s.parse::<f32>() { settings.sleep_sec = match s.parse::<f32>() {
Ok(s) => Duration::from_secs_f32(s), Ok(s) => Duration::from_secs_f32(s),
@ -194,14 +199,8 @@ impl Settings {
format!("invalid PID: {}", pid_str.quote()), format!("invalid PID: {}", pid_str.quote()),
)); ));
} }
settings.pid = pid; 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) => { Err(e) => {
return Err(USimpleError::new( return Err(USimpleError::new(
@ -214,16 +213,6 @@ impl Settings {
settings.mode = FilterMode::from(matches)?; 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<Input> = matches let mut inputs: VecDeque<Input> = matches
.get_many::<String>(options::ARG_FILES) .get_many::<String>(options::ARG_FILES)
.map(|v| v.map(|string| Input::from(string.clone())).collect()) .map(|v| v.map(|string| Input::from(string.clone())).collect())
@ -243,6 +232,81 @@ impl Settings {
Ok(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 num_inputs(&self) -> usize {
self.inputs.len()
}
/// Check [`Settings`] for problematic configurations of tail originating from user provided
/// command line arguments and print appropriate warnings.
pub fn check_warnings(&self) {
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");
}
}
// This warning originates from gnu's tail implementation of the equivalent warning. If the
// user wants to follow stdin, but tail is blocking indefinitely anyways, because of stdin
// as `tty` (but no otherwise blocking stdin), then we print a warning that `--follow`
// cannot be applied under these circumstances and is therefore ineffective.
if self.follow.is_some() && self.has_stdin() {
let blocking_stdin = self.pid == 0
&& self.follow == Some(FollowMode::Descriptor)
&& self.num_inputs() == 1
&& Handle::stdin().map_or(false, |handle| {
handle
.as_file()
.metadata()
.map_or(false, |meta| !meta.is_file())
});
if !blocking_stdin && atty::is(Stream::Stdin) {
show_warning!("following standard input indefinitely is ineffective");
}
}
}
/// Verify [`Settings`] and try to find unsolvable misconfigurations of tail originating from
/// user provided command line arguments. In contrast to [`Settings::check_warnings`] these
/// misconfigurations usually lead to the immediate exit or abortion of the running `tail`
/// process.
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>( pub fn arg_iterate<'a>(
@ -298,19 +362,6 @@ fn parse_num(src: &str) -> Result<Signum, ParseSizeError> {
}) })
} }
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<Settings> { pub fn parse_args(args: impl uucore::Args) -> UResult<Settings> {
let matches = uu_app().try_get_matches_from(arg_iterate(args)?)?; let matches = uu_app().try_get_matches_from(arg_iterate(args)?)?;
Settings::from(&matches) Settings::from(&matches)

View file

@ -7,7 +7,9 @@
//! or at the end of piped stdin with [`LinesChunk`] or [`BytesChunk`]. //! 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. //! Use [`ReverseChunks::new`] to create a new iterator over chunks of bytes from the file.
// spell-checker:ignore (ToDO) filehandle BUFSIZ // spell-checker:ignore (ToDO) filehandle BUFSIZ
use std::collections::VecDeque; use std::collections::VecDeque;
use std::fs::File; use std::fs::File;
use std::io::{BufRead, Read, Seek, SeekFrom, Write}; use std::io::{BufRead, Read, Seek, SeekFrom, Write};

View file

@ -13,7 +13,6 @@ use std::collections::hash_map::Keys;
use std::collections::HashMap; use std::collections::HashMap;
use std::fs::{File, Metadata}; use std::fs::{File, Metadata};
use std::io::{stdout, BufRead, BufReader, BufWriter}; use std::io::{stdout, BufRead, BufReader, BufWriter};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use uucore::error::UResult; use uucore::error::UResult;

View file

@ -6,4 +6,4 @@
mod files; mod files;
mod watch; mod watch;
pub use watch::{follow, WatcherService}; pub use watch::{follow, Observer};

View file

@ -13,8 +13,7 @@ use notify::{RecommendedWatcher, RecursiveMode, Watcher, WatcherKind};
use std::collections::VecDeque; use std::collections::VecDeque;
use std::io::BufRead; use std::io::BufRead;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::mpsc; use std::sync::mpsc::{self, channel, Receiver};
use std::sync::mpsc::{channel, Receiver};
use uucore::display::Quotable; use uucore::display::Quotable;
use uucore::error::{set_exit_code, UResult, USimpleError}; use uucore::error::{set_exit_code, UResult, USimpleError};
use uucore::show_error; use uucore::show_error;
@ -81,7 +80,7 @@ impl WatcherRx {
} }
} }
pub struct WatcherService { pub struct Observer {
/// Whether --retry was given on the command line /// Whether --retry was given on the command line
pub retry: bool, pub retry: bool,
@ -92,18 +91,28 @@ pub struct WatcherService {
/// platform specific event driven method. Since `use_polling` is subject to /// platform specific event driven method. Since `use_polling` is subject to
/// change during runtime it is moved out of [`Settings`]. /// change during runtime it is moved out of [`Settings`].
pub use_polling: bool, pub use_polling: bool,
pub watcher_rx: Option<WatcherRx>, pub watcher_rx: Option<WatcherRx>,
pub orphans: Vec<PathBuf>, pub orphans: Vec<PathBuf>,
pub files: FileHandling, pub files: FileHandling,
pub pid: platform::Pid,
} }
impl WatcherService { impl Observer {
pub fn new( pub fn new(
retry: bool, retry: bool,
follow: Option<FollowMode>, follow: Option<FollowMode>,
use_polling: bool, use_polling: bool,
files: FileHandling, files: FileHandling,
pid: platform::Pid,
) -> Self { ) -> Self {
let pid = if platform::supports_pid_checks(pid) {
pid
} else {
0
};
Self { Self {
retry, retry,
follow, follow,
@ -111,6 +120,7 @@ impl WatcherService {
watcher_rx: None, watcher_rx: None,
orphans: Vec::new(), orphans: Vec::new(),
files, files,
pid,
} }
} }
@ -120,6 +130,7 @@ impl WatcherService {
settings.follow, settings.follow,
settings.use_polling, settings.use_polling,
FileHandling::from(settings), FileHandling::from(settings),
settings.pid,
) )
} }
@ -460,14 +471,12 @@ impl WatcherService {
} }
} }
pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResult<()> { pub fn follow(mut observer: Observer, settings: &Settings) -> UResult<()> {
if watcher_service.files.no_files_remaining(settings) if observer.files.no_files_remaining(settings) && !observer.files.only_stdin_remaining() {
&& !watcher_service.files.only_stdin_remaining()
{
return Err(USimpleError::new(1, text::NO_FILES_REMAINING.to_string())); 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 _event_counter = 0;
let mut _timeout_counter = 0; let mut _timeout_counter = 0;
@ -478,7 +487,7 @@ pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResu
// If `--pid=p`, tail checks whether process p // If `--pid=p`, tail checks whether process p
// is alive at least every `--sleep-interval=N` seconds // 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 // p is dead, tail will also terminate
break; break;
} }
@ -487,22 +496,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. // 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, // 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. // here paths will not be removed from orphans if the path becomes available.
if watcher_service.follow_name_retry() { if observer.follow_name_retry() {
for new_path in &watcher_service.orphans { for new_path in &observer.orphans {
if new_path.exists() { 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(); let md = new_path.metadata().unwrap();
if md.is_tailable() && pd.reader.is_none() { if md.is_tailable() && pd.reader.is_none() {
show_error!( show_error!(
"{} has appeared; following new file", "{} has appeared; following new file",
pd.display_name.quote() pd.display_name.quote()
); );
watcher_service.files.update_metadata(new_path, Some(md)); observer.files.update_metadata(new_path, Some(md));
watcher_service.files.update_reader(new_path)?; observer.files.update_reader(new_path)?;
_read_some = watcher_service _read_some = observer.files.tail_file(new_path, settings.verbose)?;
.files observer
.tail_file(new_path, settings.verbose)?;
watcher_service
.watcher_rx .watcher_rx
.as_mut() .as_mut()
.unwrap() .unwrap()
@ -514,7 +521,7 @@ pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResu
// With -f, sleep for approximately N seconds (default 1.0) between iterations; // 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`. // 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 .watcher_rx
.as_mut() .as_mut()
.unwrap() .unwrap()
@ -529,9 +536,9 @@ pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResu
match rx_result { match rx_result {
Ok(Ok(event)) => { Ok(Ok(event)) => {
if let Some(event_path) = event.paths.first() { 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 // 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 +547,8 @@ pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResu
paths, paths,
})) if e.kind() == std::io::ErrorKind::NotFound => { })) if e.kind() == std::io::ErrorKind::NotFound => {
if let Some(event_path) = paths.first() { if let Some(event_path) = paths.first() {
if watcher_service.files.contains_key(event_path) { if observer.files.contains_key(event_path) {
let _ = watcher_service let _ = observer
.watcher_rx .watcher_rx
.as_mut() .as_mut()
.unwrap() .unwrap()
@ -566,16 +573,16 @@ pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResu
Err(e) => return Err(USimpleError::new(1, format!("RecvTimeoutError: {}", e))), 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. // Consider all files to potentially have new content.
// This is a workaround because `Notify::PollWatcher` // This is a workaround because `Notify::PollWatcher`
// does not recognize the "renaming" of files. // does not recognize the "renaming" of files.
paths = watcher_service.files.keys().cloned().collect::<Vec<_>>(); paths = observer.files.keys().cloned().collect::<Vec<_>>();
} }
// main print loop // main print loop
for path in &paths { 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 { if _timeout_counter == settings.max_unchanged_stats {

View file

@ -5,24 +5,14 @@
// spell-checker:ignore tailable seekable stdlib (stdlib) // spell-checker:ignore tailable seekable stdlib (stdlib)
#[cfg(unix)] use crate::text;
use std::os::unix::fs::{FileTypeExt, MetadataExt};
use std::collections::VecDeque;
use std::fs::{File, Metadata}; use std::fs::{File, Metadata};
use std::io::{Seek, SeekFrom}; use std::io::{Seek, SeekFrom};
#[cfg(unix)]
use std::os::unix::fs::{FileTypeExt, MetadataExt};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use uucore::error::UResult; 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)] #[derive(Debug, Clone)]
pub enum InputKind { pub enum InputKind {
File(PathBuf), File(PathBuf),
@ -36,6 +26,7 @@ pub struct Input {
} }
impl Input { impl Input {
// TODO: from &str may be the better choice
pub fn from(string: String) -> Self { pub fn from(string: String) -> Self {
let kind = if string == text::DASH { let kind = if string == text::DASH {
InputKind::Stdin InputKind::Stdin
@ -132,44 +123,6 @@ impl HeaderPrinter {
} }
} }
} }
#[derive(Debug, Clone)]
pub struct InputService {
pub inputs: VecDeque<Input>,
pub presume_input_pipe: bool,
pub header_printer: HeaderPrinter,
}
impl InputService {
pub fn new(verbose: bool, presume_input_pipe: bool, inputs: VecDeque<Input>) -> 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 { pub trait FileExtTail {
#[allow(clippy::wrong_self_convention)] #[allow(clippy::wrong_self_convention)]
fn is_seekable(&mut self, current_offset: u64) -> bool; fn is_seekable(&mut self, current_offset: u64) -> bool;
@ -228,9 +181,11 @@ impl MetadataExtTail for Metadata {
} }
#[cfg(windows)] #[cfg(windows)]
{ {
// TODO: `file_index` requires unstable library feature `windows_by_handle`
// use std::os::windows::prelude::*; // use std::os::windows::prelude::*;
// if let Some(self_id) = self.file_index() { // if let Some(self_id) = self.file_index() {
// if let Some(other_id) = other.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); // return self_id.eq(&other_id);
// } // }

View file

@ -11,7 +11,6 @@
#[cfg(unix)] #[cfg(unix)]
pub use self::unix::{ pub use self::unix::{
//stdin_is_bad_fd, stdin_is_pipe_or_fifo, supports_pid_checks, Pid, ProcessChecker, //stdin_is_bad_fd, stdin_is_pipe_or_fifo, supports_pid_checks, Pid, ProcessChecker,
stdin_is_pipe_or_fifo,
supports_pid_checks, supports_pid_checks,
Pid, Pid,
ProcessChecker, ProcessChecker,

View file

@ -11,8 +11,6 @@
// spell-checker:ignore (ToDO) stdlib, ISCHR, GETFD // spell-checker:ignore (ToDO) stdlib, ISCHR, GETFD
// spell-checker:ignore (options) EPERM, ENOSYS // spell-checker:ignore (options) EPERM, ENOSYS
use libc::S_IFCHR;
use nix::sys::stat::fstat;
use std::io::Error; use std::io::Error;
pub type Pid = libc::pid_t; pub type Pid = libc::pid_t;
@ -45,13 +43,6 @@ pub fn supports_pid_checks(pid: self::Pid) -> bool {
fn get_errno() -> i32 { fn get_errno() -> i32 {
Error::last_os_error().raw_os_error().unwrap() 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 { //pub fn stdin_is_bad_fd() -> bool {
// FIXME: Detect a closed file descriptor, e.g.: `tail <&-` // FIXME: Detect a closed file descriptor, e.g.: `tail <&-`

View file

@ -24,58 +24,57 @@ mod paths;
mod platform; mod platform;
pub mod text; 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 same_file::Handle;
use std::cmp::Ordering; use std::cmp::Ordering;
use std::fs::File; use std::fs::File;
use std::io::{self, stdin, stdout, BufRead, BufReader, BufWriter, Read, Seek, SeekFrom, Write}; use std::io::{self, stdin, stdout, BufRead, BufReader, BufWriter, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use uucore::{show, show_error, show_warning};
use uucore::display::Quotable; use uucore::display::Quotable;
use uucore::error::{get_exit_code, set_exit_code, FromIo, UError, UResult, USimpleError}; use uucore::error::{get_exit_code, set_exit_code, FromIo, UError, UResult, USimpleError};
use uucore::{show, show_error};
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};
#[uucore::main] #[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let settings = parse_args(args)?; let settings = parse_args(args)?;
settings.check_warnings();
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) uu_tail(&settings)
} }
fn uu_tail(settings: &Settings) -> UResult<()> { fn uu_tail(settings: &Settings) -> UResult<()> {
// Mimic GNU's tail for `tail -F` and exit immediately let mut printer = HeaderPrinter::new(settings.verbose, true);
let mut input_service = InputService::from(settings); let mut observer = Observer::from(settings);
let mut watcher_service = WatcherService::from(settings);
if input_service.has_stdin() && watcher_service.follow_name() { observer.start(settings)?;
return Err(USimpleError::new(
1,
format!("cannot follow {} by name", text::DASH.quote()),
));
}
watcher_service.start(settings)?;
// Do an initial tail print of each path's content. // Do an initial tail print of each path's content.
// Add `path` and `reader` to `files` map if `--follow` is selected. // 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() { match input.kind() {
InputKind::File(path) if cfg!(not(unix)) || path != &PathBuf::from(text::DEV_STDIN) => { InputKind::File(path) if cfg!(not(unix)) || path != &PathBuf::from(text::DEV_STDIN) => {
tail_file( tail_file(settings, &mut printer, input, path, &mut observer, 0)?;
settings,
&mut input_service,
input,
path,
&mut watcher_service,
0,
)?;
} }
// File points to /dev/stdin here // File points to /dev/stdin here
InputKind::File(_) | InputKind::Stdin => { InputKind::File(_) | InputKind::Stdin => {
tail_stdin(settings, &mut input_service, input, &mut watcher_service)?; tail_stdin(settings, &mut printer, input, &mut observer)?;
} }
} }
} }
@ -90,9 +89,8 @@ fn uu_tail(settings: &Settings) -> UResult<()> {
the input file is not a FIFO, pipe, or regular file, it is unspecified whether or the input file is not a FIFO, pipe, or regular file, it is unspecified whether or
not the -f option shall be ignored. not the -f option shall be ignored.
*/ */
if !settings.has_only_stdin() {
if !input_service.has_only_stdin() { follow::follow(observer, settings)?;
follow::follow(watcher_service, settings)?;
} }
} }
@ -105,16 +103,12 @@ fn uu_tail(settings: &Settings) -> UResult<()> {
fn tail_file( fn tail_file(
settings: &Settings, settings: &Settings,
input_service: &mut InputService, header_printer: &mut HeaderPrinter,
input: &Input, input: &Input,
path: &Path, path: &Path,
watcher_service: &mut WatcherService, observer: &mut Observer,
offset: u64, offset: u64,
) -> UResult<()> { ) -> UResult<()> {
if watcher_service.follow_descriptor_retry() {
show_warning!("--retry only effective for the initial open");
}
if !path.exists() { if !path.exists() {
set_exit_code(1); set_exit_code(1);
show_error!( show_error!(
@ -122,11 +116,11 @@ fn tail_file(
input.display_name, input.display_name,
text::NO_SUCH_FILE 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() { } else if path.is_dir() {
set_exit_code(1); set_exit_code(1);
input_service.print_header(input); header_printer.print_input(input);
let err_msg = "Is a directory".to_string(); let err_msg = "Is a directory".to_string();
show_error!("error reading '{}': {}", input.display_name, err_msg); show_error!("error reading '{}': {}", input.display_name, err_msg);
@ -142,16 +136,16 @@ fn tail_file(
msg msg
); );
} }
if !(watcher_service.follow_name_retry()) { if !(observer.follow_name_retry()) {
// skip directory if not retry // skip directory if not retry
return Ok(()); 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() { } else if input.is_tailable() {
let metadata = path.metadata().ok(); let metadata = path.metadata().ok();
match File::open(path) { match File::open(path) {
Ok(mut file) => { Ok(mut file) => {
input_service.print_header(input); header_printer.print_input(input);
let mut reader; let mut reader;
if !settings.presume_input_pipe if !settings.presume_input_pipe
&& file.is_seekable(if input.is_stdin() { offset } else { 0 }) && file.is_seekable(if input.is_stdin() { offset } else { 0 })
@ -163,7 +157,7 @@ fn tail_file(
reader = BufReader::new(file); reader = BufReader::new(file);
unbounded_tail(&mut reader, settings)?; unbounded_tail(&mut reader, settings)?;
} }
watcher_service.add_path( observer.add_path(
path, path,
input.display_name.as_str(), input.display_name.as_str(),
Some(Box::new(reader)), Some(Box::new(reader)),
@ -171,20 +165,20 @@ fn tail_file(
)?; )?;
} }
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => { 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(|| { show!(e.map_err_context(|| {
format!("cannot open '{}' for reading", input.display_name) format!("cannot open '{}' for reading", input.display_name)
})); }));
} }
Err(e) => { 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(|| { return Err(e.map_err_context(|| {
format!("cannot open '{}' for reading", input.display_name) format!("cannot open '{}' for reading", input.display_name)
})); }));
} }
} }
} else { } 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(()) Ok(())
@ -192,9 +186,9 @@ fn tail_file(
fn tail_stdin( fn tail_stdin(
settings: &Settings, settings: &Settings,
input_service: &mut InputService, header_printer: &mut HeaderPrinter,
input: &Input, input: &Input,
watcher_service: &mut WatcherService, observer: &mut Observer,
) -> UResult<()> { ) -> UResult<()> {
match input.resolve() { match input.resolve() {
// fifo // fifo
@ -211,24 +205,20 @@ fn tail_stdin(
} }
tail_file( tail_file(
settings, settings,
input_service, header_printer,
input, input,
&path, &path,
watcher_service, observer,
stdin_offset, stdin_offset,
)?; )?;
} }
// pipe // pipe
None => { None => {
input_service.print_header(input); header_printer.print_input(input);
if !paths::stdin_is_bad_fd() { if !paths::stdin_is_bad_fd() {
let mut reader = BufReader::new(stdin()); let mut reader = BufReader::new(stdin());
unbounded_tail(&mut reader, settings)?; unbounded_tail(&mut reader, settings)?;
watcher_service.add_stdin( observer.add_stdin(input.display_name.as_str(), Some(Box::new(reader)), true)?;
input.display_name.as_str(),
Some(Box::new(reader)),
true,
)?;
} else { } else {
set_exit_code(1); set_exit_code(1);
show_error!( show_error!(
@ -417,7 +407,7 @@ fn unbounded_tail<T: Read>(reader: &mut BufReader<T>, settings: &Settings) -> UR
FilterMode::Lines(Signum::Negative(count), sep) => { FilterMode::Lines(Signum::Negative(count), sep) => {
let mut chunks = chunks::LinesChunkBuffer::new(*sep, *count); let mut chunks = chunks::LinesChunkBuffer::new(*sep, *count);
chunks.fill(reader)?; chunks.fill(reader)?;
chunks.print(writer)?; chunks.print(&mut writer)?;
} }
FilterMode::Lines(Signum::PlusZero | Signum::Positive(1), _) => { FilterMode::Lines(Signum::PlusZero | Signum::Positive(1), _) => {
io::copy(reader, &mut writer)?; io::copy(reader, &mut writer)?;
@ -441,7 +431,7 @@ fn unbounded_tail<T: Read>(reader: &mut BufReader<T>, settings: &Settings) -> UR
FilterMode::Bytes(Signum::Negative(count)) => { FilterMode::Bytes(Signum::Negative(count)) => {
let mut chunks = chunks::BytesChunkBuffer::new(*count); let mut chunks = chunks::BytesChunkBuffer::new(*count);
chunks.fill(reader)?; chunks.fill(reader)?;
chunks.print(writer)?; chunks.print(&mut writer)?;
} }
FilterMode::Bytes(Signum::PlusZero | Signum::Positive(1)) => { FilterMode::Bytes(Signum::PlusZero | Signum::Positive(1)) => {
io::copy(reader, &mut writer)?; io::copy(reader, &mut writer)?;

View file

@ -19,3 +19,6 @@ pub static BACKEND: &str = "kqueue";
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub static BACKEND: &str = "ReadDirectoryChanges"; pub static BACKEND: &str = "ReadDirectoryChanges";
pub static FD0: &str = "/dev/fd/0"; pub static FD0: &str = "/dev/fd/0";
pub static IS_A_DIRECTORY: &str = "Is a directory";
pub static DEV_TTY: &str = "/dev/tty";
pub static DEV_PTMX: &str = "/dev/ptmx";

File diff suppressed because it is too large Load diff