mirror of
https://github.com/uutils/coreutils
synced 2024-12-14 15:22:38 +00:00
tail: Refactor: Handle warnings and early exits in one place. Remove InputService ...
Other changes summary: * Cleanup imports * Remove stdin_is_pipe_of_fifo function * Rename WatcherService to Observer
This commit is contained in:
parent
01153a701f
commit
d57545d09c
10 changed files with 152 additions and 184 deletions
|
@ -113,6 +113,18 @@ pub enum FollowMode {
|
||||||
Name,
|
Name,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum VerificationResult {
|
||||||
|
Ok,
|
||||||
|
CannotFollowStdinByName,
|
||||||
|
NoOutput,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum CheckResult {
|
||||||
|
Ok,
|
||||||
|
NoPidSupport,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
pub follow: Option<FollowMode>,
|
pub follow: Option<FollowMode>,
|
||||||
|
@ -149,10 +161,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),
|
||||||
|
@ -195,13 +203,6 @@ impl Settings {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
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 +215,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 +234,54 @@ 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 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>(
|
pub fn arg_iterate<'a>(
|
||||||
|
@ -298,19 +337,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)
|
||||||
|
|
|
@ -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};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -6,4 +6,4 @@
|
||||||
mod files;
|
mod files;
|
||||||
mod watch;
|
mod watch;
|
||||||
|
|
||||||
pub use watch::{follow, WatcherService};
|
pub use watch::{follow, Observer};
|
||||||
|
|
|
@ -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,17 +91,21 @@ 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 {
|
||||||
Self {
|
Self {
|
||||||
retry,
|
retry,
|
||||||
|
@ -111,6 +114,7 @@ impl WatcherService {
|
||||||
watcher_rx: None,
|
watcher_rx: None,
|
||||||
orphans: Vec::new(),
|
orphans: Vec::new(),
|
||||||
files,
|
files,
|
||||||
|
pid,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,6 +124,7 @@ impl WatcherService {
|
||||||
settings.follow,
|
settings.follow,
|
||||||
settings.use_polling,
|
settings.use_polling,
|
||||||
FileHandling::from(settings),
|
FileHandling::from(settings),
|
||||||
|
settings.pid,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -460,14 +465,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 +481,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 +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.
|
// 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 +515,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 +530,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 +541,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 +567,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 {
|
||||||
|
|
|
@ -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);
|
||||||
// }
|
// }
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 <&-`
|
||||||
|
|
|
@ -24,58 +24,61 @@ 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)?;
|
||||||
uu_tail(&settings)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn uu_tail(settings: &Settings) -> UResult<()> {
|
let mut observer = Observer::from(&settings);
|
||||||
// Mimic GNU's tail for `tail -F` and exit immediately
|
|
||||||
let mut input_service = InputService::from(settings);
|
|
||||||
let mut watcher_service = WatcherService::from(settings);
|
|
||||||
|
|
||||||
if input_service.has_stdin() && watcher_service.follow_name() {
|
match settings.check_warnings() {
|
||||||
|
args::CheckResult::NoPidSupport => observer.pid = 0,
|
||||||
|
args::CheckResult::Ok => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
match settings.verify() {
|
||||||
|
args::VerificationResult::CannotFollowStdinByName => {
|
||||||
return Err(USimpleError::new(
|
return Err(USimpleError::new(
|
||||||
1,
|
1,
|
||||||
format!("cannot follow {} by name", text::DASH.quote()),
|
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 => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
watcher_service.start(settings)?;
|
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.
|
// 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 +93,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 +107,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 +120,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 +140,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 +161,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 +169,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 +190,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 +209,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 +411,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 +435,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)?;
|
||||||
|
|
|
@ -11,6 +11,7 @@ extern crate tail;
|
||||||
|
|
||||||
use crate::common::random::*;
|
use crate::common::random::*;
|
||||||
use crate::common::util::*;
|
use crate::common::util::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
use rand::distributions::Alphanumeric;
|
use rand::distributions::Alphanumeric;
|
||||||
use std::char::from_digit;
|
use std::char::from_digit;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
Loading…
Reference in a new issue