From ccbca85d67c30a1c88edbd1283fcd99f89ab657e Mon Sep 17 00:00:00 2001 From: mattsu Date: Tue, 23 Dec 2025 20:32:55 +0900 Subject: [PATCH 1/4] feat(tail): follow redirected stdin files Re-enable test_stdin_redirect_file_follow. --- src/uu/tail/src/follow/watch.rs | 44 ++++++++++- src/uu/tail/src/tail.rs | 135 ++++++++++++++++++++++++-------- tests/by-util/test_tail.rs | 2 - 3 files changed, 146 insertions(+), 35 deletions(-) diff --git a/src/uu/tail/src/follow/watch.rs b/src/uu/tail/src/follow/watch.rs index 06a355b2217..5cf48f197b4 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,15 @@ impl Observer { update_last: bool, ) -> UResult<()> { if self.follow.is_some() { - let path = if path.is_relative() { + #[cfg(unix)] + let path = if path.is_relative() && !path.is_stdin() { std::env::current_dir()?.join(path) } else { path.to_owned() }; + #[cfg(not(unix))] + let path = path.to_owned(); + let metadata = path.metadata().ok(); self.files.insert( &path, @@ -632,6 +658,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..4c063f8adcc 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,45 +277,83 @@ 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()); + if settings.follow.is_some() { + observer.add_path(path, input.display_name.as_str(), None, true)?; + } + 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") + ) + ); + 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") + ) ); - 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)?; } + } else { + let mut reader = BufReader::new(stdin()); + unbounded_tail(&mut reader, settings)?; + observer.add_stdin(input.display_name.as_str(), Some(Box::new(reader)), true)?; } Ok(()) @@ -586,6 +624,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..1ba42ddfedb 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 From ab579ed4e1fa74d13dd83668ff5d47b434e8d8b1 Mon Sep 17 00:00:00 2001 From: mattsu Date: Tue, 13 Jan 2026 21:07:27 +0900 Subject: [PATCH 2/4] fix(tail): remove unnecessary observer.add_stdin call in tail_stdin The removed line --- src/uu/tail/src/tail.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/uu/tail/src/tail.rs b/src/uu/tail/src/tail.rs index 4c063f8adcc..49146279317 100644 --- a/src/uu/tail/src/tail.rs +++ b/src/uu/tail/src/tail.rs @@ -353,7 +353,6 @@ fn tail_stdin( } else { let mut reader = BufReader::new(stdin()); unbounded_tail(&mut reader, settings)?; - observer.add_stdin(input.display_name.as_str(), Some(Box::new(reader)), true)?; } Ok(()) From 77c881990d654519286e60e4990a9bec407cee8a Mon Sep 17 00:00:00 2001 From: mattsu Date: Thu, 19 Feb 2026 17:14:15 +0900 Subject: [PATCH 3/4] Add Windows file system support and fix stdin redirection in tail utility - Added `Win32_Storage_FileSystem` feature to windows-sys dependency for proper file system operations on Windows - Fixed path handling in Observer to ensure consistent behavior across platforms - Removed redundant stdin path addition when following files - Updated test to use proper line endings and improved assertion timing for stdin redirection with file following --- src/uu/tail/Cargo.toml | 1 + src/uu/tail/src/follow/watch.rs | 3 --- src/uu/tail/src/tail.rs | 3 --- tests/by-util/test_tail.rs | 13 ++++++++----- 4 files changed, 9 insertions(+), 11 deletions(-) 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 5cf48f197b4..187cd88ff68 100644 --- a/src/uu/tail/src/follow/watch.rs +++ b/src/uu/tail/src/follow/watch.rs @@ -167,14 +167,11 @@ impl Observer { update_last: bool, ) -> UResult<()> { if self.follow.is_some() { - #[cfg(unix)] let path = if path.is_relative() && !path.is_stdin() { std::env::current_dir()?.join(path) } else { path.to_owned() }; - #[cfg(not(unix))] - let path = path.to_owned(); let metadata = path.metadata().ok(); self.files.insert( diff --git a/src/uu/tail/src/tail.rs b/src/uu/tail/src/tail.rs index 49146279317..6f3c6d92595 100644 --- a/src/uu/tail/src/tail.rs +++ b/src/uu/tail/src/tail.rs @@ -308,9 +308,6 @@ fn tail_stdin( observer.set_stdin_is_tailable(true); observer.set_stdin_key(path.clone()); - if settings.follow.is_some() { - observer.add_path(path, input.display_name.as_str(), None, true)?; - } return Ok(()); } diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index 1ba42ddfedb..967dce4488c 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -150,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") @@ -158,10 +158,13 @@ fn test_stdin_redirect_file_follow() { .run_no_wait(); p.make_assertion_with_delay(500).is_alive(); - p.kill() - .make_assertion() - .with_all_output() - .stdout_only("foo"); + 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\nbar\n"); } #[test] From 70f8cc879247bd8c33f70de350fe2a585bb5c98e Mon Sep 17 00:00:00 2001 From: mattsu Date: Thu, 19 Feb 2026 17:15:41 +0900 Subject: [PATCH 4/4] fix: improve readability of stdin redirect file follow test assertion --- tests/by-util/test_tail.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index 967dce4488c..2f59b67c17e 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -164,7 +164,10 @@ fn test_stdin_redirect_file_follow() { .with_current_output() .stdout_only("foo\nbar\n"); - p.kill().make_assertion().with_all_output().stdout_only("foo\nbar\n"); + p.kill() + .make_assertion() + .with_all_output() + .stdout_only("foo\nbar\n"); } #[test]