diff --git a/Cargo.lock b/Cargo.lock index f2f4e9897..49fd7d4fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2985,6 +2985,7 @@ dependencies = [ name = "uu_tail" version = "0.0.16" dependencies = [ + "atty", "clap", "libc", "memchr", diff --git a/src/uu/tail/Cargo.toml b/src/uu/tail/Cargo.toml index 4c2552d0c..3f7b00436 100644 --- a/src/uu/tail/Cargo.toml +++ b/src/uu/tail/Cargo.toml @@ -22,6 +22,7 @@ memchr = "2.5.0" notify = { version = "=5.0.0", features=["macos_kqueue"]} uucore = { version=">=0.0.16", package="uucore", path="../../uucore", features=["ringbuffer", "lines"] } same-file = "1.0.6" +atty = "0.2" [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.42.0", default-features = false, features = ["Win32_System_Threading", "Win32_Foundation"] } diff --git a/src/uu/tail/src/args.rs b/src/uu/tail/src/args.rs index f2ef918f3..6e972d25f 100644 --- a/src/uu/tail/src/args.rs +++ b/src/uu/tail/src/args.rs @@ -7,8 +7,10 @@ use crate::paths::Input; use crate::{parse, platform, Quotable}; +use atty::Stream; use clap::crate_version; use clap::{parser::ValueSource, Arg, ArgAction, ArgMatches, Command}; +use same_file::Handle; use std::collections::VecDeque; use std::ffi::OsString; use std::time::Duration; @@ -113,6 +115,13 @@ pub enum FollowMode { Name, } +#[derive(Debug)] +pub enum VerificationResult { + Ok, + CannotFollowStdinByName, + NoOutput, +} + #[derive(Debug, Default)] pub struct Settings { pub follow: Option, @@ -149,10 +158,6 @@ impl Settings { settings.retry = matches.get_flag(options::RETRY) || matches.get_flag(options::FOLLOW_RETRY); - if settings.retry && settings.follow.is_none() { - show_warning!("--retry ignored; --retry is useful only when following"); - } - if let Some(s) = matches.get_one::(options::SLEEP_INT) { settings.sleep_sec = match s.parse::() { Ok(s) => Duration::from_secs_f32(s), @@ -194,14 +199,8 @@ impl Settings { 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( @@ -214,16 +213,6 @@ impl Settings { settings.mode = FilterMode::from(matches)?; - // Mimic GNU's tail for -[nc]0 without -f and exit immediately - if settings.follow.is_none() - && matches!( - settings.mode, - FilterMode::Lines(Signum::MinusZero, _) | FilterMode::Bytes(Signum::MinusZero) - ) - { - std::process::exit(0) - } - let mut inputs: VecDeque = matches .get_many::(options::ARG_FILES) .map(|v| v.map(|string| Input::from(string.clone())).collect()) @@ -243,6 +232,81 @@ impl Settings { Ok(settings) } + + pub fn has_only_stdin(&self) -> bool { + self.inputs.iter().all(|input| input.is_stdin()) + } + + pub fn has_stdin(&self) -> bool { + self.inputs.iter().any(|input| input.is_stdin()) + } + + pub fn num_inputs(&self) -> usize { + self.inputs.len() + } + + /// Check [`Settings`] for problematic configurations of tail originating from user provided + /// command line arguments and print appropriate warnings. + pub fn check_warnings(&self) { + if self.retry { + if self.follow.is_none() { + show_warning!("--retry ignored; --retry is useful only when following"); + } else if self.follow == Some(FollowMode::Descriptor) { + show_warning!("--retry only effective for the initial open"); + } + } + + if self.pid != 0 { + if self.follow.is_none() { + show_warning!("PID ignored; --pid=PID is useful only when following"); + } else if !platform::supports_pid_checks(self.pid) { + show_warning!("--pid=PID is not supported on this system"); + } + } + + // This warning originates from gnu's tail implementation of the equivalent warning. If the + // user wants to follow stdin, but tail is blocking indefinitely anyways, because of stdin + // as `tty` (but no otherwise blocking stdin), then we print a warning that `--follow` + // cannot be applied under these circumstances and is therefore ineffective. + if self.follow.is_some() && self.has_stdin() { + let blocking_stdin = self.pid == 0 + && self.follow == Some(FollowMode::Descriptor) + && self.num_inputs() == 1 + && Handle::stdin().map_or(false, |handle| { + handle + .as_file() + .metadata() + .map_or(false, |meta| !meta.is_file()) + }); + + if !blocking_stdin && atty::is(Stream::Stdin) { + show_warning!("following standard input indefinitely is ineffective"); + } + } + } + + /// Verify [`Settings`] and try to find unsolvable misconfigurations of tail originating from + /// user provided command line arguments. In contrast to [`Settings::check_warnings`] these + /// misconfigurations usually lead to the immediate exit or abortion of the running `tail` + /// process. + pub fn verify(&self) -> VerificationResult { + // Mimic GNU's tail for `tail -F` + if self.inputs.iter().any(|i| i.is_stdin()) && self.follow == Some(FollowMode::Name) { + return VerificationResult::CannotFollowStdinByName; + } + + // Mimic GNU's tail for -[nc]0 without -f and exit immediately + if self.follow.is_none() + && matches!( + self.mode, + FilterMode::Lines(Signum::MinusZero, _) | FilterMode::Bytes(Signum::MinusZero) + ) + { + return VerificationResult::NoOutput; + } + + VerificationResult::Ok + } } pub fn arg_iterate<'a>( @@ -298,19 +362,6 @@ fn parse_num(src: &str) -> Result { }) } -pub fn stdin_is_pipe_or_fifo() -> bool { - #[cfg(unix)] - { - platform::stdin_is_pipe_or_fifo() - } - #[cfg(windows)] - { - winapi_util::file::typ(winapi_util::HandleRef::stdin()) - .map(|t| t.is_disk() || t.is_pipe()) - .unwrap_or(false) - } -} - pub fn parse_args(args: impl uucore::Args) -> UResult { let matches = uu_app().try_get_matches_from(arg_iterate(args)?)?; Settings::from(&matches) diff --git a/src/uu/tail/src/chunks.rs b/src/uu/tail/src/chunks.rs index acfc69a30..7ad2e153b 100644 --- a/src/uu/tail/src/chunks.rs +++ b/src/uu/tail/src/chunks.rs @@ -7,7 +7,9 @@ //! or at the end of piped stdin with [`LinesChunk`] or [`BytesChunk`]. //! //! Use [`ReverseChunks::new`] to create a new iterator over chunks of bytes from the file. + // spell-checker:ignore (ToDO) filehandle BUFSIZ + use std::collections::VecDeque; use std::fs::File; use std::io::{BufRead, Read, Seek, SeekFrom, Write}; diff --git a/src/uu/tail/src/follow/files.rs b/src/uu/tail/src/follow/files.rs index 556defd1f..8686e73f4 100644 --- a/src/uu/tail/src/follow/files.rs +++ b/src/uu/tail/src/follow/files.rs @@ -13,7 +13,6 @@ use std::collections::hash_map::Keys; use std::collections::HashMap; use std::fs::{File, Metadata}; use std::io::{stdout, BufRead, BufReader, BufWriter}; - use std::path::{Path, PathBuf}; use uucore::error::UResult; diff --git a/src/uu/tail/src/follow/mod.rs b/src/uu/tail/src/follow/mod.rs index 4bb2798d1..e31eb54d1 100644 --- a/src/uu/tail/src/follow/mod.rs +++ b/src/uu/tail/src/follow/mod.rs @@ -6,4 +6,4 @@ mod files; mod watch; -pub use watch::{follow, WatcherService}; +pub use watch::{follow, Observer}; diff --git a/src/uu/tail/src/follow/watch.rs b/src/uu/tail/src/follow/watch.rs index dd8728c45..31a6d70cb 100644 --- a/src/uu/tail/src/follow/watch.rs +++ b/src/uu/tail/src/follow/watch.rs @@ -13,8 +13,7 @@ use notify::{RecommendedWatcher, RecursiveMode, Watcher, WatcherKind}; use std::collections::VecDeque; use std::io::BufRead; use std::path::{Path, PathBuf}; -use std::sync::mpsc; -use std::sync::mpsc::{channel, Receiver}; +use std::sync::mpsc::{self, channel, Receiver}; use uucore::display::Quotable; use uucore::error::{set_exit_code, UResult, USimpleError}; use uucore::show_error; @@ -81,7 +80,7 @@ impl WatcherRx { } } -pub struct WatcherService { +pub struct Observer { /// Whether --retry was given on the command line pub retry: bool, @@ -92,18 +91,28 @@ pub struct WatcherService { /// platform specific event driven method. Since `use_polling` is subject to /// change during runtime it is moved out of [`Settings`]. pub use_polling: bool, + pub watcher_rx: Option, pub orphans: Vec, pub files: FileHandling, + + pub pid: platform::Pid, } -impl WatcherService { +impl Observer { pub fn new( retry: bool, follow: Option, use_polling: bool, files: FileHandling, + pid: platform::Pid, ) -> Self { + let pid = if platform::supports_pid_checks(pid) { + pid + } else { + 0 + }; + Self { retry, follow, @@ -111,6 +120,7 @@ impl WatcherService { watcher_rx: None, orphans: Vec::new(), files, + pid, } } @@ -120,6 +130,7 @@ impl WatcherService { settings.follow, settings.use_polling, FileHandling::from(settings), + settings.pid, ) } @@ -460,14 +471,12 @@ impl WatcherService { } } -pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResult<()> { - if watcher_service.files.no_files_remaining(settings) - && !watcher_service.files.only_stdin_remaining() - { +pub fn follow(mut observer: Observer, settings: &Settings) -> UResult<()> { + if observer.files.no_files_remaining(settings) && !observer.files.only_stdin_remaining() { return Err(USimpleError::new(1, text::NO_FILES_REMAINING.to_string())); } - let mut process = platform::ProcessChecker::new(settings.pid); + let mut process = platform::ProcessChecker::new(observer.pid); let mut _event_counter = 0; let mut _timeout_counter = 0; @@ -478,7 +487,7 @@ pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResu // If `--pid=p`, tail checks whether process p // is alive at least every `--sleep-interval=N` seconds - if settings.follow.is_some() && settings.pid != 0 && process.is_dead() { + if settings.follow.is_some() && observer.pid != 0 && process.is_dead() { // p is dead, tail will also terminate break; } @@ -487,22 +496,20 @@ pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResu // If a path becomes an orphan during runtime, it will be added to orphans. // To be able to differentiate between the cases of test_retry8 and test_retry9, // here paths will not be removed from orphans if the path becomes available. - if watcher_service.follow_name_retry() { - for new_path in &watcher_service.orphans { + if observer.follow_name_retry() { + for new_path in &observer.orphans { if new_path.exists() { - let pd = watcher_service.files.get(new_path); + let pd = observer.files.get(new_path); let md = new_path.metadata().unwrap(); if md.is_tailable() && pd.reader.is_none() { show_error!( "{} has appeared; following new file", pd.display_name.quote() ); - watcher_service.files.update_metadata(new_path, Some(md)); - watcher_service.files.update_reader(new_path)?; - _read_some = watcher_service - .files - .tail_file(new_path, settings.verbose)?; - watcher_service + observer.files.update_metadata(new_path, Some(md)); + observer.files.update_reader(new_path)?; + _read_some = observer.files.tail_file(new_path, settings.verbose)?; + observer .watcher_rx .as_mut() .unwrap() @@ -514,7 +521,7 @@ pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResu // With -f, sleep for approximately N seconds (default 1.0) between iterations; // We wake up if Notify sends an Event or if we wait more than `sleep_sec`. - let rx_result = watcher_service + let rx_result = observer .watcher_rx .as_mut() .unwrap() @@ -529,9 +536,9 @@ pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResu match rx_result { Ok(Ok(event)) => { if let Some(event_path) = event.paths.first() { - if watcher_service.files.contains_key(event_path) { + if observer.files.contains_key(event_path) { // Handle Event if it is about a path that we are monitoring - paths = watcher_service.handle_event(&event, settings)?; + paths = observer.handle_event(&event, settings)?; } } } @@ -540,8 +547,8 @@ pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResu paths, })) if e.kind() == std::io::ErrorKind::NotFound => { if let Some(event_path) = paths.first() { - if watcher_service.files.contains_key(event_path) { - let _ = watcher_service + if observer.files.contains_key(event_path) { + let _ = observer .watcher_rx .as_mut() .unwrap() @@ -566,16 +573,16 @@ pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResu Err(e) => return Err(USimpleError::new(1, format!("RecvTimeoutError: {}", e))), } - if watcher_service.use_polling && settings.follow.is_some() { + if observer.use_polling && settings.follow.is_some() { // Consider all files to potentially have new content. // This is a workaround because `Notify::PollWatcher` // does not recognize the "renaming" of files. - paths = watcher_service.files.keys().cloned().collect::>(); + paths = observer.files.keys().cloned().collect::>(); } // main print loop for path in &paths { - _read_some = watcher_service.files.tail_file(path, settings.verbose)?; + _read_some = observer.files.tail_file(path, settings.verbose)?; } if _timeout_counter == settings.max_unchanged_stats { diff --git a/src/uu/tail/src/paths.rs b/src/uu/tail/src/paths.rs index 203a23817..03656d036 100644 --- a/src/uu/tail/src/paths.rs +++ b/src/uu/tail/src/paths.rs @@ -5,24 +5,14 @@ // spell-checker:ignore tailable seekable stdlib (stdlib) -#[cfg(unix)] -use std::os::unix::fs::{FileTypeExt, MetadataExt}; - -use std::collections::VecDeque; +use crate::text; use std::fs::{File, Metadata}; use std::io::{Seek, SeekFrom}; +#[cfg(unix)] +use std::os::unix::fs::{FileTypeExt, MetadataExt}; use std::path::{Path, PathBuf}; - use uucore::error::UResult; -use crate::args::Settings; -use crate::text; - -// * This file is part of the uutils coreutils package. -// * -// * For the full copyright and license information, please view the LICENSE -// * file that was distributed with this source code. - #[derive(Debug, Clone)] pub enum InputKind { File(PathBuf), @@ -36,6 +26,7 @@ pub struct Input { } impl Input { + // TODO: from &str may be the better choice pub fn from(string: String) -> Self { let kind = if string == text::DASH { InputKind::Stdin @@ -132,44 +123,6 @@ impl HeaderPrinter { } } } - -#[derive(Debug, Clone)] -pub struct InputService { - pub inputs: VecDeque, - pub presume_input_pipe: bool, - pub header_printer: HeaderPrinter, -} - -impl InputService { - pub fn new(verbose: bool, presume_input_pipe: bool, inputs: VecDeque) -> Self { - Self { - inputs, - presume_input_pipe, - header_printer: HeaderPrinter::new(verbose, true), - } - } - - pub fn from(settings: &Settings) -> Self { - Self::new( - settings.verbose, - settings.presume_input_pipe, - settings.inputs.clone(), - ) - } - - pub fn has_stdin(&mut self) -> bool { - self.inputs.iter().any(|input| input.is_stdin()) - } - - pub fn has_only_stdin(&self) -> bool { - self.inputs.iter().all(|input| input.is_stdin()) - } - - pub fn print_header(&mut self, input: &Input) { - self.header_printer.print_input(input); - } -} - pub trait FileExtTail { #[allow(clippy::wrong_self_convention)] fn is_seekable(&mut self, current_offset: u64) -> bool; @@ -228,9 +181,11 @@ impl MetadataExtTail for Metadata { } #[cfg(windows)] { + // TODO: `file_index` requires unstable library feature `windows_by_handle` // use std::os::windows::prelude::*; // if let Some(self_id) = self.file_index() { // if let Some(other_id) = other.file_index() { + // // TODO: not sure this is the equivalent of comparing inode numbers // // return self_id.eq(&other_id); // } diff --git a/src/uu/tail/src/platform/mod.rs b/src/uu/tail/src/platform/mod.rs index 17660731d..e5ae8b8d8 100644 --- a/src/uu/tail/src/platform/mod.rs +++ b/src/uu/tail/src/platform/mod.rs @@ -11,7 +11,6 @@ #[cfg(unix)] pub use self::unix::{ //stdin_is_bad_fd, stdin_is_pipe_or_fifo, supports_pid_checks, Pid, ProcessChecker, - stdin_is_pipe_or_fifo, supports_pid_checks, Pid, ProcessChecker, diff --git a/src/uu/tail/src/platform/unix.rs b/src/uu/tail/src/platform/unix.rs index 2627cc252..ed34b2cf9 100644 --- a/src/uu/tail/src/platform/unix.rs +++ b/src/uu/tail/src/platform/unix.rs @@ -11,8 +11,6 @@ // spell-checker:ignore (ToDO) stdlib, ISCHR, GETFD // spell-checker:ignore (options) EPERM, ENOSYS -use libc::S_IFCHR; -use nix::sys::stat::fstat; use std::io::Error; pub type Pid = libc::pid_t; @@ -45,13 +43,6 @@ pub fn supports_pid_checks(pid: self::Pid) -> bool { fn get_errno() -> i32 { Error::last_os_error().raw_os_error().unwrap() } -#[inline] -pub fn stdin_is_pipe_or_fifo() -> bool { - // IFCHR means the file (stdin) is a character input device, which is the case of a terminal. - // We just need to check if stdin is not a character device here, because we are not interested - // in the type of stdin itself. - fstat(libc::STDIN_FILENO).map_or(false, |file| file.st_mode as libc::mode_t & S_IFCHR == 0) -} //pub fn stdin_is_bad_fd() -> bool { // FIXME: Detect a closed file descriptor, e.g.: `tail <&-` diff --git a/src/uu/tail/src/tail.rs b/src/uu/tail/src/tail.rs index af56c9813..2a5a94352 100644 --- a/src/uu/tail/src/tail.rs +++ b/src/uu/tail/src/tail.rs @@ -24,58 +24,57 @@ mod paths; mod platform; pub mod text; +pub use args::uu_app; +use args::{parse_args, FilterMode, Settings, Signum}; +use chunks::ReverseChunks; +use follow::Observer; +use paths::{FileExtTail, HeaderPrinter, Input, InputKind, MetadataExtTail}; use same_file::Handle; use std::cmp::Ordering; use std::fs::File; use std::io::{self, stdin, stdout, BufRead, BufReader, BufWriter, Read, Seek, SeekFrom, Write}; use std::path::{Path, PathBuf}; -use uucore::{show, show_error, show_warning}; - use uucore::display::Quotable; use uucore::error::{get_exit_code, set_exit_code, FromIo, UError, UResult, USimpleError}; - -pub use args::uu_app; -use args::{parse_args, FilterMode, Settings, Signum}; -use chunks::ReverseChunks; -use follow::WatcherService; -use paths::{FileExtTail, Input, InputKind, InputService, MetadataExtTail}; +use uucore::{show, show_error}; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let settings = parse_args(args)?; + + settings.check_warnings(); + + match settings.verify() { + args::VerificationResult::CannotFollowStdinByName => { + return Err(USimpleError::new( + 1, + format!("cannot follow {} by name", text::DASH.quote()), + )) + } + // Exit early if we do not output anything. Note, that this may break a pipe + // when tail is on the receiving side. + args::VerificationResult::NoOutput => return Ok(()), + args::VerificationResult::Ok => {} + } + uu_tail(&settings) } fn uu_tail(settings: &Settings) -> UResult<()> { - // Mimic GNU's tail for `tail -F` and exit immediately - let mut input_service = InputService::from(settings); - let mut watcher_service = WatcherService::from(settings); + let mut printer = HeaderPrinter::new(settings.verbose, true); + let mut observer = Observer::from(settings); - if input_service.has_stdin() && watcher_service.follow_name() { - return Err(USimpleError::new( - 1, - format!("cannot follow {} by name", text::DASH.quote()), - )); - } - - watcher_service.start(settings)?; + observer.start(settings)?; // Do an initial tail print of each path's content. // Add `path` and `reader` to `files` map if `--follow` is selected. - for input in &input_service.inputs.clone() { + for input in &settings.inputs.clone() { match input.kind() { InputKind::File(path) if cfg!(not(unix)) || path != &PathBuf::from(text::DEV_STDIN) => { - tail_file( - settings, - &mut input_service, - input, - path, - &mut watcher_service, - 0, - )?; + tail_file(settings, &mut printer, input, path, &mut observer, 0)?; } // File points to /dev/stdin here InputKind::File(_) | InputKind::Stdin => { - tail_stdin(settings, &mut input_service, input, &mut watcher_service)?; + tail_stdin(settings, &mut printer, input, &mut observer)?; } } } @@ -90,9 +89,8 @@ fn uu_tail(settings: &Settings) -> UResult<()> { the input file is not a FIFO, pipe, or regular file, it is unspecified whether or not the -f option shall be ignored. */ - - if !input_service.has_only_stdin() { - follow::follow(watcher_service, settings)?; + if !settings.has_only_stdin() { + follow::follow(observer, settings)?; } } @@ -105,16 +103,12 @@ fn uu_tail(settings: &Settings) -> UResult<()> { fn tail_file( settings: &Settings, - input_service: &mut InputService, + header_printer: &mut HeaderPrinter, input: &Input, path: &Path, - watcher_service: &mut WatcherService, + observer: &mut Observer, offset: u64, ) -> UResult<()> { - if watcher_service.follow_descriptor_retry() { - show_warning!("--retry only effective for the initial open"); - } - if !path.exists() { set_exit_code(1); show_error!( @@ -122,11 +116,11 @@ fn tail_file( input.display_name, text::NO_SUCH_FILE ); - watcher_service.add_bad_path(path, input.display_name.as_str(), false)?; + observer.add_bad_path(path, input.display_name.as_str(), false)?; } else if path.is_dir() { set_exit_code(1); - input_service.print_header(input); + header_printer.print_input(input); let err_msg = "Is a directory".to_string(); show_error!("error reading '{}': {}", input.display_name, err_msg); @@ -142,16 +136,16 @@ fn tail_file( msg ); } - if !(watcher_service.follow_name_retry()) { + if !(observer.follow_name_retry()) { // skip directory if not retry return Ok(()); } - watcher_service.add_bad_path(path, input.display_name.as_str(), false)?; + observer.add_bad_path(path, input.display_name.as_str(), false)?; } else if input.is_tailable() { let metadata = path.metadata().ok(); match File::open(path) { Ok(mut file) => { - input_service.print_header(input); + header_printer.print_input(input); let mut reader; if !settings.presume_input_pipe && file.is_seekable(if input.is_stdin() { offset } else { 0 }) @@ -163,7 +157,7 @@ fn tail_file( reader = BufReader::new(file); unbounded_tail(&mut reader, settings)?; } - watcher_service.add_path( + observer.add_path( path, input.display_name.as_str(), Some(Box::new(reader)), @@ -171,20 +165,20 @@ fn tail_file( )?; } Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => { - watcher_service.add_bad_path(path, input.display_name.as_str(), false)?; + observer.add_bad_path(path, input.display_name.as_str(), false)?; show!(e.map_err_context(|| { format!("cannot open '{}' for reading", input.display_name) })); } Err(e) => { - watcher_service.add_bad_path(path, input.display_name.as_str(), false)?; + observer.add_bad_path(path, input.display_name.as_str(), false)?; return Err(e.map_err_context(|| { format!("cannot open '{}' for reading", input.display_name) })); } } } else { - watcher_service.add_bad_path(path, input.display_name.as_str(), false)?; + observer.add_bad_path(path, input.display_name.as_str(), false)?; } Ok(()) @@ -192,9 +186,9 @@ fn tail_file( fn tail_stdin( settings: &Settings, - input_service: &mut InputService, + header_printer: &mut HeaderPrinter, input: &Input, - watcher_service: &mut WatcherService, + observer: &mut Observer, ) -> UResult<()> { match input.resolve() { // fifo @@ -211,24 +205,20 @@ fn tail_stdin( } tail_file( settings, - input_service, + header_printer, input, &path, - watcher_service, + observer, stdin_offset, )?; } // pipe None => { - input_service.print_header(input); + header_printer.print_input(input); if !paths::stdin_is_bad_fd() { let mut reader = BufReader::new(stdin()); unbounded_tail(&mut reader, settings)?; - watcher_service.add_stdin( - input.display_name.as_str(), - Some(Box::new(reader)), - true, - )?; + observer.add_stdin(input.display_name.as_str(), Some(Box::new(reader)), true)?; } else { set_exit_code(1); show_error!( @@ -417,7 +407,7 @@ fn unbounded_tail(reader: &mut BufReader, settings: &Settings) -> UR FilterMode::Lines(Signum::Negative(count), sep) => { let mut chunks = chunks::LinesChunkBuffer::new(*sep, *count); chunks.fill(reader)?; - chunks.print(writer)?; + chunks.print(&mut writer)?; } FilterMode::Lines(Signum::PlusZero | Signum::Positive(1), _) => { io::copy(reader, &mut writer)?; @@ -441,7 +431,7 @@ fn unbounded_tail(reader: &mut BufReader, settings: &Settings) -> UR FilterMode::Bytes(Signum::Negative(count)) => { let mut chunks = chunks::BytesChunkBuffer::new(*count); chunks.fill(reader)?; - chunks.print(writer)?; + chunks.print(&mut writer)?; } FilterMode::Bytes(Signum::PlusZero | Signum::Positive(1)) => { io::copy(reader, &mut writer)?; diff --git a/src/uu/tail/src/text.rs b/src/uu/tail/src/text.rs index 62ad9cf70..0e4a26f39 100644 --- a/src/uu/tail/src/text.rs +++ b/src/uu/tail/src/text.rs @@ -19,3 +19,6 @@ pub static BACKEND: &str = "kqueue"; #[cfg(target_os = "windows")] pub static BACKEND: &str = "ReadDirectoryChanges"; pub static FD0: &str = "/dev/fd/0"; +pub static IS_A_DIRECTORY: &str = "Is a directory"; +pub static DEV_TTY: &str = "/dev/tty"; +pub static DEV_PTMX: &str = "/dev/ptmx"; diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index e59dcf3ea..6ea075541 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -5,17 +5,33 @@ // spell-checker:ignore (ToDO) abcdefghijklmnopqrstuvwxyz efghijklmnopqrstuvwxyz vwxyz emptyfile file siette ocho nueve diez MULT // spell-checker:ignore (libs) kqueue -// spell-checker:ignore (jargon) tailable untailable +// spell-checker:ignore (jargon) tailable untailable datasame runneradmin tmpi extern crate tail; use crate::common::random::*; use crate::common::util::*; +use pretty_assertions::assert_eq; use rand::distributions::Alphanumeric; use std::char::from_digit; +use std::fs::File; use std::io::Write; +#[cfg(not(target_vendor = "apple"))] +use std::io::{Seek, SeekFrom}; +#[cfg(all( + not(target_vendor = "apple"), + not(target_os = "windows"), + not(target_os = "freebsd") +))] +use std::path::Path; use std::process::Stdio; use tail::chunks::BUFFER_SIZE as CHUNK_BUFFER_SIZE; +#[cfg(all( + not(target_vendor = "apple"), + not(target_os = "windows"), + not(target_os = "freebsd") +))] +use tail::text; static FOOBAR_TXT: &str = "foobar.txt"; static FOOBAR_2_TXT: &str = "foobar2.txt"; @@ -73,12 +89,12 @@ fn test_stdin_redirect_file() { at.write("f", "foo"); ts.ucmd() - .set_stdin(std::fs::File::open(at.plus("f")).unwrap()) + .set_stdin(File::open(at.plus("f")).unwrap()) .run() .stdout_is("foo") .succeeded(); ts.ucmd() - .set_stdin(std::fs::File::open(at.plus("f")).unwrap()) + .set_stdin(File::open(at.plus("f")).unwrap()) .arg("-v") .run() .no_stderr() @@ -88,7 +104,7 @@ fn test_stdin_redirect_file() { let mut p = ts .ucmd() .arg("-f") - .set_stdin(std::fs::File::open(at.plus("f")).unwrap()) + .set_stdin(File::open(at.plus("f")).unwrap()) .run_no_wait(); p.make_assertion_with_delay(500).is_alive(); @@ -102,13 +118,12 @@ fn test_stdin_redirect_file() { #[cfg(not(target_vendor = "apple"))] // FIXME: for currently not working platforms fn test_stdin_redirect_offset() { // inspired by: "gnu/tests/tail-2/start-middle.sh" - use std::io::{Seek, SeekFrom}; let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; at.write("k", "1\n2\n"); - let mut fh = std::fs::File::open(at.plus("k")).unwrap(); + let mut fh = File::open(at.plus("k")).unwrap(); fh.seek(SeekFrom::Start(2)).unwrap(); ts.ucmd() @@ -127,7 +142,6 @@ fn test_stdin_redirect_offset2() { // expected: ==> standard input <== // like test_stdin_redirect_offset but with multiple files - use std::io::{Seek, SeekFrom}; let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; @@ -135,7 +149,7 @@ fn test_stdin_redirect_offset2() { at.write("k", "1\n2\n"); at.write("l", "3\n4\n"); at.write("m", "5\n6\n"); - let mut fh = std::fs::File::open(at.plus("k")).unwrap(); + let mut fh = File::open(at.plus("k")).unwrap(); fh.seek(SeekFrom::Start(2)).unwrap(); ts.ucmd() @@ -257,7 +271,7 @@ fn test_follow_redirect_stdin_name_retry() { let mut args = vec!["-F", "-"]; for _ in 0..2 { ts.ucmd() - .set_stdin(std::fs::File::open(at.plus("f")).unwrap()) + .set_stdin(File::open(at.plus("f")).unwrap()) .args(&args) .fails() .no_stdout() @@ -284,13 +298,13 @@ fn test_stdin_redirect_dir() { at.mkdir("dir"); ts.ucmd() - .set_stdin(std::fs::File::open(at.plus("dir")).unwrap()) + .set_stdin(File::open(at.plus("dir")).unwrap()) .fails() .no_stdout() .stderr_is("tail: error reading 'standard input': Is a directory") .code_is(1); ts.ucmd() - .set_stdin(std::fs::File::open(at.plus("dir")).unwrap()) + .set_stdin(File::open(at.plus("dir")).unwrap()) .arg("-") .fails() .no_stdout() @@ -317,13 +331,13 @@ fn test_stdin_redirect_dir_when_target_os_is_macos() { at.mkdir("dir"); ts.ucmd() - .set_stdin(std::fs::File::open(at.plus("dir")).unwrap()) + .set_stdin(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()) + .set_stdin(File::open(at.plus("dir")).unwrap()) .arg("-") .fails() .no_stdout() @@ -365,61 +379,6 @@ fn test_follow_stdin_name_retry() { } } -#[test] -#[cfg(target_os = "linux")] -#[cfg(disable_until_fixed)] -fn test_follow_stdin_explicit_indefinitely() { - // inspired by: "gnu/tests/tail-2/follow-stdin.sh" - // tail -f - /dev/null standard input <== - - let ts = TestScenario::new(util_name!()); - - let mut p = ts - .ucmd() - .set_stdin(Stdio::null()) - .args(&["-f", "-", "/dev/null"]) - .run_no_wait(); - - p.make_assertion_with_delay(500).is_alive(); - p.kill() - .make_assertion() - .with_all_output() - .stdout_is("==> standard input <==") - .stderr_is("tail: warning: following standard input indefinitely is ineffective"); - - // Also: - // $ echo bar > foo - // - // $ tail -f - - - // tail: warning: following standard input indefinitely is ineffective - // ==> standard input <== - // - // $ tail -f - foo - // tail: warning: following standard input indefinitely is ineffective - // ==> standard input <== - // - // - // $ tail -f - foo - // tail: warning: following standard input indefinitely is ineffective - // ==> standard input <== - // - // $ tail -f foo - - // tail: warning: following standard input indefinitely is ineffective - // ==> foo <== - // bar - // - // ==> standard input <== - // - - // $ echo f00 | tail -f foo - - // bar - // - - // TODO: Implement the above behavior of GNU's tail for following stdin indefinitely -} - #[test] fn test_follow_bad_fd() { // Provoke a "bad file descriptor" error by closing the fd @@ -688,7 +647,7 @@ fn test_follow_invalid_pid() { not(target_os = "android") ))] // FIXME: for currently not working platforms fn test_follow_with_pid() { - use std::process::{Command, Stdio}; + use std::process::Command; let (at, mut ucmd) = at_and_ucmd!(); @@ -1056,6 +1015,7 @@ fn test_positive_zero_bytes() { ts.ucmd() .args(&["-c", "0"]) .pipe_in("abcde") + .ignore_stdin_write_error() .succeeds() .no_stdout() .no_stderr(); @@ -1539,8 +1499,8 @@ fn test_retry8() { let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; - let watched_file = std::path::Path::new("watched_file"); - let parent_dir = std::path::Path::new("parent_dir"); + let watched_file = Path::new("watched_file"); + let parent_dir = Path::new("parent_dir"); let user_path = parent_dir.join(watched_file); let parent_dir = parent_dir.to_str().unwrap(); let user_path = user_path.to_str().unwrap(); @@ -1603,12 +1563,12 @@ fn test_retry9() { // Ensure that inotify will switch to polling mode if directory // of the watched file was removed and recreated. - use tail::text::BACKEND; + use text::BACKEND; let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; - let watched_file = std::path::Path::new("watched_file"); - let parent_dir = std::path::Path::new("parent_dir"); + let watched_file = Path::new("watched_file"); + let parent_dir = Path::new("parent_dir"); let user_path = parent_dir.join(watched_file); let parent_dir = parent_dir.to_str().unwrap(); let user_path = user_path.to_str().unwrap(); @@ -3496,3 +3456,1070 @@ fn test_args_when_presume_input_pipe_given_input_is_file() { .succeeds() .stdout_only(random_string); } + +#[test] +#[cfg(disable_until_fixed)] +// FIXME: currently missing in the error message is the last line >>tail: no files remaining<< +fn test_when_follow_retry_given_redirected_stdin_from_directory_then_correct_error_message() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.mkdir("dir"); + + let expected = "tail: warning: --retry only effective for the initial open\n\ + tail: error reading 'standard input': Is a directory\n\ + tail: 'standard input': cannot follow end of this type of file\n\ + tail: no files remaining\n"; + ts.ucmd() + .set_stdin(File::open(at.plus("dir")).unwrap()) + .args(&["-f", "--retry"]) + .fails() + .stderr_only(expected) + .code_is(1); +} + +#[test] +fn test_when_argument_file_is_a_directory() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.mkdir("dir"); + + let expected = "tail: error reading 'dir': Is a directory"; + ts.ucmd() + .arg("dir") + .fails() + .stderr_only(expected) + .code_is(1); +} + +// TODO: make this work on windows +#[test] +#[cfg(unix)] +fn test_when_argument_file_is_a_symlink() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + let mut file = at.make_file("target"); + + at.symlink_file("target", "link"); + + ts.ucmd() + .args(&["-c", "+0", "link"]) + .succeeds() + .no_stdout() + .no_stderr(); + + let random_string = RandomString::generate(AlphanumericNewline, 100); + let result = file.write_all(random_string.as_bytes()); + assert!(result.is_ok()); + + ts.ucmd() + .args(&["-c", "+0", "link"]) + .succeeds() + .stdout_only(random_string); + + at.mkdir("dir"); + + at.symlink_file("dir", "dir_link"); + + let expected = "tail: error reading 'dir_link': Is a directory"; + ts.ucmd() + .arg("dir_link") + .fails() + .stderr_only(expected) + .code_is(1); +} + +// TODO: make this work on windows +#[test] +#[cfg(unix)] +fn test_when_argument_file_is_a_symlink_to_directory_then_error() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + at.mkdir("dir"); + at.symlink_file("dir", "dir_link"); + + let expected = "tail: error reading 'dir_link': Is a directory"; + ts.ucmd() + .arg("dir_link") + .fails() + .stderr_only(expected) + .code_is(1); +} + +// TODO: make this work on windows +#[test] +#[cfg(unix)] +#[cfg(disabled_until_fixed)] +fn test_when_argument_file_is_a_faulty_symlink_then_error() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + at.symlink_file("self", "self"); + + #[cfg(all(not(target_env = "musl"), not(target_os = "android")))] + let expected = "tail: cannot open 'self' for reading: Too many levels of symbolic links"; + #[cfg(all(not(target_env = "musl"), target_os = "android"))] + let expected = "tail: cannot open 'self' for reading: Too many symbolic links encountered"; + #[cfg(all(target_env = "musl", not(target_os = "android")))] + let expected = "tail: cannot open 'self' for reading: Symbolic link loop"; + + ts.ucmd() + .arg("self") + .fails() + .stderr_only(expected) + .code_is(1); + + at.symlink_file("missing", "broken"); + + let expected = "tail: cannot open 'broken' for reading: No such file or directory"; + ts.ucmd() + .arg("broken") + .fails() + .stderr_only(expected) + .code_is(1); +} + +#[test] +#[cfg(unix)] +#[cfg(disabled_until_fixed)] +fn test_when_argument_file_is_non_existent_unix_socket_address_then_error() { + use std::os::unix::net; + + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + let socket = "socket"; + + // We only bind to create the socket file but do not listen + let result = net::UnixListener::bind(at.plus(socket)); + assert!(result.is_ok()); + + #[cfg(all(not(target_os = "freebsd"), not(target_os = "macos")))] + let expected_stderr = format!( + "tail: cannot open '{}' for reading: No such device or address", + socket + ); + #[cfg(target_os = "freebsd")] + let expected_stderr = format!( + "tail: cannot open '{}' for reading: Operation not supported", + socket + ); + #[cfg(target_os = "macos")] + let expected_stderr = format!( + "tail: cannot open '{}' for reading: Operation not supported on socket", + socket + ); + + ts.ucmd() + .arg(socket) + .fails() + .stderr_only(&expected_stderr) + .code_is(1); + + let path = "file"; + let mut file = at.make_file(path); + + let random_string = RandomString::generate(AlphanumericNewline, 100); + let result = file.write_all(random_string.as_bytes()); + assert!(result.is_ok()); + + let expected_stdout = vec![format!("==> {} <==", path), random_string].join("\n"); + ts.ucmd() + .args(&["-c", "+0", path, socket]) + .fails() + .stdout_is(&expected_stdout) + .stderr_is(&expected_stderr); + + // tail does not stop processing files when having encountered a "No such + // device or address" error. + ts.ucmd() + .args(&["-c", "+0", socket, path]) + .fails() + .stdout_is(&expected_stdout) + .stderr_is(&expected_stderr); +} + +#[test] +#[cfg(disabled_until_fixed)] +fn test_when_argument_files_are_simple_combinations_of_stdin_and_regular_file() { + let scene = TestScenario::new(util_name!()); + let fixtures = &scene.fixtures; + + fixtures.write("empty", ""); + fixtures.write("data", "file data"); + fixtures.write("fifo", "fifo data"); + + let expected = "==> standard input <==\n\ + fifo data\n\ + ==> empty <==\n"; + scene + .ucmd() + .args(&["-c", "+0", "-", "empty"]) + .set_stdin(File::open(fixtures.plus("fifo")).unwrap()) + .run() + .success() + .stdout_only(expected); + + let expected = "==> standard input <==\n\ + \n\ + ==> empty <==\n"; + scene + .ucmd() + .args(&["-c", "+0", "-", "empty"]) + .pipe_in("") + .run() + .success() + .stdout_only(expected); + + let expected = "==> empty <==\n\ + \n\ + ==> standard input <==\n"; + scene + .ucmd() + .args(&["-c", "+0", "empty", "-"]) + .pipe_in("") + .run() + .success() + .stdout_only(expected); + + let expected = "==> empty <==\n\ + \n\ + ==> standard input <==\n\ + fifo data"; + scene + .ucmd() + .args(&["-c", "+0", "empty", "-"]) + .set_stdin(File::open(fixtures.plus("fifo")).unwrap()) + .run() + .success() + .stdout_only(expected); + + let expected = "==> standard input <==\n\ + pipe data\n\ + ==> data <==\n\ + file data"; + scene + .ucmd() + .args(&["-c", "+0", "-", "data"]) + .pipe_in("pipe data") + .run() + .success() + .stdout_only(expected); + + let expected = "==> data <==\n\ + file data\n\ + ==> standard input <==\n\ + pipe data"; + scene + .ucmd() + .args(&["-c", "+0", "data", "-"]) + .pipe_in("pipe data") + .run() + .success() + .stdout_only(expected); + + let expected = "==> standard input <==\n\ + pipe data\n\ + ==> standard input <==\n"; + scene + .ucmd() + .args(&["-c", "+0", "-", "-"]) + .pipe_in("pipe data") + .run() + .success() + .stdout_only(expected); + + let expected = "==> standard input <==\n\ + fifo data\n\ + ==> standard input <==\n"; + scene + .ucmd() + .args(&["-c", "+0", "-", "-"]) + .set_stdin(File::open(fixtures.plus("fifo")).unwrap()) + .run() + .success() + .stdout_only(expected); +} + +#[test] +#[cfg(disabled_until_fixed)] +fn test_when_argument_files_are_triple_combinations_of_fifo_pipe_and_regular_file() { + let scene = TestScenario::new(util_name!()); + let fixtures = &scene.fixtures; + + fixtures.write("empty", ""); + fixtures.write("data", "file data"); + fixtures.write("fifo", "fifo data"); + + let expected = "==> standard input <==\n\ + \n\ + ==> empty <==\n\ + \n\ + ==> standard input <==\n"; + + scene + .ucmd() + .args(&["-c", "+0", "-", "empty", "-"]) + .set_stdin(File::open(fixtures.plus("empty")).unwrap()) + .run() + .stdout_only(expected) + .success(); + + let expected = "==> standard input <==\n\ + \n\ + ==> empty <==\n\ + \n\ + ==> standard input <==\n"; + scene + .ucmd() + .args(&["-c", "+0", "-", "empty", "-"]) + .pipe_in("") + .stderr_to_stdout() + .run() + .stdout_only(expected) + .success(); + + let expected = "==> standard input <==\n\ + pipe data\n\ + ==> data <==\n\ + file data\n\ + ==> standard input <==\n"; + scene + .ucmd() + .args(&["-c", "+0", "-", "data", "-"]) + .pipe_in("pipe data") + .run() + .stdout_only(expected) + .success(); + + // Correct behavior in a sh shell is to remember the file pointer for the fifo, so we don't + // print the fifo twice. This matches the behavior, if only the pipe is present without fifo + // (See test above). Note that for example a zsh shell prints the pipe data and has therefore + // different output from the sh shell (or cmd shell on windows). + + // windows: tail returns with success although there is an error message present (on some + // windows systems). This error message comes from `echo` (the line ending `\r\n` indicates that + // too) which cannot write to the pipe because tail finished before echo was able to write to + // the pipe. Seems that windows `cmd` (like posix shells) ignores pipes when a fifo is present. + // This is actually the wished behavior and the test therefore succeeds. + #[cfg(windows)] + let expected = "==> standard input <==\n\ + fifo data\n\ + ==> data <==\n\ + file data\n\ + ==> standard input <==\n\ + (The process tried to write to a nonexistent pipe.\r\n)?"; + #[cfg(unix)] + let expected = "==> standard input <==\n\ + fifo data\n\ + ==> data <==\n\ + file data\n\ + ==> standard input <==\n"; + + #[cfg(windows)] + let cmd = ["cmd", "/C"]; + #[cfg(unix)] + let cmd = ["sh", "-c"]; + + scene + .cmd(cmd[0]) + .arg(cmd[1]) + .arg(format!( + "echo pipe data | {} tail -c +0 - data - < fifo", + scene.bin_path.display(), + )) + .run() + .stdout_only(expected) + .success(); + + let expected = "==> standard input <==\n\ + fifo data\n\ + ==> data <==\n\ + file data\n\ + ==> standard input <==\n"; + scene + .ucmd() + .args(&["-c", "+0", "-", "data", "-"]) + .set_stdin(File::open(fixtures.plus("fifo")).unwrap()) + .run() + .stdout_only(expected) + .success(); +} + +// Bug description: The content of a file is not printed to stdout if the output data does not +// contain newlines and --follow was given as arguments. +// +// This test is only formal on linux, since we currently do not detect this kind of error within the +// test system. However, this behavior shows up on the command line and, at the time of writing this +// description, with this test on macos and windows. +#[test] +#[cfg(disable_until_fixed)] +fn test_when_follow_retry_then_initial_print_of_file_is_written_to_stdout() { + let scene = TestScenario::new(util_name!()); + let fixtures = &scene.fixtures; + + let expected_stdout = "file data"; + fixtures.write("data", expected_stdout); + + let mut child = scene + .ucmd() + .args(&["--follow=name", "--retry", "data"]) + .run_no_wait(); + + child + .delay(1500) + .kill() + .make_assertion() + .with_current_output() + .stdout_only(expected_stdout); +} + +// TODO: Add test for the warning `--pid=PID is not supported on this system` +#[test] +fn test_args_when_settings_check_warnings_then_shows_warnings() { + let scene = TestScenario::new(util_name!()); + let fixtures = &scene.fixtures; + + let file_data = "file data\n"; + fixtures.write("data", file_data); + + let expected_stdout = format!( + "tail: warning: --retry ignored; --retry is useful only when following\n\ + {}", + file_data + ); + scene + .ucmd() + .args(&["--retry", "data"]) + .stderr_to_stdout() + .run() + .stdout_only(expected_stdout) + .success(); + + let expected_stdout = format!( + "tail: warning: --retry only effective for the initial open\n\ + {}", + file_data + ); + let mut child = scene + .ucmd() + .args(&["--follow=descriptor", "--retry", "data"]) + .stderr_to_stdout() + .run_no_wait(); + + child + .delay(500) + .kill() + .make_assertion() + .with_current_output() + .stdout_only(expected_stdout); + + let expected_stdout = format!( + "tail: warning: PID ignored; --pid=PID is useful only when following\n\ + {}", + file_data + ); + scene + .ucmd() + .args(&["--pid=1000", "data"]) + .stderr_to_stdout() + .run() + .stdout_only(expected_stdout) + .success(); + + let expected_stdout = format!( + "tail: warning: --retry ignored; --retry is useful only when following\n\ + tail: warning: PID ignored; --pid=PID is useful only when following\n\ + {}", + file_data + ); + scene + .ucmd() + .args(&["--pid=1000", "--retry", "data"]) + .stderr_to_stdout() + .run() + .stdout_only(expected_stdout) + .success(); +} + +/// TODO: Write similar tests for windows +#[test] +#[cfg(target_os = "linux")] +fn test_args_when_settings_check_warnings_follow_indefinitely_then_warning() { + let scene = TestScenario::new(util_name!()); + + let file_data = "file data\n"; + scene.fixtures.write("data", file_data); + + let expected_stdout = "==> standard input <==\n"; + let expected_stderr = "tail: warning: following standard input indefinitely is ineffective\n"; + + // `tail -f - data` (without any redirect) would also print this warning in a terminal but we're + // not attached to a `tty` in the ci, so it's not possible to setup a test case for this + // particular usage. However, setting stdin to a `tty` behaves equivalently and we're faking an + // attached `tty` that way. + + // testing here that the warning is printed to stderr + // tail -f - data < /dev/ptmx + let mut child = scene + .ucmd() + .args(&["--follow=descriptor", "-", "data"]) + .set_stdin(File::open(text::DEV_PTMX).unwrap()) + .run_no_wait(); + + child.make_assertion_with_delay(500).is_alive(); + child + .kill() + .make_assertion() + .with_current_output() + .stderr_is(expected_stderr) + .stdout_is(expected_stdout); + + let expected_stdout = "tail: warning: following standard input indefinitely is ineffective\n\ + ==> standard input <==\n"; + // same like above but this time the order of the output matters and we're redirecting stderr to + // stdout + // tail -f - data < /dev/ptmx + let mut child = scene + .ucmd() + .args(&["--follow=descriptor", "-", "data"]) + .set_stdin(File::open(text::DEV_PTMX).unwrap()) + .stderr_to_stdout() + .run_no_wait(); + + child.make_assertion_with_delay(500).is_alive(); + child + .kill() + .make_assertion() + .with_current_output() + .stdout_only(expected_stdout); + + let expected_stdout = format!( + "tail: warning: following standard input indefinitely is ineffective\n\ + ==> data <==\n\ + {}\n\ + ==> standard input <==\n", + file_data + ); + // tail -f data - < /dev/ptmx + let mut child = scene + .ucmd() + .args(&["--follow=descriptor", "data", "-"]) + .set_stdin(File::open(text::DEV_PTMX).unwrap()) + .stderr_to_stdout() + .run_no_wait(); + + child.make_assertion_with_delay(500).is_alive(); + child + .kill() + .make_assertion() + .with_current_output() + .stdout_only(expected_stdout); + + let expected_stdout = "tail: warning: following standard input indefinitely is ineffective\n\ + ==> standard input <==\n"; + // tail -f - - < /dev/ptmx + let mut child = scene + .ucmd() + .args(&["--follow=descriptor", "-", "-"]) + .set_stdin(File::open(text::DEV_PTMX).unwrap()) + .stderr_to_stdout() + .run_no_wait(); + + child.make_assertion_with_delay(500).is_alive(); + child + .kill() + .make_assertion() + .with_current_output() + .stdout_only(expected_stdout); + + let expected_stdout = "tail: warning: following standard input indefinitely is ineffective\n\ + ==> standard input <==\n"; + // tail -f - - data < /dev/ptmx + let mut child = scene + .ucmd() + .args(&["--follow=descriptor", "-", "-", "data"]) + .set_stdin(File::open(text::DEV_PTMX).unwrap()) + .stderr_to_stdout() + .run_no_wait(); + + child.make_assertion_with_delay(500).is_alive(); + child + .kill() + .make_assertion() + .with_current_output() + .stdout_only(expected_stdout); + + let expected_stdout = "tail: warning: following standard input indefinitely is ineffective\n\ + ==> standard input <==\n"; + // tail --pid=100000 -f - data < /dev/ptmx + let mut child = scene + .ucmd() + .args(&["--follow=descriptor", "--pid=100000", "-", "data"]) + .set_stdin(File::open(text::DEV_PTMX).unwrap()) + .stderr_to_stdout() + .run_no_wait(); + + child.make_assertion_with_delay(500).is_alive(); + child + .kill() + .make_assertion() + .with_current_output() + .stdout_only(expected_stdout); +} + +#[test] +#[cfg(unix)] +fn test_args_when_settings_check_warnings_follow_indefinitely_then_no_warning() { + let scene = TestScenario::new(util_name!()); + let fixtures = &scene.fixtures; + + #[cfg(target_vendor = "apple")] + let delay = 1000; + #[cfg(not(target_vendor = "apple"))] + let delay = 500; + + let file_data = "file data\n"; + let fifo_data = "fifo data\n"; + let fifo_name = "fifo"; + let file_name = "data"; + fixtures.write(file_name, file_data); + fixtures.write(fifo_name, fifo_data); + + let pipe_data = "pipe data"; + let expected_stdout = format!( + "==> standard input <==\n\ + {}\n\ + ==> {} <==\n\ + {}", + pipe_data, file_name, file_data + ); + let mut child = scene + .ucmd() + .args(&["--follow=descriptor", "-", file_name]) + .pipe_in(pipe_data) + .stderr_to_stdout() + .run_no_wait(); + + child.make_assertion_with_delay(delay).is_alive(); + child + .kill() + .make_assertion_with_delay(delay) + .with_current_output() + .stdout_only(expected_stdout); + + // Test with regular file instead of /dev/tty + // Fails currently on macos with + // Diff < left / right > : + // ==> standard input <== + // >fifo data + // > + // ==> data <== + // file data + #[cfg(not(target_vendor = "apple"))] + { + let expected_stdout = format!( + "==> standard input <==\n\ + {}\n\ + ==> {} <==\n\ + {}", + fifo_data, file_name, file_data + ); + let mut child = scene + .ucmd() + .args(&["--follow=descriptor", "-", file_name]) + .set_stdin(File::open(fixtures.plus(fifo_name)).unwrap()) + .stderr_to_stdout() + .run_no_wait(); + + child.make_assertion_with_delay(delay).is_alive(); + child + .kill() + .make_assertion_with_delay(delay) + .with_current_output() + .stdout_only(expected_stdout); + + let expected_stdout = format!( + "==> standard input <==\n\ + {}\n\ + ==> {} <==\n\ + {}", + fifo_data, file_name, file_data + ); + let mut child = scene + .ucmd() + .args(&["--follow=descriptor", "--pid=0", "-", file_name]) + .set_stdin(File::open(fixtures.plus(fifo_name)).unwrap()) + .stderr_to_stdout() + .run_no_wait(); + + child.make_assertion_with_delay(delay).is_alive(); + child + .kill() + .make_assertion_with_delay(delay) + .with_current_output() + .stdout_only(expected_stdout); + } +} + +/// The expected test outputs come from gnu's tail. +#[test] +#[cfg(disable_until_fixed)] +fn test_follow_when_files_are_pointing_to_same_relative_file_and_data_is_appended() { + let scene = TestScenario::new(util_name!()); + let fixtures = &scene.fixtures; + + let file_data = "file data"; + let relative_path_name = "data"; + + fixtures.write(relative_path_name, file_data); + let absolute_path = fixtures.plus("data").canonicalize().unwrap(); + + // run with relative path first and then the absolute path + let mut child = scene + .ucmd() + .args(&[ + "--follow=name", + relative_path_name, + absolute_path.to_str().unwrap(), + ]) + .run_no_wait(); + + let more_data = "more data"; + child.delay(500); + + fixtures.append(relative_path_name, more_data); + + let expected_stdout = format!( + "==> {0} <==\n\ + {1}\n\ + ==> {2} <==\n\ + {1}\n\ + ==> {0} <==\n\ + {3}", + relative_path_name, + file_data, + absolute_path.to_str().unwrap(), + more_data + ); + + child + .make_assertion_with_delay(DEFAULT_SLEEP_INTERVAL_MILLIS) + .is_alive(); + child + .kill() + .make_assertion() + .with_current_output() + .stderr_only(expected_stdout); + + // run with absolute path first and then the relative path + fixtures.write(relative_path_name, file_data); + let mut child = scene + .ucmd() + .args(&[ + "--follow=name", + absolute_path.to_str().unwrap(), + relative_path_name, + ]) + .run_no_wait(); + + child.delay(500); + let more_data = "more data"; + fixtures.append(relative_path_name, more_data); + + let expected_stdout = format!( + "==> {0} <==\n\ + {1}\n\ + ==> {2} <==\n\ + {1}\n\ + ==> {0} <==\n\ + {3}", + absolute_path.to_str().unwrap(), + file_data, + relative_path_name, + more_data + ); + + child + .make_assertion_with_delay(DEFAULT_SLEEP_INTERVAL_MILLIS) + .is_alive(); + child + .kill() + .make_assertion() + .with_current_output() + .stdout_only(expected_stdout); +} + +/// The expected test outputs come from gnu's tail. +#[test] +#[cfg(disable_until_fixed)] +fn test_follow_when_files_are_pointing_to_same_relative_file_and_file_is_truncated() { + let scene = TestScenario::new(util_name!()); + let fixtures = &scene.fixtures; + + let file_data = "file data"; + let relative_path_name = "data"; + + fixtures.write(relative_path_name, file_data); + let absolute_path = fixtures.plus("data").canonicalize().unwrap(); + + let mut child = scene + .ucmd() + .args(&[ + "--follow=descriptor", + "--max-unchanged-stats=1", + "--sleep-interval=0.1", + relative_path_name, + absolute_path.to_str().unwrap(), + ]) + .stderr_to_stdout() + .run_no_wait(); + + child.delay(500); + let less_data = "less"; + fixtures.write(relative_path_name, "less"); + + let expected_stdout = format!( + "==> {0} <==\n\ + {1}\n\ + ==> {2} <==\n\ + {1}{4}: {0}: file truncated\n\ + \n\ + ==> {0} <==\n\ + {3}", + relative_path_name, // 0 + file_data, // 1 + absolute_path.to_str().unwrap(), // 2 + less_data, // 3 + scene.util_name // 4 + ); + + child.make_assertion_with_delay(500).is_alive(); + child + .kill() + .make_assertion() + .with_current_output() + .stdout_only(expected_stdout); +} + +/// The expected test outputs come from gnu's tail. +#[test] +#[cfg(unix)] +#[cfg(disable_until_fixed)] +fn test_follow_when_file_and_symlink_are_pointing_to_same_file_and_append_data() { + let scene = TestScenario::new(util_name!()); + let fixtures = &scene.fixtures; + + let file_data = "file data"; + let path_name = "data"; + let link_name = "link"; + + fixtures.write(path_name, file_data); + fixtures.symlink_file(path_name, link_name); + + let mut child = scene + .ucmd() + .args(&[ + "--follow=descriptor", + "--max-unchanged-stats=1", + "--sleep-interval=0.1", + path_name, + link_name, + ]) + .run_no_wait(); + + child.delay(500); + let more_data = "more data"; + fixtures.append(path_name, more_data); + + let expected_stdout = format!( + "==> {0} <==\n\ + {1}\n\ + ==> {2} <==\n\ + {1}\n\ + ==> {0} <==\n\ + {3}\n\ + ==> {2} <==\n\ + {3}", + path_name, // 0 + file_data, // 1 + link_name, // 2 + more_data, // 3 + ); + + child.make_assertion_with_delay(500).is_alive(); + child + .kill() + .make_assertion() + .with_current_output() + .stdout_only(expected_stdout); + + fixtures.write(path_name, file_data); + let mut child = scene + .ucmd() + .args(&[ + "--follow=descriptor", + "--max-unchanged-stats=1", + "--sleep-interval=0.1", + link_name, + path_name, + ]) + .run_no_wait(); + + child.delay(500); + let more_data = "more data"; + fixtures.append(path_name, more_data); + + let expected_stdout = format!( + "==> {0} <==\n\ + {1}\n\ + ==> {2} <==\n\ + {1}\n\ + ==> {0} <==\n\ + {3}\n\ + ==> {2} <==\n\ + {3}", + link_name, // 0 + file_data, // 1 + path_name, // 2 + more_data, // 3 + ); + + child.make_assertion_with_delay(500).is_alive(); + child + .kill() + .make_assertion() + .with_current_output() + .stdout_only(expected_stdout); +} + +// Fails with: +// 'Assertion failed. Expected 'tail' to be running but exited with status=exit status: 1. +// stdout: +// stderr: tail: warning: --retry ignored; --retry is useful only when following +// tail: error reading 'dir': Is a directory +// ' +#[test] +#[cfg(disabled_until_fixed)] +fn test_args_when_directory_given_shorthand_big_f_together_with_retry() { + let scene = TestScenario::new(util_name!()); + let fixtures = &scene.fixtures; + + let dirname = "dir"; + fixtures.mkdir(dirname); + let expected_stderr = format!( + "tail: error reading '{0}': Is a directory\n\ + tail: {0}: cannot follow end of this type of file\n", + dirname + ); + + let mut child = scene.ucmd().args(&["-F", "--retry", "dir"]).run_no_wait(); + + child.make_assertion_with_delay(500).is_alive(); + child + .kill() + .make_assertion() + .with_current_output() + .stderr_only(expected_stderr); +} + +/// Fails on macos sometimes with +/// Diff < left / right > : +/// ==> data <== +/// file data +/// ==> /absolute/path/to/data <== +/// file data +/// +/// Fails on windows with +/// Diff < left / right > : +// ==> data <== +// file data +// ==> \\?\C:\Users\runneradmin\AppData\Local\Temp\.tmpi6lNnX\data <== +// >file data +// < +// +// Fails on freebsd with +// Diff < left / right > : +// ==> data <== +// file data +// ==> /tmp/.tmpZPXPlS/data <== +// >file data +// < +#[test] +#[cfg(all( + not(target_vendor = "apple"), + not(target_os = "windows"), + not(target_os = "freebsd") +))] +fn test_follow_when_files_are_pointing_to_same_relative_file_and_file_stays_same_size() { + let scene = TestScenario::new(util_name!()); + let fixtures = &scene.fixtures; + + let file_data = "file data"; + let relative_path_name = "data"; + + fixtures.write(relative_path_name, file_data); + let absolute_path = scene.fixtures.plus("data").canonicalize().unwrap(); + + let mut child = scene + .ucmd() + .args(&[ + "--follow=descriptor", + "--max-unchanged-stats=1", + "--sleep-interval=0.1", + relative_path_name, + absolute_path.to_str().unwrap(), + ]) + .run_no_wait(); + + child.delay(500); + let same_data = "same data"; // equal size to file_data + fixtures.write(relative_path_name, same_data); + + let expected_stdout = format!( + "==> {0} <==\n\ + {1}\n\ + ==> {2} <==\n\ + {1}", + relative_path_name, // 0 + file_data, // 1 + absolute_path.to_str().unwrap(), // 2 + ); + + child.make_assertion_with_delay(500).is_alive(); + child + .kill() + .make_assertion() + .with_current_output() + .stdout_only(expected_stdout); +} + +#[test] +#[cfg(disable_until_fixed)] +fn test_args_sleep_interval_when_illegal_argument_then_usage_error() { + let scene = TestScenario::new(util_name!()); + for interval in [ + &format!("{}0", f64::MAX), + &format!("{}0.0", f64::MAX), + "1_000", + ".", + "' '", + "", + " ", + "0,0", + "one.zero", + ".zero", + "one.", + "0..0", + ] { + scene + .ucmd() + .args(&["--sleep-interval", interval]) + .run() + .usage_error(format!("invalid number of seconds: '{}'", interval)) + .code_is(1); + } +}