diff --git a/src/uu/tail/Cargo.toml b/src/uu/tail/Cargo.toml index 5e11a54119c..095f4c10e2f 100644 --- a/src/uu/tail/Cargo.toml +++ b/src/uu/tail/Cargo.toml @@ -33,6 +33,7 @@ nix = { workspace = true, features = ["fs"] } [target.'cfg(windows)'.dependencies] windows-sys = { workspace = true, features = [ + "Win32_Storage_FileSystem", "Win32_System_Threading", "Win32_Foundation", ] } diff --git a/src/uu/tail/src/follow/watch.rs b/src/uu/tail/src/follow/watch.rs index 06a355b2217..187cd88ff68 100644 --- a/src/uu/tail/src/follow/watch.rs +++ b/src/uu/tail/src/follow/watch.rs @@ -90,6 +90,9 @@ pub struct Observer { /// The [`FollowMode`] pub follow: Option, + /// True when stdin resolves to a tailable file that we should follow. + stdin_is_tailable: bool, + /// 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`]. @@ -100,6 +103,7 @@ pub struct Observer { pub files: FileHandling, pub pid: platform::Pid, + stdin_key: Option, } impl Observer { @@ -119,14 +123,32 @@ impl Observer { Self { retry, follow, + stdin_is_tailable: false, use_polling, watcher_rx: None, orphans: Vec::new(), files, pid, + stdin_key: None, } } + pub fn set_stdin_is_tailable(&mut self, value: bool) { + self.stdin_is_tailable = value; + } + + pub fn stdin_is_tailable(&self) -> bool { + self.stdin_is_tailable + } + + pub fn set_stdin_key>(&mut self, path: P) { + self.stdin_key = Some(path.into()); + } + + pub fn stdin_key(&self) -> Option<&PathBuf> { + self.stdin_key.as_ref() + } + pub fn from(settings: &Settings) -> Self { Self::new( settings.retry, @@ -145,11 +167,12 @@ impl Observer { update_last: bool, ) -> UResult<()> { if self.follow.is_some() { - let path = if path.is_relative() { + let path = if path.is_relative() && !path.is_stdin() { std::env::current_dir()?.join(path) } else { path.to_owned() }; + let metadata = path.metadata().ok(); self.files.insert( &path, @@ -632,6 +655,22 @@ pub fn follow(mut observer: Observer, settings: &Settings) -> UResult<()> { paths = observer.files.keys().cloned().collect::>(); } + if observer.stdin_is_tailable() { + if let Some(stdin_path) = observer.stdin_key() { + if observer.files.contains_key(stdin_path) && !paths.iter().any(|p| p == stdin_path) + { + paths.push(stdin_path.clone()); + } + } else { + let stdin_path = PathBuf::from(text::DEV_STDIN); + if observer.files.contains_key(stdin_path.as_path()) + && !paths.iter().any(|p| p.is_stdin()) + { + paths.push(stdin_path); + } + } + } + // main print loop for path in &paths { _read_some = observer.files.tail_file(path, settings.verbose)?; diff --git a/src/uu/tail/src/tail.rs b/src/uu/tail/src/tail.rs index ad568db7628..6f3c6d92595 100644 --- a/src/uu/tail/src/tail.rs +++ b/src/uu/tail/src/tail.rs @@ -101,7 +101,7 @@ 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 !settings.has_only_stdin() || settings.pid != 0 { + if !settings.has_only_stdin() || settings.pid != 0 || observer.stdin_is_tailable() { follow::follow(observer, settings)?; } } @@ -277,47 +277,81 @@ fn tail_stdin( return Ok(()); } - if let Some(path) = input.resolve() { - // fifo + let resolved_stdin = input.resolve().or_else(resolve_stdin_path); + + if let Some(ref path) = resolved_stdin { + let mut stdin_is_seekable_file = false; let mut stdin_offset = 0; - if cfg!(unix) { - // Save the current seek position/offset of a stdin redirected file. - // This is needed to pass "gnu/tests/tail-2/start-middle.sh" - if let Ok(mut stdin_handle) = Handle::stdin() { - if let Ok(offset) = stdin_handle.as_file_mut().stream_position() { - stdin_offset = offset; - } + + if let Ok(mut stdin_handle) = Handle::stdin() { + if let Ok(offset) = stdin_handle.as_file_mut().stream_position() { + stdin_offset = offset; + } + if let Ok(meta) = stdin_handle.as_file_mut().metadata() { + stdin_is_seekable_file = meta.is_file(); } } - tail_file( - settings, - header_printer, - input, - &path, - observer, - stdin_offset, - )?; - } else { - // pipe - header_printer.print_input(input); - if paths::stdin_is_bad_fd() { + + if !stdin_is_seekable_file { + stdin_is_seekable_file = path.metadata().map(|meta| meta.is_file()).unwrap_or(false); + } + + if stdin_is_seekable_file { + tail_file( + settings, + header_printer, + input, + path, + observer, + stdin_offset, + )?; + + observer.set_stdin_is_tailable(true); + observer.set_stdin_key(path.clone()); + return Ok(()); + } + + if path.metadata().map(|meta| meta.is_dir()).unwrap_or(false) { + header_printer.print_input(input); set_exit_code(1); show_error!( "{}", - translate!("tail-error-cannot-fstat", "file" => translate!("tail-stdin-header"), "error" => translate!("tail-bad-fd")) + translate!( + "tail-error-reading-file", + "file" => input.display_name.clone(), + "error" => translate!("tail-is-a-directory") + ) ); - if settings.follow.is_some() { - show_error!( - "{}", - translate!("tail-error-reading-file", "file" => translate!("tail-stdin-header"), "error" => translate!("tail-bad-fd")) - ); - } - } else { - let mut reader = BufReader::new(stdin()); - unbounded_tail(&mut reader, settings)?; + return Ok(()); } } + header_printer.print_input(input); + if paths::stdin_is_bad_fd() { + set_exit_code(1); + show_error!( + "{}", + translate!( + "tail-error-cannot-fstat", + "file" => translate!("tail-stdin-header"), + "error" => translate!("tail-bad-fd") + ) + ); + if settings.follow.is_some() { + show_error!( + "{}", + translate!( + "tail-error-reading-file", + "file" => translate!("tail-stdin-header"), + "error" => translate!("tail-bad-fd") + ) + ); + } + } else { + let mut reader = BufReader::new(stdin()); + unbounded_tail(&mut reader, settings)?; + } + Ok(()) } @@ -586,6 +620,39 @@ where } } +#[cfg(windows)] +fn resolve_stdin_path() -> Option { + use std::os::windows::io::AsRawHandle; + use windows_sys::Win32::Foundation::MAX_PATH; + use windows_sys::Win32::Storage::FileSystem::{FILE_NAME_OPENED, GetFinalPathNameByHandleW}; + + let handle = std::io::stdin().lock().as_raw_handle(); + if handle.is_null() { + return None; + } + + let mut buffer = [0u16; MAX_PATH as usize]; + let len = unsafe { + GetFinalPathNameByHandleW( + handle, + buffer.as_mut_ptr(), + buffer.len() as u32, + FILE_NAME_OPENED, + ) + } as usize; + + if len == 0 || len >= buffer.len() { + return None; + } + + String::from_utf16(&buffer[..len]).ok().map(PathBuf::from) +} + +#[cfg(not(windows))] +fn resolve_stdin_path() -> Option { + None +} + #[cfg(test)] mod tests { diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index f2875e7b1e3..2f59b67c17e 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -140,8 +140,6 @@ fn test_stdin_redirect_file() { } #[test] -// FIXME: the -f test fails with: Assertion failed. Expected 'tail' to be running but exited with status=exit status: 0 -#[ignore = "disabled until fixed"] #[cfg(not(target_vendor = "apple"))] // FIXME: for currently not working platforms fn test_stdin_redirect_file_follow() { // $ echo foo > f @@ -152,7 +150,7 @@ fn test_stdin_redirect_file_follow() { let (at, mut ucmd) = at_and_ucmd!(); - at.write("f", "foo"); + at.write("f", "foo\n"); let mut p = ucmd .arg("-f") @@ -160,10 +158,16 @@ fn test_stdin_redirect_file_follow() { .run_no_wait(); p.make_assertion_with_delay(500).is_alive(); + at.append("f", "bar\n"); + + p.make_assertion_with_delay(DEFAULT_SLEEP_INTERVAL_MILLIS) + .with_current_output() + .stdout_only("foo\nbar\n"); + p.kill() .make_assertion() .with_all_output() - .stdout_only("foo"); + .stdout_only("foo\nbar\n"); } #[test]