mirror of
https://github.com/uutils/coreutils
synced 2024-12-14 07:12:44 +00:00
commit
8c839888e1
9 changed files with 1848 additions and 1448 deletions
473
src/uu/tail/src/args.rs
Normal file
473
src/uu/tail/src/args.rs
Normal file
|
@ -0,0 +1,473 @@
|
|||
// * 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.
|
||||
|
||||
// spell-checker:ignore (ToDO) kqueue Signum
|
||||
|
||||
use crate::paths::Input;
|
||||
use crate::{parse, platform, Quotable};
|
||||
use clap::{Arg, ArgMatches, Command, ValueSource};
|
||||
use std::collections::VecDeque;
|
||||
use std::ffi::OsString;
|
||||
use std::time::Duration;
|
||||
use uucore::error::{UResult, USimpleError, UUsageError};
|
||||
use uucore::format_usage;
|
||||
use uucore::parse_size::{parse_size, ParseSizeError};
|
||||
|
||||
const ABOUT: &str = "\
|
||||
Print the last 10 lines of each FILE to standard output.\n\
|
||||
With more than one FILE, precede each with a header giving the file name.\n\
|
||||
With no FILE, or when FILE is -, read standard input.\n\
|
||||
\n\
|
||||
Mandatory arguments to long flags are mandatory for short flags too.\
|
||||
";
|
||||
const USAGE: &str = "{} [FLAG]... [FILE]...";
|
||||
|
||||
pub mod options {
|
||||
pub mod verbosity {
|
||||
pub static QUIET: &str = "quiet";
|
||||
pub static VERBOSE: &str = "verbose";
|
||||
}
|
||||
pub static BYTES: &str = "bytes";
|
||||
pub static FOLLOW: &str = "follow";
|
||||
pub static LINES: &str = "lines";
|
||||
pub static PID: &str = "pid";
|
||||
pub static SLEEP_INT: &str = "sleep-interval";
|
||||
pub static ZERO_TERM: &str = "zero-terminated";
|
||||
pub static DISABLE_INOTIFY_TERM: &str = "-disable-inotify"; // NOTE: three hyphens is correct
|
||||
pub static USE_POLLING: &str = "use-polling";
|
||||
pub static RETRY: &str = "retry";
|
||||
pub static FOLLOW_RETRY: &str = "F";
|
||||
pub static MAX_UNCHANGED_STATS: &str = "max-unchanged-stats";
|
||||
pub static ARG_FILES: &str = "files";
|
||||
pub static PRESUME_INPUT_PIPE: &str = "-presume-input-pipe"; // NOTE: three hyphens is correct
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Signum {
|
||||
Negative(u64),
|
||||
Positive(u64),
|
||||
PlusZero,
|
||||
MinusZero,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum FilterMode {
|
||||
Bytes(Signum),
|
||||
|
||||
/// Mode for lines delimited by delimiter as u8
|
||||
Lines(Signum, u8),
|
||||
}
|
||||
|
||||
impl FilterMode {
|
||||
fn from(matches: &ArgMatches) -> UResult<Self> {
|
||||
let zero_term = matches.contains_id(options::ZERO_TERM);
|
||||
let mode = if let Some(arg) = matches.value_of(options::BYTES) {
|
||||
match parse_num(arg) {
|
||||
Ok(signum) => Self::Bytes(signum),
|
||||
Err(e) => {
|
||||
return Err(UUsageError::new(
|
||||
1,
|
||||
format!("invalid number of bytes: {}", e),
|
||||
))
|
||||
}
|
||||
}
|
||||
} else if let Some(arg) = matches.value_of(options::LINES) {
|
||||
match parse_num(arg) {
|
||||
Ok(signum) => {
|
||||
let delimiter = if zero_term { 0 } else { b'\n' };
|
||||
Self::Lines(signum, delimiter)
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(UUsageError::new(
|
||||
1,
|
||||
format!("invalid number of lines: {}", e),
|
||||
))
|
||||
}
|
||||
}
|
||||
} else if zero_term {
|
||||
Self::default_zero()
|
||||
} else {
|
||||
Self::default()
|
||||
};
|
||||
|
||||
Ok(mode)
|
||||
}
|
||||
|
||||
fn default_zero() -> Self {
|
||||
Self::Lines(Signum::Negative(10), 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FilterMode {
|
||||
fn default() -> Self {
|
||||
Self::Lines(Signum::Negative(10), b'\n')
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum FollowMode {
|
||||
Descriptor,
|
||||
Name,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Settings {
|
||||
pub follow: Option<FollowMode>,
|
||||
pub max_unchanged_stats: u32,
|
||||
pub mode: FilterMode,
|
||||
pub pid: platform::Pid,
|
||||
pub retry: bool,
|
||||
pub sleep_sec: Duration,
|
||||
pub use_polling: bool,
|
||||
pub verbose: bool,
|
||||
pub presume_input_pipe: bool,
|
||||
pub inputs: VecDeque<Input>,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn from(matches: &clap::ArgMatches) -> UResult<Self> {
|
||||
let mut settings: Self = Self {
|
||||
sleep_sec: Duration::from_secs_f32(1.0),
|
||||
max_unchanged_stats: 5,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
settings.follow = if matches.contains_id(options::FOLLOW_RETRY) {
|
||||
Some(FollowMode::Name)
|
||||
} else if matches.value_source(options::FOLLOW) != Some(ValueSource::CommandLine) {
|
||||
None
|
||||
} else if matches.value_of(options::FOLLOW) == Some("name") {
|
||||
Some(FollowMode::Name)
|
||||
} else {
|
||||
Some(FollowMode::Descriptor)
|
||||
};
|
||||
|
||||
settings.retry =
|
||||
matches.contains_id(options::RETRY) || matches.contains_id(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.value_of(options::SLEEP_INT) {
|
||||
settings.sleep_sec = match s.parse::<f32>() {
|
||||
Ok(s) => Duration::from_secs_f32(s),
|
||||
Err(_) => {
|
||||
return Err(UUsageError::new(
|
||||
1,
|
||||
format!("invalid number of seconds: {}", s.quote()),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
settings.use_polling = matches.contains_id(options::USE_POLLING);
|
||||
|
||||
if let Some(s) = matches.value_of(options::MAX_UNCHANGED_STATS) {
|
||||
settings.max_unchanged_stats = match s.parse::<u32>() {
|
||||
Ok(s) => s,
|
||||
Err(_) => {
|
||||
return Err(UUsageError::new(
|
||||
1,
|
||||
format!(
|
||||
"invalid maximum number of unchanged stats between opens: {}",
|
||||
s.quote()
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(pid_str) = matches.value_of(options::PID) {
|
||||
match pid_str.parse() {
|
||||
Ok(pid) => {
|
||||
// NOTE: on unix platform::Pid is i32, on windows platform::Pid is u32
|
||||
#[cfg(unix)]
|
||||
if pid < 0 {
|
||||
// NOTE: tail only accepts an unsigned pid
|
||||
return Err(USimpleError::new(
|
||||
1,
|
||||
format!("invalid PID: {}", pid_str.quote()),
|
||||
));
|
||||
}
|
||||
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(
|
||||
1,
|
||||
format!("invalid PID: {}: {}", pid_str.quote(), e),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
.get_many::<String>(options::ARG_FILES)
|
||||
.map(|v| v.map(|string| Input::from(string.clone())).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
// apply default and add '-' to inputs if none is present
|
||||
if inputs.is_empty() {
|
||||
inputs.push_front(Input::default());
|
||||
}
|
||||
|
||||
settings.verbose = (matches.contains_id(options::verbosity::VERBOSE) || inputs.len() > 1)
|
||||
&& !matches.contains_id(options::verbosity::QUIET);
|
||||
|
||||
settings.inputs = inputs;
|
||||
|
||||
settings.presume_input_pipe = matches.contains_id(options::PRESUME_INPUT_PIPE);
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn arg_iterate<'a>(
|
||||
mut args: impl uucore::Args + 'a,
|
||||
) -> UResult<Box<dyn Iterator<Item = OsString> + 'a>> {
|
||||
// argv[0] is always present
|
||||
let first = args.next().unwrap();
|
||||
if let Some(second) = args.next() {
|
||||
if let Some(s) = second.to_str() {
|
||||
match parse::parse_obsolete(s) {
|
||||
Some(Ok(iter)) => Ok(Box::new(vec![first].into_iter().chain(iter).chain(args))),
|
||||
Some(Err(e)) => Err(UUsageError::new(
|
||||
1,
|
||||
match e {
|
||||
parse::ParseError::Syntax => format!("bad argument format: {}", s.quote()),
|
||||
parse::ParseError::Overflow => format!(
|
||||
"invalid argument: {} Value too large for defined datatype",
|
||||
s.quote()
|
||||
),
|
||||
},
|
||||
)),
|
||||
None => Ok(Box::new(vec![first, second].into_iter().chain(args))),
|
||||
}
|
||||
} else {
|
||||
Err(UUsageError::new(1, "bad argument encoding".to_owned()))
|
||||
}
|
||||
} else {
|
||||
Ok(Box::new(vec![first].into_iter()))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_num(src: &str) -> Result<Signum, ParseSizeError> {
|
||||
let mut size_string = src.trim();
|
||||
let mut starting_with = false;
|
||||
|
||||
if let Some(c) = size_string.chars().next() {
|
||||
if c == '+' || c == '-' {
|
||||
// tail: '-' is not documented (8.32 man pages)
|
||||
size_string = &size_string[1..];
|
||||
if c == '+' {
|
||||
starting_with = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(ParseSizeError::ParseFailure(src.to_string()));
|
||||
}
|
||||
|
||||
parse_size(size_string).map(|n| match (n, starting_with) {
|
||||
(0, true) => Signum::PlusZero,
|
||||
(0, false) => Signum::MinusZero,
|
||||
(n, true) => Signum::Positive(n),
|
||||
(n, false) => Signum::Negative(n),
|
||||
})
|
||||
}
|
||||
|
||||
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> {
|
||||
let matches = uu_app().try_get_matches_from(arg_iterate(args)?)?;
|
||||
Settings::from(&matches)
|
||||
}
|
||||
|
||||
pub fn uu_app<'a>() -> Command<'a> {
|
||||
#[cfg(target_os = "linux")]
|
||||
pub static POLLING_HELP: &str = "Disable 'inotify' support and use polling instead";
|
||||
#[cfg(all(unix, not(target_os = "linux")))]
|
||||
pub static POLLING_HELP: &str = "Disable 'kqueue' support and use polling instead";
|
||||
#[cfg(target_os = "windows")]
|
||||
pub static POLLING_HELP: &str =
|
||||
"Disable 'ReadDirectoryChanges' support and use polling instead";
|
||||
|
||||
Command::new(uucore::util_name())
|
||||
.version(crate_version!())
|
||||
.about(ABOUT)
|
||||
.override_usage(format_usage(USAGE))
|
||||
.infer_long_args(true)
|
||||
.arg(
|
||||
Arg::new(options::BYTES)
|
||||
.short('c')
|
||||
.long(options::BYTES)
|
||||
.takes_value(true)
|
||||
.allow_hyphen_values(true)
|
||||
.overrides_with_all(&[options::BYTES, options::LINES])
|
||||
.help("Number of bytes to print"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(options::FOLLOW)
|
||||
.short('f')
|
||||
.long(options::FOLLOW)
|
||||
.default_value("descriptor")
|
||||
.takes_value(true)
|
||||
.min_values(0)
|
||||
.max_values(1)
|
||||
.require_equals(true)
|
||||
.value_parser(["descriptor", "name"])
|
||||
.help("Print the file as it grows"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(options::LINES)
|
||||
.short('n')
|
||||
.long(options::LINES)
|
||||
.takes_value(true)
|
||||
.allow_hyphen_values(true)
|
||||
.overrides_with_all(&[options::BYTES, options::LINES])
|
||||
.help("Number of lines to print"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(options::PID)
|
||||
.long(options::PID)
|
||||
.takes_value(true)
|
||||
.help("With -f, terminate after process ID, PID dies"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(options::verbosity::QUIET)
|
||||
.short('q')
|
||||
.long(options::verbosity::QUIET)
|
||||
.visible_alias("silent")
|
||||
.overrides_with_all(&[options::verbosity::QUIET, options::verbosity::VERBOSE])
|
||||
.help("Never output headers giving file names"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(options::SLEEP_INT)
|
||||
.short('s')
|
||||
.takes_value(true)
|
||||
.long(options::SLEEP_INT)
|
||||
.help("Number of seconds to sleep between polling the file when running with -f"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(options::MAX_UNCHANGED_STATS)
|
||||
.takes_value(true)
|
||||
.long(options::MAX_UNCHANGED_STATS)
|
||||
.help(
|
||||
"Reopen a FILE which has not changed size after N (default 5) iterations \
|
||||
to see if it has been unlinked or renamed (this is the usual case of rotated \
|
||||
log files); This option is meaningful only when polling \
|
||||
(i.e., with --use-polling) and when --follow=name",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(options::verbosity::VERBOSE)
|
||||
.short('v')
|
||||
.long(options::verbosity::VERBOSE)
|
||||
.overrides_with_all(&[options::verbosity::QUIET, options::verbosity::VERBOSE])
|
||||
.help("Always output headers giving file names"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(options::ZERO_TERM)
|
||||
.short('z')
|
||||
.long(options::ZERO_TERM)
|
||||
.help("Line delimiter is NUL, not newline"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(options::USE_POLLING)
|
||||
.alias(options::DISABLE_INOTIFY_TERM) // NOTE: Used by GNU's test suite
|
||||
.alias("dis") // NOTE: Used by GNU's test suite
|
||||
.long(options::USE_POLLING)
|
||||
.help(POLLING_HELP),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(options::RETRY)
|
||||
.long(options::RETRY)
|
||||
.help("Keep trying to open a file if it is inaccessible"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(options::FOLLOW_RETRY)
|
||||
.short('F')
|
||||
.help("Same as --follow=name --retry")
|
||||
.overrides_with_all(&[options::RETRY, options::FOLLOW]),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(options::PRESUME_INPUT_PIPE)
|
||||
.long(options::PRESUME_INPUT_PIPE)
|
||||
.alias(options::PRESUME_INPUT_PIPE)
|
||||
.hide(true),
|
||||
)
|
||||
|
||||
.arg(
|
||||
Arg::new(options::ARG_FILES)
|
||||
.multiple_occurrences(true)
|
||||
.takes_value(true)
|
||||
.min_values(1)
|
||||
.value_hint(clap::ValueHint::FilePath),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_num_when_sign_is_given() {
|
||||
let result = parse_num("+0");
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), Signum::PlusZero);
|
||||
|
||||
let result = parse_num("+1");
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), Signum::Positive(1));
|
||||
|
||||
let result = parse_num("-0");
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), Signum::MinusZero);
|
||||
|
||||
let result = parse_num("-1");
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), Signum::Negative(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_num_when_no_sign_is_given() {
|
||||
let result = parse_num("0");
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), Signum::MinusZero);
|
||||
|
||||
let result = parse_num("1");
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), Signum::Negative(1));
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@
|
|||
// spell-checker:ignore (ToDO) filehandle BUFSIZ
|
||||
use std::collections::VecDeque;
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, Read, Seek, SeekFrom, Write};
|
||||
use std::io::{BufRead, Read, Seek, SeekFrom, Write};
|
||||
use uucore::error::UResult;
|
||||
|
||||
/// When reading files in reverse in `bounded_tail`, this is the size of each
|
||||
|
@ -208,7 +208,7 @@ impl BytesChunk {
|
|||
/// that number of bytes. If EOF is reached (so 0 bytes are read), then returns
|
||||
/// [`UResult<None>`] or else the result with [`Some(bytes)`] where bytes is the number of bytes
|
||||
/// read from the source.
|
||||
pub fn fill(&mut self, filehandle: &mut BufReader<impl Read>) -> UResult<Option<usize>> {
|
||||
pub fn fill(&mut self, filehandle: &mut impl BufRead) -> UResult<Option<usize>> {
|
||||
let num_bytes = filehandle.read(&mut self.buffer)?;
|
||||
self.bytes = num_bytes;
|
||||
if num_bytes == 0 {
|
||||
|
@ -283,7 +283,7 @@ impl BytesChunkBuffer {
|
|||
/// let mut chunks = BytesChunkBuffer::new(num_print);
|
||||
/// chunks.fill(&mut reader).unwrap();
|
||||
/// ```
|
||||
pub fn fill(&mut self, reader: &mut BufReader<impl Read>) -> UResult<()> {
|
||||
pub fn fill(&mut self, reader: &mut impl BufRead) -> UResult<()> {
|
||||
let mut chunk = Box::new(BytesChunk::new());
|
||||
|
||||
// fill chunks with all bytes from reader and reuse already instantiated chunks if possible
|
||||
|
@ -323,6 +323,10 @@ impl BytesChunkBuffer {
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn has_data(&self) -> bool {
|
||||
!self.chunks.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Works similar to a [`BytesChunk`] but also stores the number of lines encountered in the current
|
||||
|
@ -452,7 +456,7 @@ impl LinesChunk {
|
|||
/// that number of bytes. This function works like the [`BytesChunk::fill`] function besides
|
||||
/// that this function also counts and stores the number of lines encountered while reading from
|
||||
/// the `filehandle`.
|
||||
pub fn fill(&mut self, filehandle: &mut BufReader<impl Read>) -> UResult<Option<usize>> {
|
||||
pub fn fill(&mut self, filehandle: &mut impl BufRead) -> UResult<Option<usize>> {
|
||||
match self.chunk.fill(filehandle)? {
|
||||
None => {
|
||||
self.lines = 0;
|
||||
|
@ -556,7 +560,7 @@ impl LinesChunkBuffer {
|
|||
/// in sum exactly `self.num_print` lines stored in all chunks. The method returns an iterator
|
||||
/// over these chunks. If there are no chunks, for example because the piped stdin contained no
|
||||
/// lines, or `num_print = 0` then `iterator.next` will return None.
|
||||
pub fn fill(&mut self, reader: &mut BufReader<impl Read>) -> UResult<()> {
|
||||
pub fn fill(&mut self, reader: &mut impl BufRead) -> UResult<()> {
|
||||
let mut chunk = Box::new(LinesChunk::new(self.delimiter));
|
||||
|
||||
while (chunk.fill(reader)?).is_some() {
|
||||
|
|
211
src/uu/tail/src/follow/files.rs
Normal file
211
src/uu/tail/src/follow/files.rs
Normal file
|
@ -0,0 +1,211 @@
|
|||
// * 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.
|
||||
|
||||
// spell-checker:ignore tailable seekable stdlib (stdlib)
|
||||
|
||||
use crate::args::Settings;
|
||||
use crate::chunks::BytesChunkBuffer;
|
||||
use crate::paths::{HeaderPrinter, PathExtTail};
|
||||
use crate::text;
|
||||
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;
|
||||
|
||||
/// Data structure to keep a handle on files to follow.
|
||||
/// `last` always holds the path/key of the last file that was printed from.
|
||||
/// The keys of the HashMap can point to an existing file path (normal case),
|
||||
/// or stdin ("-"), or to a non existing path (--retry).
|
||||
/// For existing files, all keys in the HashMap are absolute Paths.
|
||||
pub struct FileHandling {
|
||||
map: HashMap<PathBuf, PathData>,
|
||||
last: Option<PathBuf>,
|
||||
header_printer: HeaderPrinter,
|
||||
}
|
||||
|
||||
impl FileHandling {
|
||||
pub fn from(settings: &Settings) -> Self {
|
||||
Self {
|
||||
map: HashMap::with_capacity(settings.inputs.len()),
|
||||
last: None,
|
||||
header_printer: HeaderPrinter::new(settings.verbose, false),
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper for HashMap::insert using Path::canonicalize
|
||||
pub fn insert(&mut self, k: &Path, v: PathData, update_last: bool) {
|
||||
let k = Self::canonicalize_path(k);
|
||||
if update_last {
|
||||
self.last = Some(k.to_owned());
|
||||
}
|
||||
let _ = self.map.insert(k, v);
|
||||
}
|
||||
|
||||
/// Wrapper for HashMap::remove using Path::canonicalize
|
||||
pub fn remove(&mut self, k: &Path) -> PathData {
|
||||
self.map.remove(&Self::canonicalize_path(k)).unwrap()
|
||||
}
|
||||
|
||||
/// Wrapper for HashMap::get using Path::canonicalize
|
||||
pub fn get(&self, k: &Path) -> &PathData {
|
||||
self.map.get(&Self::canonicalize_path(k)).unwrap()
|
||||
}
|
||||
|
||||
/// Wrapper for HashMap::get_mut using Path::canonicalize
|
||||
pub fn get_mut(&mut self, k: &Path) -> &mut PathData {
|
||||
self.map.get_mut(&Self::canonicalize_path(k)).unwrap()
|
||||
}
|
||||
|
||||
/// Canonicalize `path` if it is not already an absolute path
|
||||
fn canonicalize_path(path: &Path) -> PathBuf {
|
||||
if path.is_relative() && !path.is_stdin() {
|
||||
if let Ok(p) = path.canonicalize() {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
path.to_owned()
|
||||
}
|
||||
|
||||
pub fn get_mut_metadata(&mut self, path: &Path) -> Option<&Metadata> {
|
||||
self.get_mut(path).metadata.as_ref()
|
||||
}
|
||||
|
||||
pub fn keys(&self) -> Keys<PathBuf, PathData> {
|
||||
self.map.keys()
|
||||
}
|
||||
|
||||
pub fn contains_key(&self, k: &Path) -> bool {
|
||||
self.map.contains_key(k)
|
||||
}
|
||||
|
||||
pub fn get_last(&self) -> Option<&PathBuf> {
|
||||
self.last.as_ref()
|
||||
}
|
||||
|
||||
/// Return true if there is only stdin remaining
|
||||
pub fn only_stdin_remaining(&self) -> bool {
|
||||
self.map.len() == 1 && (self.map.contains_key(Path::new(text::DASH)))
|
||||
}
|
||||
|
||||
/// Return true if there is at least one "tailable" path (or stdin) remaining
|
||||
pub fn files_remaining(&self) -> bool {
|
||||
for path in self.map.keys() {
|
||||
if path.is_tailable() || path.is_stdin() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns true if there are no files remaining
|
||||
pub fn no_files_remaining(&self, settings: &Settings) -> bool {
|
||||
self.map.is_empty() || !self.files_remaining() && !settings.retry
|
||||
}
|
||||
|
||||
/// Set `reader` to None to indicate that `path` is not an existing file anymore.
|
||||
pub fn reset_reader(&mut self, path: &Path) {
|
||||
self.get_mut(path).reader = None;
|
||||
}
|
||||
|
||||
/// Reopen the file at the monitored `path`
|
||||
pub fn update_reader(&mut self, path: &Path) -> UResult<()> {
|
||||
/*
|
||||
BUG: If it's not necessary to reopen a file, GNU's tail calls seek to offset 0.
|
||||
However we can't call seek here because `BufRead` does not implement `Seek`.
|
||||
As a workaround we always reopen the file even though this might not always
|
||||
be necessary.
|
||||
*/
|
||||
self.get_mut(path)
|
||||
.reader
|
||||
.replace(Box::new(BufReader::new(File::open(&path)?)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reload metadata from `path`, or `metadata`
|
||||
pub fn update_metadata(&mut self, path: &Path, metadata: Option<Metadata>) {
|
||||
self.get_mut(path).metadata = if metadata.is_some() {
|
||||
metadata
|
||||
} else {
|
||||
path.metadata().ok()
|
||||
};
|
||||
}
|
||||
|
||||
/// Read new data from `path` and print it to stdout
|
||||
pub fn tail_file(&mut self, path: &Path, verbose: bool) -> UResult<bool> {
|
||||
let mut chunks = BytesChunkBuffer::new(u64::MAX);
|
||||
if let Some(reader) = self.get_mut(path).reader.as_mut() {
|
||||
chunks.fill(reader)?;
|
||||
}
|
||||
if chunks.has_data() {
|
||||
if self.needs_header(path, verbose) {
|
||||
let display_name = self.get(path).display_name.clone();
|
||||
self.header_printer.print(display_name.as_str());
|
||||
}
|
||||
|
||||
let stdout = stdout();
|
||||
let writer = BufWriter::new(stdout.lock());
|
||||
chunks.print(writer)?;
|
||||
|
||||
self.last.replace(path.to_owned());
|
||||
self.update_metadata(path, None);
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Decide if printing `path` needs a header based on when it was last printed
|
||||
pub fn needs_header(&self, path: &Path, verbose: bool) -> bool {
|
||||
if verbose {
|
||||
if let Some(ref last) = self.last {
|
||||
return !last.eq(&path);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Data structure to keep a handle on the BufReader, Metadata
|
||||
/// and the display_name (header_name) of files that are being followed.
|
||||
pub struct PathData {
|
||||
pub reader: Option<Box<dyn BufRead>>,
|
||||
pub metadata: Option<Metadata>,
|
||||
pub display_name: String,
|
||||
}
|
||||
|
||||
impl PathData {
|
||||
pub fn new(
|
||||
reader: Option<Box<dyn BufRead>>,
|
||||
metadata: Option<Metadata>,
|
||||
display_name: &str,
|
||||
) -> Self {
|
||||
Self {
|
||||
reader,
|
||||
metadata,
|
||||
display_name: display_name.to_owned(),
|
||||
}
|
||||
}
|
||||
pub fn from_other_with_path(data: Self, path: &Path) -> Self {
|
||||
// Remove old reader
|
||||
let old_reader = data.reader;
|
||||
let reader = if old_reader.is_some() {
|
||||
// Use old reader with the same file descriptor if there is one
|
||||
old_reader
|
||||
} else if let Ok(file) = File::open(path) {
|
||||
// Open new file tail from start
|
||||
Some(Box::new(BufReader::new(file)) as Box<dyn BufRead>)
|
||||
} else {
|
||||
// Probably file was renamed/moved or removed again
|
||||
None
|
||||
};
|
||||
|
||||
Self::new(reader, path.metadata().ok(), data.display_name.as_str())
|
||||
}
|
||||
}
|
9
src/uu/tail/src/follow/mod.rs
Normal file
9
src/uu/tail/src/follow/mod.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
// * 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.
|
||||
|
||||
mod files;
|
||||
mod watch;
|
||||
|
||||
pub use watch::{follow, WatcherService};
|
595
src/uu/tail/src/follow/watch.rs
Normal file
595
src/uu/tail/src/follow/watch.rs
Normal file
|
@ -0,0 +1,595 @@
|
|||
// * 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.
|
||||
|
||||
// spell-checker:ignore (ToDO) tailable untailable stdlib kqueue Uncategorized unwatch
|
||||
|
||||
use crate::args::{FollowMode, Settings};
|
||||
use crate::follow::files::{FileHandling, PathData};
|
||||
use crate::paths::{Input, InputKind, MetadataExtTail, PathExtTail};
|
||||
use crate::{platform, text};
|
||||
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 uucore::display::Quotable;
|
||||
use uucore::error::{set_exit_code, UResult, USimpleError};
|
||||
|
||||
pub struct WatcherRx {
|
||||
watcher: Box<dyn Watcher>,
|
||||
receiver: Receiver<Result<notify::Event, notify::Error>>,
|
||||
}
|
||||
|
||||
impl WatcherRx {
|
||||
fn new(
|
||||
watcher: Box<dyn Watcher>,
|
||||
receiver: Receiver<Result<notify::Event, notify::Error>>,
|
||||
) -> Self {
|
||||
Self { watcher, receiver }
|
||||
}
|
||||
|
||||
/// Wrapper for `notify::Watcher::watch` to also add the parent directory of `path` if necessary.
|
||||
fn watch_with_parent(&mut self, path: &Path) -> UResult<()> {
|
||||
let mut path = path.to_owned();
|
||||
#[cfg(target_os = "linux")]
|
||||
if path.is_file() {
|
||||
/*
|
||||
NOTE: Using the parent directory instead of the file is a workaround.
|
||||
This workaround follows the recommendation of the notify crate authors:
|
||||
> On some platforms, if the `path` is renamed or removed while being watched, behavior may
|
||||
> be unexpected. See discussions in [#165] and [#166]. If less surprising behavior is wanted
|
||||
> one may non-recursively watch the _parent_ directory as well and manage related events.
|
||||
NOTE: Adding both: file and parent results in duplicate/wrong events.
|
||||
Tested for notify::InotifyWatcher and for notify::PollWatcher.
|
||||
*/
|
||||
if let Some(parent) = path.parent() {
|
||||
if parent.is_dir() {
|
||||
path = parent.to_owned();
|
||||
} else {
|
||||
path = PathBuf::from(".");
|
||||
}
|
||||
} else {
|
||||
return Err(USimpleError::new(
|
||||
1,
|
||||
format!("cannot watch parent directory of {}", path.display()),
|
||||
));
|
||||
};
|
||||
}
|
||||
if path.is_relative() {
|
||||
path = path.canonicalize()?;
|
||||
}
|
||||
|
||||
// for syscalls: 2x "inotify_add_watch" ("filename" and ".") and 1x "inotify_rm_watch"
|
||||
self.watch(&path, RecursiveMode::NonRecursive)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn watch(&mut self, path: &Path, mode: RecursiveMode) -> UResult<()> {
|
||||
self.watcher
|
||||
.watch(path, mode)
|
||||
.map_err(|err| USimpleError::new(1, err.to_string()))
|
||||
}
|
||||
|
||||
fn unwatch(&mut self, path: &Path) -> UResult<()> {
|
||||
self.watcher
|
||||
.unwatch(path)
|
||||
.map_err(|err| USimpleError::new(1, err.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WatcherService {
|
||||
/// Whether --retry was given on the command line
|
||||
pub retry: bool,
|
||||
|
||||
/// The [`FollowMode`]
|
||||
pub follow: Option<FollowMode>,
|
||||
|
||||
/// Indicates whether to use the fallback `polling` method instead of the
|
||||
/// 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<WatcherRx>,
|
||||
pub orphans: Vec<PathBuf>,
|
||||
pub files: FileHandling,
|
||||
}
|
||||
|
||||
impl WatcherService {
|
||||
pub fn new(
|
||||
retry: bool,
|
||||
follow: Option<FollowMode>,
|
||||
use_polling: bool,
|
||||
files: FileHandling,
|
||||
) -> Self {
|
||||
Self {
|
||||
retry,
|
||||
follow,
|
||||
use_polling,
|
||||
watcher_rx: None,
|
||||
orphans: Vec::new(),
|
||||
files,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from(settings: &Settings) -> Self {
|
||||
Self::new(
|
||||
settings.retry,
|
||||
settings.follow,
|
||||
settings.use_polling,
|
||||
FileHandling::from(settings),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn add_path(
|
||||
&mut self,
|
||||
path: &Path,
|
||||
display_name: &str,
|
||||
reader: Option<Box<dyn BufRead>>,
|
||||
update_last: bool,
|
||||
) -> UResult<()> {
|
||||
if self.follow.is_some() {
|
||||
let path = if path.is_relative() {
|
||||
std::env::current_dir()?.join(path)
|
||||
} else {
|
||||
path.to_owned()
|
||||
};
|
||||
let metadata = path.metadata().ok();
|
||||
self.files.insert(
|
||||
&path,
|
||||
PathData::new(reader, metadata, display_name),
|
||||
update_last,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_stdin(
|
||||
&mut self,
|
||||
display_name: &str,
|
||||
reader: Option<Box<dyn BufRead>>,
|
||||
update_last: bool,
|
||||
) -> UResult<()> {
|
||||
if self.follow == Some(FollowMode::Descriptor) {
|
||||
return self.add_path(
|
||||
&PathBuf::from(text::DEV_STDIN),
|
||||
display_name,
|
||||
reader,
|
||||
update_last,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_bad_path(
|
||||
&mut self,
|
||||
path: &Path,
|
||||
display_name: &str,
|
||||
update_last: bool,
|
||||
) -> UResult<()> {
|
||||
if self.retry && self.follow.is_some() {
|
||||
return self.add_path(path, display_name, None, update_last);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn start(&mut self, settings: &Settings) -> UResult<()> {
|
||||
if settings.follow.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (tx, rx) = channel();
|
||||
|
||||
/*
|
||||
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 / kqueue
|
||||
Windows: ReadDirectoryChangesWatcher
|
||||
FreeBSD / NetBSD / OpenBSD / DragonflyBSD: kqueue
|
||||
Fallback: polling every n seconds
|
||||
|
||||
NOTE:
|
||||
We force the use of kqueue with: features=["macos_kqueue"].
|
||||
On macOS only `kqueue` is suitable for our use case because `FSEvents`
|
||||
waits for file close util it delivers a modify event. See:
|
||||
https://github.com/notify-rs/notify/issues/240
|
||||
*/
|
||||
|
||||
let watcher: Box<dyn Watcher>;
|
||||
let watcher_config = notify::Config::default()
|
||||
.with_poll_interval(settings.sleep_sec)
|
||||
/*
|
||||
NOTE: By enabling compare_contents, performance will be significantly impacted
|
||||
as all files will need to be read and hashed at each `poll_interval`.
|
||||
However, this is necessary to pass: "gnu/tests/tail-2/F-vs-rename.sh"
|
||||
*/
|
||||
.with_compare_contents(true);
|
||||
if self.use_polling || RecommendedWatcher::kind() == WatcherKind::PollWatcher {
|
||||
self.use_polling = true; // We have to use polling because there's no supported backend
|
||||
watcher = Box::new(notify::PollWatcher::new(tx, watcher_config).unwrap());
|
||||
} else {
|
||||
let tx_clone = tx.clone();
|
||||
match notify::RecommendedWatcher::new(tx, notify::Config::default()) {
|
||||
Ok(w) => watcher = Box::new(w),
|
||||
Err(e) if e.to_string().starts_with("Too many open files") => {
|
||||
/*
|
||||
NOTE: This ErrorKind is `Uncategorized`, but it is not recommended
|
||||
to match an error against `Uncategorized`
|
||||
NOTE: Could be tested with decreasing `max_user_instances`, e.g.:
|
||||
`sudo sysctl fs.inotify.max_user_instances=64`
|
||||
*/
|
||||
show_error!(
|
||||
"{} cannot be used, reverting to polling: Too many open files",
|
||||
text::BACKEND
|
||||
);
|
||||
set_exit_code(1);
|
||||
self.use_polling = true;
|
||||
watcher = Box::new(notify::PollWatcher::new(tx_clone, watcher_config).unwrap());
|
||||
}
|
||||
Err(e) => return Err(USimpleError::new(1, e.to_string())),
|
||||
};
|
||||
}
|
||||
|
||||
self.watcher_rx = Some(WatcherRx::new(watcher, rx));
|
||||
self.init_files(&settings.inputs)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn follow_descriptor(&self) -> bool {
|
||||
self.follow == Some(FollowMode::Descriptor)
|
||||
}
|
||||
|
||||
pub fn follow_name(&self) -> bool {
|
||||
self.follow == Some(FollowMode::Name)
|
||||
}
|
||||
|
||||
pub fn follow_descriptor_retry(&self) -> bool {
|
||||
self.follow_descriptor() && self.retry
|
||||
}
|
||||
|
||||
pub fn follow_name_retry(&self) -> bool {
|
||||
self.follow_name() && self.retry
|
||||
}
|
||||
|
||||
fn init_files(&mut self, inputs: &VecDeque<Input>) -> UResult<()> {
|
||||
if let Some(watcher_rx) = &mut self.watcher_rx {
|
||||
for input in inputs {
|
||||
match input.kind() {
|
||||
InputKind::Stdin => continue,
|
||||
InputKind::File(path) => {
|
||||
#[cfg(all(unix, not(target_os = "linux")))]
|
||||
if !path.is_file() {
|
||||
continue;
|
||||
}
|
||||
let mut path = path.to_owned();
|
||||
if path.is_relative() {
|
||||
path = std::env::current_dir()?.join(path);
|
||||
}
|
||||
|
||||
if path.is_tailable() {
|
||||
// Add existing regular files to `Watcher` (InotifyWatcher).
|
||||
watcher_rx.watch_with_parent(&path)?;
|
||||
} else if !path.is_orphan() {
|
||||
// If `path` is not a tailable file, add its parent to `Watcher`.
|
||||
watcher_rx
|
||||
.watch(path.parent().unwrap(), RecursiveMode::NonRecursive)?;
|
||||
} else {
|
||||
// If there is no parent, add `path` to `orphans`.
|
||||
self.orphans.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_event(
|
||||
&mut self,
|
||||
event: ¬ify::Event,
|
||||
settings: &Settings,
|
||||
) -> UResult<Vec<PathBuf>> {
|
||||
use notify::event::*;
|
||||
|
||||
let event_path = event.paths.first().unwrap();
|
||||
let mut paths: Vec<PathBuf> = vec![];
|
||||
let display_name = self.files.get(event_path).display_name.clone();
|
||||
|
||||
match event.kind {
|
||||
EventKind::Modify(ModifyKind::Metadata(MetadataKind::Any | MetadataKind::WriteTime))
|
||||
|
||||
// | EventKind::Access(AccessKind::Close(AccessMode::Write))
|
||||
| EventKind::Create(CreateKind::File | CreateKind::Folder | CreateKind::Any)
|
||||
| EventKind::Modify(ModifyKind::Data(DataChange::Any))
|
||||
| EventKind::Modify(ModifyKind::Name(RenameMode::To)) => {
|
||||
if let Ok(new_md) = event_path.metadata() {
|
||||
|
||||
let is_tailable = new_md.is_tailable();
|
||||
let pd = self.files.get(event_path);
|
||||
if let Some(old_md) = &pd.metadata {
|
||||
if is_tailable {
|
||||
// We resume tracking from the start of the file,
|
||||
// assuming it has been truncated to 0. This mimics GNU's `tail`
|
||||
// behavior and is the usual truncation operation for log self.files.
|
||||
if !old_md.is_tailable() {
|
||||
show_error!( "{} has become accessible", display_name.quote());
|
||||
self.files.update_reader(event_path)?;
|
||||
} else if pd.reader.is_none() {
|
||||
show_error!( "{} has appeared; following new file", display_name.quote());
|
||||
self.files.update_reader(event_path)?;
|
||||
} else if event.kind == EventKind::Modify(ModifyKind::Name(RenameMode::To))
|
||||
|| (self.use_polling
|
||||
&& !old_md.file_id_eq(&new_md)) {
|
||||
show_error!( "{} has been replaced; following new file", display_name.quote());
|
||||
self.files.update_reader(event_path)?;
|
||||
} else if old_md.got_truncated(&new_md)? {
|
||||
show_error!("{}: file truncated", display_name);
|
||||
self.files.update_reader(event_path)?;
|
||||
}
|
||||
paths.push(event_path.to_owned());
|
||||
} else if !is_tailable && old_md.is_tailable() {
|
||||
if pd.reader.is_some() {
|
||||
self.files.reset_reader(event_path);
|
||||
} else {
|
||||
show_error!(
|
||||
"{} has been replaced with an untailable file",
|
||||
display_name.quote()
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if is_tailable {
|
||||
show_error!( "{} has appeared; following new file", display_name.quote());
|
||||
self.files.update_reader(event_path)?;
|
||||
paths.push(event_path.to_owned());
|
||||
} else if settings.retry {
|
||||
if self.follow_descriptor() {
|
||||
show_error!(
|
||||
"{} has been replaced with an untailable file; giving up on this name",
|
||||
display_name.quote()
|
||||
);
|
||||
let _ = self.watcher_rx.as_mut().unwrap().watcher.unwatch(event_path);
|
||||
self.files.remove(event_path);
|
||||
if self.files.no_files_remaining(settings) {
|
||||
return Err(USimpleError::new(1, text::NO_FILES_REMAINING));
|
||||
}
|
||||
} else {
|
||||
show_error!(
|
||||
"{} has been replaced with an untailable file",
|
||||
display_name.quote()
|
||||
);
|
||||
}
|
||||
}
|
||||
self.files.update_metadata(event_path, Some(new_md));
|
||||
}
|
||||
}
|
||||
EventKind::Remove(RemoveKind::File | RemoveKind::Any)
|
||||
|
||||
// | EventKind::Modify(ModifyKind::Name(RenameMode::Any))
|
||||
| EventKind::Modify(ModifyKind::Name(RenameMode::From)) => {
|
||||
if self.follow_name() {
|
||||
if settings.retry {
|
||||
if let Some(old_md) = self.files.get_mut_metadata(event_path) {
|
||||
if old_md.is_tailable() && self.files.get(event_path).reader.is_some() {
|
||||
show_error!(
|
||||
"{} {}: {}",
|
||||
display_name.quote(),
|
||||
text::BECOME_INACCESSIBLE,
|
||||
text::NO_SUCH_FILE
|
||||
);
|
||||
}
|
||||
}
|
||||
if event_path.is_orphan() && !self.orphans.contains(event_path) {
|
||||
show_error!("directory containing watched file was removed");
|
||||
show_error!(
|
||||
"{} cannot be used, reverting to polling",
|
||||
text::BACKEND
|
||||
);
|
||||
self.orphans.push(event_path.to_owned());
|
||||
let _ = self.watcher_rx.as_mut().unwrap().unwatch(event_path);
|
||||
}
|
||||
} else {
|
||||
show_error!("{}: {}", display_name, text::NO_SUCH_FILE);
|
||||
if !self.files.files_remaining() && self.use_polling {
|
||||
// NOTE: GNU's tail exits here for `---disable-inotify`
|
||||
return Err(USimpleError::new(1, text::NO_FILES_REMAINING));
|
||||
}
|
||||
}
|
||||
self.files.reset_reader(event_path);
|
||||
} else if self.follow_descriptor_retry() {
|
||||
// --retry only effective for the initial open
|
||||
let _ = self.watcher_rx.as_mut().unwrap().unwatch(event_path);
|
||||
self.files.remove(event_path);
|
||||
} else if self.use_polling && event.kind == EventKind::Remove(RemoveKind::Any) {
|
||||
/*
|
||||
BUG: The watched file was removed. Since we're using Polling, this
|
||||
could be a rename. We can't tell because `notify::PollWatcher` doesn't
|
||||
recognize renames properly.
|
||||
Ideally we want to call seek to offset 0 on the file handle.
|
||||
But because we only have access to `PathData::reader` as `BufRead`,
|
||||
we cannot seek to 0 with `BufReader::seek_relative`.
|
||||
Also because we don't have the new name, we cannot work around this
|
||||
by simply reopening the file.
|
||||
*/
|
||||
}
|
||||
}
|
||||
EventKind::Modify(ModifyKind::Name(RenameMode::Both)) => {
|
||||
/*
|
||||
NOTE: For `tail -f a`, keep tracking additions to b after `mv a b`
|
||||
(gnu/tests/tail-2/descriptor-vs-rename.sh)
|
||||
NOTE: The File/BufReader doesn't need to be updated.
|
||||
However, we need to update our `files.map`.
|
||||
This can only be done for inotify, because this EventKind does not
|
||||
trigger for the PollWatcher.
|
||||
BUG: As a result, there's a bug if polling is used:
|
||||
$ tail -f file_a ---disable-inotify
|
||||
$ mv file_a file_b
|
||||
$ echo A >> file_b
|
||||
$ echo A >> file_a
|
||||
The last append to file_a is printed, however this shouldn't be because
|
||||
after the "mv" tail should only follow "file_b".
|
||||
TODO: [2022-05; jhscheer] add test for this bug
|
||||
*/
|
||||
|
||||
if self.follow_descriptor() {
|
||||
let new_path = event.paths.last().unwrap();
|
||||
paths.push(new_path.to_owned());
|
||||
|
||||
let new_data = PathData::from_other_with_path(self.files.remove(event_path), new_path);
|
||||
self.files.insert(
|
||||
new_path,
|
||||
new_data,
|
||||
self.files.get_last().unwrap() == event_path
|
||||
);
|
||||
|
||||
// Unwatch old path and watch new path
|
||||
let _ = self.watcher_rx.as_mut().unwrap().unwatch(event_path);
|
||||
self.watcher_rx.as_mut().unwrap().watch_with_parent(new_path)?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(paths)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResult<()> {
|
||||
if watcher_service.files.no_files_remaining(settings)
|
||||
&& !watcher_service.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 _event_counter = 0;
|
||||
let mut _timeout_counter = 0;
|
||||
|
||||
// main follow loop
|
||||
loop {
|
||||
let mut _read_some = false;
|
||||
|
||||
// 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() {
|
||||
// p is dead, tail will also terminate
|
||||
break;
|
||||
}
|
||||
|
||||
// For `-F` we need to poll if an orphan path becomes available during runtime.
|
||||
// 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 new_path.exists() {
|
||||
let pd = watcher_service.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
|
||||
.watcher_rx
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.watch_with_parent(new_path)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
.watcher_rx
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.receiver
|
||||
.recv_timeout(settings.sleep_sec);
|
||||
if rx_result.is_ok() {
|
||||
_event_counter += 1;
|
||||
_timeout_counter = 0;
|
||||
}
|
||||
|
||||
let mut paths = vec![]; // Paths worth checking for new content to print
|
||||
match rx_result {
|
||||
Ok(Ok(event)) => {
|
||||
if let Some(event_path) = event.paths.first() {
|
||||
if watcher_service.files.contains_key(event_path) {
|
||||
// Handle Event if it is about a path that we are monitoring
|
||||
paths = watcher_service.handle_event(&event, settings)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Err(notify::Error {
|
||||
kind: notify::ErrorKind::Io(ref e),
|
||||
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
|
||||
.watcher_rx
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.watcher
|
||||
.unwatch(event_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Err(notify::Error {
|
||||
kind: notify::ErrorKind::MaxFilesWatch,
|
||||
..
|
||||
})) => {
|
||||
return Err(USimpleError::new(
|
||||
1,
|
||||
format!("{} resources exhausted", text::BACKEND),
|
||||
))
|
||||
}
|
||||
Ok(Err(e)) => return Err(USimpleError::new(1, format!("NotifyError: {}", e))),
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => {
|
||||
_timeout_counter += 1;
|
||||
}
|
||||
Err(e) => return Err(USimpleError::new(1, format!("RecvTimeoutError: {}", e))),
|
||||
}
|
||||
|
||||
if watcher_service.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::<Vec<_>>();
|
||||
}
|
||||
|
||||
// main print loop
|
||||
for path in &paths {
|
||||
_read_some = watcher_service.files.tail_file(path, settings.verbose)?;
|
||||
}
|
||||
|
||||
if _timeout_counter == settings.max_unchanged_stats {
|
||||
/*
|
||||
TODO: [2021-10; jhscheer] implement timeout_counter for each file.
|
||||
‘--max-unchanged-stats=n’
|
||||
When tailing a file by name, if there have been n (default n=5) consecutive iterations
|
||||
for which the file has not changed, then open/fstat the file to determine if that file
|
||||
name is still associated with the same device/inode-number pair as before. When
|
||||
following a log file that is rotated, this is approximately the number of seconds
|
||||
between when tail prints the last pre-rotation lines and when it prints the lines that
|
||||
have accumulated in the new log file. This option is meaningful only when polling
|
||||
(i.e., without inotify) and when following by name.
|
||||
*/
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
279
src/uu/tail/src/paths.rs
Normal file
279
src/uu/tail/src/paths.rs
Normal file
|
@ -0,0 +1,279 @@
|
|||
// * 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.
|
||||
|
||||
// spell-checker:ignore tailable seekable stdlib (stdlib)
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::{FileTypeExt, MetadataExt};
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::fs::{File, Metadata};
|
||||
use std::io::{Seek, SeekFrom};
|
||||
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),
|
||||
Stdin,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Input {
|
||||
kind: InputKind,
|
||||
pub display_name: String,
|
||||
}
|
||||
|
||||
impl Input {
|
||||
pub fn from(string: String) -> Self {
|
||||
let kind = if string == text::DASH {
|
||||
InputKind::Stdin
|
||||
} else {
|
||||
InputKind::File(PathBuf::from(&string))
|
||||
};
|
||||
|
||||
let display_name = match kind {
|
||||
InputKind::File(_) => string,
|
||||
InputKind::Stdin => {
|
||||
if cfg!(unix) {
|
||||
text::STDIN_HEADER.to_string()
|
||||
} else {
|
||||
string
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Self { kind, display_name }
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> &InputKind {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
pub fn is_stdin(&self) -> bool {
|
||||
match self.kind {
|
||||
InputKind::File(_) => false,
|
||||
InputKind::Stdin => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve(&self) -> Option<PathBuf> {
|
||||
match &self.kind {
|
||||
InputKind::File(path) if path != &PathBuf::from(text::DEV_STDIN) => {
|
||||
path.canonicalize().ok()
|
||||
}
|
||||
InputKind::File(_) | InputKind::Stdin => {
|
||||
if cfg!(unix) {
|
||||
PathBuf::from(text::DEV_STDIN).canonicalize().ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_tailable(&self) -> bool {
|
||||
match &self.kind {
|
||||
InputKind::File(path) => path_is_tailable(path),
|
||||
InputKind::Stdin => self.resolve().map_or(false, |path| path_is_tailable(&path)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Input {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
kind: InputKind::Stdin,
|
||||
display_name: String::from(text::STDIN_HEADER),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct HeaderPrinter {
|
||||
verbose: bool,
|
||||
first_header: bool,
|
||||
}
|
||||
|
||||
impl HeaderPrinter {
|
||||
pub fn new(verbose: bool, first_header: bool) -> Self {
|
||||
Self {
|
||||
verbose,
|
||||
first_header,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_input(&mut self, input: &Input) {
|
||||
self.print(input.display_name.as_str());
|
||||
}
|
||||
|
||||
pub fn print(&mut self, string: &str) {
|
||||
if self.verbose {
|
||||
println!(
|
||||
"{}==> {} <==",
|
||||
if self.first_header { "" } else { "\n" },
|
||||
string,
|
||||
);
|
||||
self.first_header = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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 {
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
fn is_seekable(&mut self, current_offset: u64) -> bool;
|
||||
}
|
||||
|
||||
impl FileExtTail for File {
|
||||
/// Test if File is seekable.
|
||||
/// Set the current position offset to `current_offset`.
|
||||
fn is_seekable(&mut self, current_offset: u64) -> bool {
|
||||
self.seek(SeekFrom::Current(0)).is_ok()
|
||||
&& self.seek(SeekFrom::End(0)).is_ok()
|
||||
&& self.seek(SeekFrom::Start(current_offset)).is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MetadataExtTail {
|
||||
fn is_tailable(&self) -> bool;
|
||||
fn got_truncated(&self, other: &Metadata) -> UResult<bool>;
|
||||
fn get_block_size(&self) -> u64;
|
||||
fn file_id_eq(&self, other: &Metadata) -> bool;
|
||||
}
|
||||
|
||||
impl MetadataExtTail for Metadata {
|
||||
fn is_tailable(&self) -> bool {
|
||||
let ft = self.file_type();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
ft.is_file() || ft.is_char_device() || ft.is_fifo()
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
ft.is_file()
|
||||
}
|
||||
}
|
||||
|
||||
/// Return true if the file was modified and is now shorter
|
||||
fn got_truncated(&self, other: &Metadata) -> UResult<bool> {
|
||||
Ok(other.len() < self.len() && other.modified()? != self.modified()?)
|
||||
}
|
||||
|
||||
fn get_block_size(&self) -> u64 {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
self.blocks()
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
self.len()
|
||||
}
|
||||
}
|
||||
|
||||
fn file_id_eq(&self, _other: &Metadata) -> bool {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
self.ino().eq(&_other.ino())
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// use std::os::windows::prelude::*;
|
||||
// if let Some(self_id) = self.file_index() {
|
||||
// if let Some(other_id) = other.file_index() {
|
||||
//
|
||||
// return self_id.eq(&other_id);
|
||||
// }
|
||||
// }
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PathExtTail {
|
||||
fn is_stdin(&self) -> bool;
|
||||
fn is_orphan(&self) -> bool;
|
||||
fn is_tailable(&self) -> bool;
|
||||
}
|
||||
|
||||
impl PathExtTail for Path {
|
||||
fn is_stdin(&self) -> bool {
|
||||
self.eq(Self::new(text::DASH))
|
||||
|| self.eq(Self::new(text::DEV_STDIN))
|
||||
|| self.eq(Self::new(text::STDIN_HEADER))
|
||||
}
|
||||
|
||||
/// Return true if `path` does not have an existing parent directory
|
||||
fn is_orphan(&self) -> bool {
|
||||
!matches!(self.parent(), Some(parent) if parent.is_dir())
|
||||
}
|
||||
|
||||
/// Return true if `path` is is a file type that can be tailed
|
||||
fn is_tailable(&self) -> bool {
|
||||
path_is_tailable(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path_is_tailable(path: &Path) -> bool {
|
||||
path.is_file() || path.exists() && path.metadata().map_or(false, |meta| meta.is_tailable())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn stdin_is_bad_fd() -> bool {
|
||||
// FIXME : Rust's stdlib is reopening fds as /dev/null
|
||||
// see also: https://github.com/uutils/coreutils/issues/2873
|
||||
// (gnu/tests/tail-2/follow-stdin.sh fails because of this)
|
||||
//#[cfg(unix)]
|
||||
{
|
||||
//platform::stdin_is_bad_fd()
|
||||
}
|
||||
//#[cfg(not(unix))]
|
||||
false
|
||||
}
|
File diff suppressed because it is too large
Load diff
20
src/uu/tail/src/text.rs
Normal file
20
src/uu/tail/src/text.rs
Normal file
|
@ -0,0 +1,20 @@
|
|||
// * 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.
|
||||
|
||||
// spell-checker:ignore (ToDO) kqueue
|
||||
|
||||
pub static DASH: &str = "-";
|
||||
pub static DEV_STDIN: &str = "/dev/stdin";
|
||||
pub static STDIN_HEADER: &str = "standard input";
|
||||
pub static NO_FILES_REMAINING: &str = "no files remaining";
|
||||
pub static NO_SUCH_FILE: &str = "No such file or directory";
|
||||
pub static BECOME_INACCESSIBLE: &str = "has become inaccessible";
|
||||
pub static BAD_FD: &str = "Bad file descriptor";
|
||||
#[cfg(target_os = "linux")]
|
||||
pub static BACKEND: &str = "inotify";
|
||||
#[cfg(all(unix, not(target_os = "linux")))]
|
||||
pub static BACKEND: &str = "kqueue";
|
||||
#[cfg(target_os = "windows")]
|
||||
pub static BACKEND: &str = "ReadDirectoryChanges";
|
|
@ -7,6 +7,8 @@
|
|||
// spell-checker:ignore (libs) kqueue
|
||||
// spell-checker:ignore (jargon) tailable untailable
|
||||
|
||||
// TODO: add tests for presume_input_pipe
|
||||
|
||||
extern crate tail;
|
||||
|
||||
use crate::common::util::*;
|
||||
|
@ -264,6 +266,7 @@ fn test_follow_redirect_stdin_name_retry() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(target_os = "macos"))] // See test_stdin_redirect_dir_when_target_os_is_macos
|
||||
#[cfg(all(unix, not(any(target_os = "android", target_os = "freebsd"))))] // FIXME: fix this test for Android/FreeBSD
|
||||
fn test_stdin_redirect_dir() {
|
||||
// $ mkdir dir
|
||||
|
@ -289,6 +292,39 @@ fn test_stdin_redirect_dir() {
|
|||
.code_is(1);
|
||||
}
|
||||
|
||||
// On macOS path.is_dir() can be false for directories if it was a redirect,
|
||||
// e.g. `$ tail < DIR. The library feature to detect the
|
||||
// std::io::ErrorKind::IsADirectory isn't stable so we currently show the a wrong
|
||||
// error message.
|
||||
// FIXME: If `std::io::ErrorKind::IsADirectory` becomes stable or macos handles
|
||||
// redirected directories like linux show the correct message like in
|
||||
// `test_stdin_redirect_dir`
|
||||
#[test]
|
||||
#[cfg(target_os = "macos")]
|
||||
fn test_stdin_redirect_dir_when_target_os_is_macos() {
|
||||
// $ mkdir dir
|
||||
// $ tail < dir, $ tail - < dir
|
||||
// tail: error reading 'standard input': Is a directory
|
||||
|
||||
let ts = TestScenario::new(util_name!());
|
||||
let at = &ts.fixtures;
|
||||
at.mkdir("dir");
|
||||
|
||||
ts.ucmd()
|
||||
.set_stdin(std::fs::File::open(at.plus("dir")).unwrap())
|
||||
.fails()
|
||||
.no_stdout()
|
||||
.stderr_is("tail: cannot open 'standard input' for reading: No such file or directory")
|
||||
.code_is(1);
|
||||
ts.ucmd()
|
||||
.set_stdin(std::fs::File::open(at.plus("dir")).unwrap())
|
||||
.arg("-")
|
||||
.fails()
|
||||
.no_stdout()
|
||||
.stderr_is("tail: cannot open 'standard input' for reading: No such file or directory")
|
||||
.code_is(1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "linux")]
|
||||
fn test_follow_stdin_descriptor() {
|
||||
|
|
Loading…
Reference in a new issue