Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/uu/tail/locales/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ tail-status-has-been-replaced-following-new-file = { $file } has been replaced;
tail-status-file-truncated = { $file }: file truncated
tail-status-replaced-with-untailable-file = { $file } has been replaced with an untailable file
tail-status-replaced-with-untailable-file-giving-up = { $file } has been replaced with an untailable file; giving up on this name
tail-status-replaced-with-untailable-symlink = { $file } has been replaced with an untailable symbolic link
tail-status-file-became-inaccessible = { $file } { $become_inaccessible }: { $no_such_file }
tail-status-directory-containing-watched-file-removed = directory containing watched file was removed
tail-status-backend-cannot-be-used-reverting-to-polling = { $backend } cannot be used, reverting to polling
Expand Down
1 change: 1 addition & 0 deletions src/uu/tail/locales/fr-FR.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ tail-status-has-been-replaced-following-new-file = { $file } a été remplacé ;
tail-status-file-truncated = { $file } : fichier tronqué
tail-status-replaced-with-untailable-file = { $file } a été remplacé par un fichier non suivable
tail-status-replaced-with-untailable-file-giving-up = { $file } a été remplacé par un fichier non suivable ; abandon de ce nom
tail-status-replaced-with-untailable-symlink = { $file } a été remplacé par un lien symbolique non suivable
tail-status-file-became-inaccessible = { $file } { $become_inaccessible } : { $no_such_file }
tail-status-directory-containing-watched-file-removed = le répertoire contenant le fichier surveillé a été supprimé
tail-status-backend-cannot-be-used-reverting-to-polling = { $backend } ne peut pas être utilisé, retour au sondage
Expand Down
17 changes: 15 additions & 2 deletions src/uu/tail/src/follow/files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

use crate::args::Settings;
use crate::chunks::BytesChunkBuffer;
use crate::paths::{HeaderPrinter, PathExtTail};
use crate::paths::{HeaderPrinter, PathExtTail, path_is_symlink};
use crate::text;
use std::collections::HashMap;
use std::collections::hash_map::Keys;
Expand Down Expand Up @@ -134,6 +134,10 @@ impl FileHandling {
};
}

pub fn update_symlink(&mut self, path: &Path, is_symlink: bool) {
self.get_mut(path).is_symlink = is_symlink;
}

/// 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);
Expand Down Expand Up @@ -178,18 +182,21 @@ pub struct PathData {
pub reader: Option<Box<dyn BufRead>>,
pub metadata: Option<Metadata>,
pub display_name: String,
pub is_symlink: bool,
}

impl PathData {
pub fn new(
reader: Option<Box<dyn BufRead>>,
metadata: Option<Metadata>,
display_name: &str,
is_symlink: bool,
) -> Self {
Self {
reader,
metadata,
display_name: display_name.to_owned(),
is_symlink,
}
}
pub fn from_other_with_path(data: Self, path: &Path) -> Self {
Expand All @@ -205,7 +212,13 @@ impl PathData {
// Probably file was renamed/moved or removed again
None
};
let is_symlink = path_is_symlink(path);

Self::new(reader, path.metadata().ok(), data.display_name.as_str())
Self::new(
reader,
path.metadata().ok(),
data.display_name.as_str(),
is_symlink,
)
}
}
43 changes: 41 additions & 2 deletions src/uu/tail/src/follow/watch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

use crate::args::{FollowMode, Settings};
use crate::follow::files::{FileHandling, PathData};
use crate::paths::{Input, InputKind, MetadataExtTail, PathExtTail};
use crate::paths::{Input, InputKind, MetadataExtTail, PathExtTail, path_is_symlink};
use crate::{platform, text};
use notify::{RecommendedWatcher, RecursiveMode, Watcher, WatcherKind};
use std::io::BufRead;
Expand Down Expand Up @@ -151,9 +151,10 @@ impl Observer {
path.to_owned()
};
let metadata = path.metadata().ok();
let is_symlink = path_is_symlink(&path);
self.files.insert(
&path,
PathData::new(reader, metadata, display_name),
PathData::new(reader, metadata, display_name, is_symlink),
update_last,
);
}
Expand Down Expand Up @@ -304,8 +305,29 @@ impl Observer {
EventKind::Modify(ModifyKind::Metadata(MetadataKind::Any | MetadataKind::WriteTime) | ModifyKind::Data(DataChange::Any) | ModifyKind::Name(RenameMode::To)) |
EventKind::Create(CreateKind::File | CreateKind::Folder | CreateKind::Any) => {
if let Ok(new_md) = event_path.metadata() {
let new_is_symlink = path_is_symlink(event_path);
let is_tailable = new_md.is_tailable();
let pd = self.files.get(event_path);

if self.follow_name() && !pd.is_symlink && new_is_symlink {
// GNU tail treats a path that turns into a symlink as untailable when
// following by name, to avoid following the symlink target.
if pd.reader.is_some() {
Comment on lines 312 to 315
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add comments to describe why we could be here

self.files.reset_reader(event_path);
}
show_error!(
"{}",
translate!(
"tail-status-replaced-with-untailable-symlink",
"file" => display_name.quote()
)
);
self.files
.update_metadata(event_path, event_path.symlink_metadata().ok());
self.files.update_symlink(event_path, true);
return Ok(paths);
}

if let Some(old_md) = &pd.metadata {
if is_tailable {
// We resume tracking from the start of the file,
Expand Down Expand Up @@ -374,6 +396,7 @@ impl Observer {
}
}
self.files.update_metadata(event_path, Some(new_md));
self.files.update_symlink(event_path, new_is_symlink);
}
}
EventKind::Remove(RemoveKind::File | RemoveKind::Any)
Expand Down Expand Up @@ -497,12 +520,28 @@ pub fn follow(mut observer: Observer, settings: &Settings) -> UResult<()> {
if new_path.exists() {
let pd = observer.files.get(new_path);
let md = new_path.metadata().unwrap();
let new_is_symlink = path_is_symlink(new_path);
if !pd.is_symlink && new_is_symlink {
show_error!(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see that it is duplicated from above

"{}",
translate!(
"tail-status-replaced-with-untailable-symlink",
"file" => pd.display_name.quote()
)
);
observer
.files
.update_metadata(new_path, new_path.symlink_metadata().ok());
observer.files.update_symlink(new_path, true);
continue;
}
if md.is_tailable() && pd.reader.is_none() {
show_error!(
"{}",
translate!("tail-status-has-appeared-following-new-file", "file" => pd.display_name.quote())
);
observer.files.update_metadata(new_path, Some(md));
observer.files.update_symlink(new_path, new_is_symlink);
observer.files.update_reader(new_path)?;
_read_some = observer.files.tail_file(new_path, settings.verbose)?;
observer
Expand Down
5 changes: 5 additions & 0 deletions src/uu/tail/src/paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,11 @@ pub fn path_is_tailable(path: &Path) -> bool {
path.is_file() || path.exists() && path.metadata().is_ok_and(|meta| meta.is_tailable())
}

pub fn path_is_symlink(path: &Path) -> bool {
path.symlink_metadata()
.is_ok_and(|meta| meta.file_type().is_symlink())
}

#[inline]
#[cfg(unix)]
pub fn stdin_is_bad_fd() -> bool {
Expand Down
39 changes: 37 additions & 2 deletions tests/by-util/test_tail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3684,17 +3684,52 @@ fn test_when_argument_file_is_a_symlink() {
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\n";
ts.ucmd()
.arg("dir_link")
.fails_with_code(1)
.stderr_only(expected);
}

// TODO: make this work on windows
#[test]
#[cfg(unix)]
fn test_follow_name_replaced_with_symlink() {
let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;
let file = "testfile";
let target = "target";

at.write(file, "original\n");
at.write(target, "target\n");

let mut child = ts
.ucmd()
.args(&[
"--follow=name",
"-n",
"0",
"--sleep-interval=0.1",
"--use-polling",
file,
])
.run_no_wait();

child.delay(500);
at.remove(file);
at.symlink_file(target, file);
child.delay(500);

child
.kill()
.make_assertion()
.with_all_output()
.stderr_contains("tail: 'testfile' has been replaced with an untailable symbolic link")
.stdout_is("");
}

// TODO: make this work on windows
#[test]
#[cfg(unix)]
Expand Down
Loading