Merge pull request #3905 from Joining7943/refactor-tail

Refactor tail
This commit is contained in:
Terts Diepraam 2022-09-17 20:39:53 +02:00 committed by GitHub
commit 8c839888e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 1848 additions and 1448 deletions

473
src/uu/tail/src/args.rs Normal file
View 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));
}
}

View file

@ -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() {

View 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())
}
}

View 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};

View 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: &notify::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
View 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
View 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";

View file

@ -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() {