tail: reduce CPU load for polling (#3618)

* tail: reduce CPU load for polling

This reduces the CPU load for polling drastically (from ~80% down to ~5%)
by removing/fixing several previous workarounds related to polling,
while still passing all related GNU test-suite checks.
* set Notify::PollWatcher delay to: sleep_sec/10 instead of
  sleep_sec/100
* set recv_timeout to sleep_sec instead of sleep_sec/100
* remove the manual polling of watched files

Bugs:
* fix an issue with headers to consistently pass
"test_follow_name_retry_headers" and "gnu/tests/tail-2/overlay-headers.sh"

Code clean-up and refactor
* make fields of struct FileHandling private (and add getters/setters)
to ensure that the paths are absolute and match the paths returned by
Notify::Events
* replace calls to "crash!" with "return USimpleError"
* clean-up formatting
This commit is contained in:
Jan Scheer 2022-06-21 22:21:19 +02:00 committed by GitHub
parent c277e933c9
commit 75edeea5e4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 848 additions and 517 deletions

View file

@ -18,7 +18,7 @@ path = "src/tail.rs"
[dependencies]
clap = { version = "3.1", features = ["wrap_help", "cargo"] }
libc = "0.2.126"
notify = { version = "5.0.0-pre.15", features=["macos_kqueue"]}
notify = { version = "=5.0.0-pre.15", features=["macos_kqueue"]}
uucore = { version=">=0.0.11", package="uucore", path="../../uucore", features=["ringbuffer", "lines"] }
[target.'cfg(windows)'.dependencies]

View file

@ -5,7 +5,6 @@
## Missing features
* `--max-unchanged-stats`
* check whether process p is alive at least every number of seconds (relevant for `--pid`)
Note:
There's a stub for `--max-unchanged-stats` so GNU test-suite checks using it can run, however this flag has no functionality yet.

File diff suppressed because it is too large Load diff

View file

@ -89,6 +89,7 @@ fn test_stdin_redirect_file() {
.arg("-f")
.set_stdin(std::fs::File::open(at.plus("f")).unwrap())
.run_no_wait();
sleep(Duration::from_millis(500));
p.kill().unwrap();
@ -249,6 +250,7 @@ fn test_follow_stdin_descriptor() {
for _ in 0..2 {
let mut p = ts.ucmd().args(&args).run_no_wait();
sleep(Duration::from_millis(500));
p.kill().unwrap();
let (buf_stdout, buf_stderr) = take_stdout_stderr(&mut p);
@ -292,6 +294,7 @@ fn test_follow_stdin_explicit_indefinitely() {
.set_stdin(Stdio::null())
.args(&["-f", "-", "/dev/null"])
.run_no_wait();
sleep(Duration::from_millis(500));
p.kill().unwrap();
@ -379,6 +382,7 @@ fn test_null_default() {
}
#[test]
#[cfg(unix)]
fn test_follow_single() {
let (at, mut ucmd) = at_and_ucmd!();
@ -402,6 +406,7 @@ fn test_follow_single() {
/// Test for following when bytes are written that are not valid UTF-8.
#[test]
#[cfg(unix)]
fn test_follow_non_utf8_bytes() {
// Tail the test file and start following it.
let (at, mut ucmd) = at_and_ucmd!();
@ -433,6 +438,7 @@ fn test_follow_non_utf8_bytes() {
}
#[test]
#[cfg(unix)]
fn test_follow_multiple() {
let (at, mut ucmd) = at_and_ucmd!();
let mut child = ucmd
@ -498,10 +504,10 @@ fn test_follow_multiple_untailable() {
let expected_stdout = "==> DIR1 <==\n\n==> DIR2 <==\n";
let expected_stderr = "tail: error reading 'DIR1': Is a directory\n\
tail: DIR1: cannot follow end of this type of file; giving up on this name\n\
tail: error reading 'DIR2': Is a directory\n\
tail: DIR2: cannot follow end of this type of file; giving up on this name\n\
tail: no files remaining\n";
tail: DIR1: cannot follow end of this type of file; giving up on this name\n\
tail: error reading 'DIR2': Is a directory\n\
tail: DIR2: cannot follow end of this type of file; giving up on this name\n\
tail: no files remaining\n";
let (at, mut ucmd) = at_and_ucmd!();
at.mkdir("DIR1");
@ -527,6 +533,30 @@ fn test_follow_stdin_pipe() {
.no_stderr();
}
#[test]
#[cfg(unix)]
fn test_follow_invalid_pid() {
new_ucmd!()
.args(&["-f", "--pid=-1234"])
.fails()
.no_stdout()
.stderr_is("tail: invalid PID: '-1234'\n");
new_ucmd!()
.args(&["-f", "--pid=abc"])
.fails()
.no_stdout()
.stderr_is("tail: invalid PID: 'abc': invalid digit found in string\n");
let max_pid = (i32::MAX as i64 + 1).to_string();
new_ucmd!()
.args(&["-f", "--pid", &max_pid])
.fails()
.no_stdout()
.stderr_is(format!(
"tail: invalid PID: '{}': number too large to fit in target type\n",
max_pid
));
}
// FixME: test PASSES for usual windows builds, but fails for coverage testing builds (likely related to the specific RUSTFLAGS '-Zpanic_abort_tests -Cpanic=abort') This test also breaks tty settings under bash requiring a 'stty sane' or reset. // spell-checker:disable-line
#[cfg(disable_until_fixed)]
#[test]
@ -721,7 +751,7 @@ fn test_multiple_input_files_missing() {
.stdout_is_fixture("foobar_follow_multiple.expected")
.stderr_is(
"tail: cannot open 'missing1' for reading: No such file or directory\n\
tail: cannot open 'missing2' for reading: No such file or directory",
tail: cannot open 'missing2' for reading: No such file or directory",
)
.code_is(1);
}
@ -740,7 +770,7 @@ fn test_follow_missing() {
.no_stdout()
.stderr_is(
"tail: cannot open 'missing' for reading: No such file or directory\n\
tail: no files remaining",
tail: no files remaining",
)
.code_is(1);
}
@ -813,8 +843,8 @@ fn test_dir_follow() {
.no_stdout()
.stderr_is(
"tail: error reading 'DIR': Is a directory\n\
tail: DIR: cannot follow end of this type of file; giving up on this name\n\
tail: no files remaining\n",
tail: DIR: cannot follow end of this type of file; giving up on this name\n\
tail: no files remaining\n",
)
.code_is(1);
}
@ -833,9 +863,9 @@ fn test_dir_follow_retry() {
.run()
.stderr_is(
"tail: warning: --retry only effective for the initial open\n\
tail: error reading 'DIR': Is a directory\n\
tail: DIR: cannot follow end of this type of file\n\
tail: no files remaining\n",
tail: error reading 'DIR': Is a directory\n\
tail: DIR: cannot follow end of this type of file\n\
tail: no files remaining\n",
)
.code_is(1);
}
@ -1129,18 +1159,20 @@ fn test_retry3() {
let missing = "missing";
let expected_stderr = "tail: cannot open 'missing' for reading: No such file or directory\n\
tail: 'missing' has appeared; following new file\n";
tail: 'missing' has appeared; following new file\n";
let expected_stdout = "X\n";
let delay = 1000;
let mut delay = 1500;
let mut args = vec!["--follow=name", "--retry", missing, "--use-polling"];
for _ in 0..2 {
let mut p = ts.ucmd().set_stdin(Stdio::null()).args(&args).run_no_wait();
sleep(Duration::from_millis(delay));
at.touch(missing);
sleep(Duration::from_millis(delay));
at.truncate(missing, "X\n");
sleep(Duration::from_millis(2 * delay));
sleep(Duration::from_millis(delay));
p.kill().unwrap();
@ -1150,6 +1182,7 @@ fn test_retry3() {
at.remove(missing);
args.pop();
delay /= 3;
}
}
@ -1165,11 +1198,10 @@ fn test_retry4() {
let missing = "missing";
let expected_stderr = "tail: warning: --retry only effective for the initial open\n\
tail: cannot open 'missing' for reading: No such file or directory\n\
tail: 'missing' has appeared; following new file\n\
tail: missing: file truncated\n";
tail: cannot open 'missing' for reading: No such file or directory\n\
tail: 'missing' has appeared; following new file\n\
tail: missing: file truncated\n";
let expected_stdout = "X1\nX\n";
let delay = 1000;
let mut args = vec![
"-s.1",
"--max-unchanged-stats=1",
@ -1178,14 +1210,17 @@ fn test_retry4() {
missing,
"---disable-inotify",
];
let mut delay = 1500;
for _ in 0..2 {
let mut p = ts.ucmd().set_stdin(Stdio::null()).args(&args).run_no_wait();
sleep(Duration::from_millis(delay));
at.touch(missing);
sleep(Duration::from_millis(delay));
at.truncate(missing, "X1\n");
sleep(Duration::from_millis(delay));
at.truncate(missing, "X\n");
sleep(Duration::from_millis(delay));
@ -1197,6 +1232,7 @@ fn test_retry4() {
at.remove(missing);
args.pop();
delay /= 3;
}
}
@ -1211,15 +1247,16 @@ fn test_retry5() {
let missing = "missing";
let expected_stderr = "tail: warning: --retry only effective for the initial open\n\
tail: cannot open 'missing' for reading: No such file or directory\n\
tail: 'missing' has been replaced with an untailable file; giving up on this name\n\
tail: no files remaining\n";
let delay = 1000;
tail: cannot open 'missing' for reading: No such file or directory\n\
tail: 'missing' has been replaced with an untailable file; giving up on this name\n\
tail: no files remaining\n";
let mut delay = 1500;
let mut args = vec!["--follow=descriptor", "--retry", missing, "--use-polling"];
for _ in 0..2 {
let mut p = ts.ucmd().set_stdin(Stdio::null()).args(&args).run_no_wait();
sleep(Duration::from_millis(delay));
at.mkdir(missing);
sleep(Duration::from_millis(delay));
@ -1231,6 +1268,7 @@ fn test_retry5() {
at.rmdir(missing);
args.pop();
delay /= 3;
}
}
@ -1283,15 +1321,13 @@ fn test_retry7() {
let untailable = "untailable";
let expected_stderr = "tail: error reading 'untailable': Is a directory\n\
tail: untailable: cannot follow end of this type of file\n\
tail: 'untailable' has become accessible\n\
tail: 'untailable' has become inaccessible: No such file or directory\n\
tail: 'untailable' has been replaced with an untailable file\n\
tail: 'untailable' has become accessible\n";
tail: untailable: cannot follow end of this type of file\n\
tail: 'untailable' has become accessible\n\
tail: 'untailable' has become inaccessible: No such file or directory\n\
tail: 'untailable' has been replaced with an untailable file\n\
tail: 'untailable' has become accessible\n";
let expected_stdout = "foo\nbar\n";
let delay = 1000;
let mut args = vec![
"-s.1",
"--max-unchanged-stats=1",
@ -1299,8 +1335,11 @@ fn test_retry7() {
untailable,
"--use-polling",
];
let mut delay = 1500;
for _ in 0..2 {
at.mkdir(untailable);
let mut p = ts.ucmd().set_stdin(Stdio::null()).args(&args).run_no_wait();
sleep(Duration::from_millis(delay));
@ -1334,7 +1373,7 @@ fn test_retry7() {
args.pop();
at.remove(untailable);
sleep(Duration::from_millis(delay));
delay /= 3;
}
}
@ -1417,14 +1456,14 @@ fn test_retry9() {
let expected_stderr = format!(
"\
tail: 'parent_dir/watched_file' has become inaccessible: No such file or directory\n\
tail: directory containing watched file was removed\n\
tail: {} cannot be used, reverting to polling\n\
tail: 'parent_dir/watched_file' has appeared; following new file\n\
tail: 'parent_dir/watched_file' has become inaccessible: No such file or directory\n\
tail: 'parent_dir/watched_file' has appeared; following new file\n\
tail: 'parent_dir/watched_file' has become inaccessible: No such file or directory\n\
tail: 'parent_dir/watched_file' has appeared; following new file\n",
tail: 'parent_dir/watched_file' has become inaccessible: No such file or directory\n\
tail: directory containing watched file was removed\n\
tail: {} cannot be used, reverting to polling\n\
tail: 'parent_dir/watched_file' has appeared; following new file\n\
tail: 'parent_dir/watched_file' has become inaccessible: No such file or directory\n\
tail: 'parent_dir/watched_file' has appeared; following new file\n\
tail: 'parent_dir/watched_file' has become inaccessible: No such file or directory\n\
tail: 'parent_dir/watched_file' has appeared; following new file\n",
BACKEND
);
let expected_stdout = "foo\nbar\nfoo\nbar\n";
@ -1469,7 +1508,6 @@ fn test_retry9() {
sleep(Duration::from_millis(delay));
p.kill().unwrap();
sleep(Duration::from_millis(delay));
let (buf_stdout, buf_stderr) = take_stdout_stderr(&mut p);
assert_eq!(buf_stdout, expected_stdout);
@ -1500,7 +1538,7 @@ fn test_follow_descriptor_vs_rename1() {
"---disable-inotify",
];
let delay = 500;
let mut delay = 1500;
for _ in 0..2 {
at.touch(file_a);
@ -1523,13 +1561,13 @@ fn test_follow_descriptor_vs_rename1() {
sleep(Duration::from_millis(delay));
p.kill().unwrap();
sleep(Duration::from_millis(delay));
let (buf_stdout, buf_stderr) = take_stdout_stderr(&mut p);
assert_eq!(buf_stdout, "A\nB\nC\n");
assert!(buf_stderr.is_empty());
args.pop();
delay /= 3;
}
}
@ -1555,18 +1593,20 @@ fn test_follow_descriptor_vs_rename2() {
"---disable-inotify",
];
let delay = 100;
let mut delay = 1500;
for _ in 0..2 {
at.touch(file_a);
at.touch(file_b);
let mut p = ts.ucmd().set_stdin(Stdio::null()).args(&args).run_no_wait();
sleep(Duration::from_millis(delay));
at.rename(file_a, file_c);
sleep(Duration::from_millis(1000));
sleep(Duration::from_millis(delay));
at.append(file_c, "X\n");
sleep(Duration::from_millis(delay));
p.kill().unwrap();
sleep(Duration::from_millis(delay));
let (buf_stdout, buf_stderr) = take_stdout_stderr(&mut p);
assert_eq!(
@ -1576,6 +1616,68 @@ fn test_follow_descriptor_vs_rename2() {
assert!(buf_stderr.is_empty());
args.pop();
delay /= 3;
}
}
#[test]
#[cfg(target_os = "linux")]
fn test_follow_name_retry_headers() {
// inspired by: "gnu/tests/tail-2/F-headers.sh"
// Ensure tail -F distinguishes output with the
// correct headers for created/renamed files
/*
$ tail --follow=descriptor -s.1 --max-unchanged-stats=1 -F a b
tail: cannot open 'a' for reading: No such file or directory
tail: cannot open 'b' for reading: No such file or directory
tail: 'a' has appeared; following new file
==> a <==
x
tail: 'b' has appeared; following new file
==> b <==
y
*/
let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;
let file_a = "a";
let file_b = "b";
let mut args = vec![
"-F",
"-s.1",
"--max-unchanged-stats=1",
file_a,
file_b,
"---disable-inotify",
];
let mut delay = 1500;
for _ in 0..2 {
let mut p = ts.ucmd().set_stdin(Stdio::null()).args(&args).run_no_wait();
sleep(Duration::from_millis(delay));
at.truncate(file_a, "x\n");
sleep(Duration::from_millis(delay));
at.truncate(file_b, "y\n");
sleep(Duration::from_millis(delay));
p.kill().unwrap();
let (buf_stdout, buf_stderr) = take_stdout_stderr(&mut p);
assert_eq!(buf_stdout, "\n==> a <==\nx\n\n==> b <==\ny\n");
assert_eq!(
buf_stderr,
"tail: cannot open 'a' for reading: No such file or directory\n\
tail: cannot open 'b' for reading: No such file or directory\n\
tail: 'a' has appeared; following new file\n\
tail: 'b' has appeared; following new file\n"
);
at.remove(file_a);
at.remove(file_b);
args.pop();
delay /= 3;
}
}
@ -1604,15 +1706,16 @@ fn test_follow_name_remove() {
),
];
let delay = 2000;
let mut args = vec!["--follow=name", source_copy, "--use-polling"];
let mut delay = 1500;
#[allow(clippy::needless_range_loop)]
for i in 0..2 {
at.copy(source, source_copy);
let mut p = ts.ucmd().set_stdin(Stdio::null()).args(&args).run_no_wait();
let mut p = ts.ucmd().set_stdin(Stdio::null()).args(&args).run_no_wait();
sleep(Duration::from_millis(delay));
at.remove(source_copy);
sleep(Duration::from_millis(delay));
@ -1623,6 +1726,7 @@ fn test_follow_name_remove() {
assert_eq!(buf_stderr, expected_stderr[i]);
args.pop();
delay /= 3;
}
}
@ -1661,7 +1765,7 @@ fn test_follow_name_truncate1() {
}
#[test]
#[cfg(unix)]
#[cfg(target_os = "linux")] // FIXME: fix this test for BSD/macOS
fn test_follow_name_truncate2() {
// This test triggers a truncate event while `tail --follow=name file` is running.
// $ ((sleep 1 && echo -n "x\nx\nx\n" >> file && sleep 1 && \
@ -1738,18 +1842,17 @@ fn test_follow_name_truncate4() {
let mut args = vec!["-s.1", "--max-unchanged-stats=1", "-F", "file"];
let delay = 300;
let mut delay = 500;
for _ in 0..2 {
at.append("file", "foobar\n");
let mut p = ts.ucmd().set_stdin(Stdio::null()).args(&args).run_no_wait();
sleep(Duration::from_millis(100));
sleep(Duration::from_millis(delay));
at.truncate("file", "foobar\n");
sleep(Duration::from_millis(delay));
p.kill().unwrap();
sleep(Duration::from_millis(delay));
let (buf_stdout, buf_stderr) = take_stdout_stderr(&mut p);
assert!(buf_stderr.is_empty());
@ -1757,17 +1860,24 @@ fn test_follow_name_truncate4() {
at.remove("file");
args.push("---disable-inotify");
delay *= 3;
}
}
#[test]
#[cfg(all(unix, not(target_os = "android")))] // NOTE: Should work on Android but CI VM is too slow.
#[cfg(all(unix, not(target_os = "android")))]
fn test_follow_truncate_fast() {
// inspired by: "gnu/tests/tail-2/truncate.sh"
// Ensure all logs are output upon file truncation
// This is similar to `test_follow_name_truncate1-3` but uses very short delays
// to better mimic the tight timings used in the "truncate.sh" test.
// This is here to test for "speed" only, all the logic is already covered by other tests.
if is_ci() {
println!("TEST SKIPPED (too fast for CI)");
return;
}
let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;
@ -1775,7 +1885,7 @@ fn test_follow_truncate_fast() {
let mut args = vec!["-s.1", "--max-unchanged-stats=1", "f", "---disable-inotify"];
let follow = vec!["-f", "-F"];
let delay = 150;
let mut delay = 1000;
for _ in 0..2 {
for mode in &follow {
args.push(mode);
@ -1789,7 +1899,6 @@ fn test_follow_truncate_fast() {
sleep(Duration::from_millis(delay));
p.kill().unwrap();
sleep(Duration::from_millis(delay));
let (buf_stdout, buf_stderr) = take_stdout_stderr(&mut p);
assert_eq!(
@ -1801,12 +1910,13 @@ fn test_follow_truncate_fast() {
args.pop();
}
args.pop();
delay = 250;
}
}
#[test]
#[cfg(all(unix, not(any(target_os = "android", target_vendor = "apple"))))] // FIXME: make this work not just on Linux
fn test_follow_name_move_create() {
fn test_follow_name_move_create1() {
// This test triggers a move/create event while `tail --follow=name file` is running.
// ((sleep 2 && mv file backup && sleep 2 && cp backup file &)>/dev/null 2>&1 &) ; tail --follow=name file
@ -1830,14 +1940,15 @@ fn test_follow_name_move_create() {
#[cfg(not(target_os = "linux"))]
let expected_stderr = format!("{}: {}: No such file or directory\n", ts.util_name, source);
let delay = 500;
let args = ["--follow=name", source];
let mut p = ts.ucmd().set_stdin(Stdio::null()).args(&args).run_no_wait();
let delay = 2000;
sleep(Duration::from_millis(delay));
at.rename(source, backup);
sleep(Duration::from_millis(delay));
at.copy(backup, source);
sleep(Duration::from_millis(delay));
@ -1892,13 +2003,12 @@ fn test_follow_name_move_create2() {
sleep(Duration::from_millis(delay));
p.kill().unwrap();
sleep(Duration::from_millis(delay));
let (buf_stdout, buf_stderr) = take_stdout_stderr(&mut p);
assert_eq!(
buf_stderr,
"tail: '1' has become inaccessible: No such file or directory\n\
tail: '1' has appeared; following new file\n"
tail: '1' has appeared; following new file\n"
);
// NOTE: Because "gnu/tests/tail-2/inotify-hash-abuse.sh" 'forgets' to clear the files used
@ -1916,15 +2026,16 @@ fn test_follow_name_move_create2() {
at.remove("f");
args.push("---disable-inotify");
delay = 2000;
delay *= 3;
}
}
#[test]
#[cfg(all(unix, not(any(target_os = "android", target_vendor = "apple"))))] // FIXME: make this work not just on Linux
fn test_follow_name_move() {
fn test_follow_name_move1() {
// This test triggers a move event while `tail --follow=name file` is running.
// ((sleep 2 && mv file backup &)>/dev/null 2>&1 &) ; tail --follow=name file
// NOTE: For `---disable-inotify` tail exits with "no file remaining", it stays open w/o it.
let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;
@ -1934,22 +2045,23 @@ fn test_follow_name_move() {
let expected_stdout = at.read(FOLLOW_NAME_SHORT_EXP);
let expected_stderr = [
format!("{}: {}: No such file or directory\n", ts.util_name, source),
format!(
"{}: {}: No such file or directory\n{0}: no files remaining\n",
ts.util_name, source
),
format!("{}: {}: No such file or directory\n", ts.util_name, source),
];
let mut args = vec!["--follow=name", source, "--use-polling"];
let mut args = vec!["--follow=name", source];
let mut delay = 500;
#[allow(clippy::needless_range_loop)]
for i in 0..2 {
let mut p = ts.ucmd().set_stdin(Stdio::null()).args(&args).run_no_wait();
sleep(Duration::from_millis(delay));
sleep(Duration::from_millis(2000));
at.rename(source, backup);
sleep(Duration::from_millis(5000));
sleep(Duration::from_millis(delay));
p.kill().unwrap();
@ -1958,14 +2070,15 @@ fn test_follow_name_move() {
assert_eq!(buf_stderr, expected_stderr[i]);
at.rename(backup, source);
args.pop();
args.push("--use-polling");
delay *= 3;
}
}
#[test]
#[cfg(all(unix, not(any(target_os = "android", target_vendor = "apple"))))] // FIXME: make this work not just on Linux
fn test_follow_name_move2() {
// Like test_follow_name_move, but move to a name that's already monitored.
// Like test_follow_name_move1, but move to a name that's already monitored.
// $ echo file1_content > file1; echo file2_content > file2; \
// ((sleep 2 ; mv file1 file2 ; sleep 1 ; echo "more_file2_content" >> file2 ; sleep 1 ; \
@ -1993,48 +2106,60 @@ fn test_follow_name_move2() {
let expected_stdout = format!(
"==> {0} <==\n{0}_content\n\n==> {1} <==\n{1}_content\n{0}_content\n\
more_{1}_content\n\n==> {0} <==\nmore_{0}_content\n",
more_{1}_content\n\n==> {0} <==\nmore_{0}_content\n",
file1, file2
);
let expected_stderr = format!(
let mut expected_stderr = format!(
"{0}: {1}: No such file or directory\n\
{0}: '{2}' has been replaced; following new file\n\
{0}: '{1}' has appeared; following new file\n",
{0}: '{2}' has been replaced; following new file\n\
{0}: '{1}' has appeared; following new file\n",
ts.util_name, file1, file2
);
at.append(file1, "file1_content\n");
at.append(file2, "file2_content\n");
let mut args = vec!["--follow=name", file1, file2];
// TODO: [2021-05; jhscheer] fix this for `--use-polling`
let mut args = vec!["--follow=name", file1, file2 /*, "--use-polling" */];
let mut delay = 500;
for _ in 0..2 {
at.truncate(file1, "file1_content\n");
at.truncate(file2, "file2_content\n");
#[allow(clippy::needless_range_loop)]
for _ in 0..1 {
let mut p = ts.ucmd().set_stdin(Stdio::null()).args(&args).run_no_wait();
sleep(Duration::from_millis(delay));
sleep(Duration::from_millis(1000));
at.rename(file1, file2);
sleep(Duration::from_millis(1000));
sleep(Duration::from_millis(delay));
at.append(file2, "more_file2_content\n");
sleep(Duration::from_millis(1000));
sleep(Duration::from_millis(delay));
at.append(file1, "more_file1_content\n");
sleep(Duration::from_millis(1000));
sleep(Duration::from_millis(delay));
p.kill().unwrap();
let (buf_stdout, buf_stderr) = take_stdout_stderr(&mut p);
println!("out:\n{}\nerr:\n{}", buf_stdout, buf_stderr);
assert_eq!(buf_stdout, expected_stdout);
assert_eq!(buf_stderr, expected_stderr);
args.pop();
args.push("--use-polling");
delay *= 3;
// NOTE: Switch the first and second line because the events come in this order from
// `notify::PollWatcher`. However, for GNU's tail, the order between polling and not
// polling does not change.
expected_stderr = format!(
"{0}: '{2}' has been replaced; following new file\n\
{0}: {1}: No such file or directory\n\
{0}: '{1}' has appeared; following new file\n",
ts.util_name, file1, file2
);
}
}
#[test]
#[cfg(all(unix, not(any(target_os = "android", target_vendor = "apple"))))] // FIXME: make this work not just on Linux
fn test_follow_name_move_retry() {
// Similar to test_follow_name_move but with `--retry` (`-F`)
fn test_follow_name_move_retry1() {
// Similar to test_follow_name_move1 but with `--retry` (`-F`)
// This test triggers two move/rename events while `tail --follow=name --retry file` is running.
let ts = TestScenario::new(util_name!());
@ -2045,41 +2170,138 @@ fn test_follow_name_move_retry() {
let expected_stderr = format!(
"{0}: '{1}' has become inaccessible: No such file or directory\n\
{0}: '{1}' has appeared; following new file\n",
{0}: '{1}' has appeared; following new file\n",
ts.util_name, source
);
let expected_stdout = "tailed\nnew content\n";
let mut args = vec!["--follow=name", "--retry", source, "--use-polling"];
let mut delay = 1500;
for _ in 0..2 {
at.touch(source);
let mut p = ts.ucmd().set_stdin(Stdio::null()).args(&args).run_no_wait();
sleep(Duration::from_millis(delay));
sleep(Duration::from_millis(1000));
at.append(source, "tailed\n");
sleep(Duration::from_millis(delay));
sleep(Duration::from_millis(2000));
// with --follow=name, tail should stop monitoring the renamed file
at.rename(source, backup);
sleep(Duration::from_millis(4000));
sleep(Duration::from_millis(delay));
// overwrite backup while it's not monitored
at.truncate(backup, "new content\n");
sleep(Duration::from_millis(500));
sleep(Duration::from_millis(delay));
// move back, tail should pick this up and print new content
at.rename(backup, source);
sleep(Duration::from_millis(4000));
sleep(Duration::from_millis(delay));
p.kill().unwrap();
let (buf_stdout, buf_stderr) = take_stdout_stderr(&mut p);
dbg!(&buf_stdout, &buf_stderr);
assert_eq!(buf_stdout, expected_stdout);
assert_eq!(buf_stderr, expected_stderr);
at.remove(source);
args.pop();
delay /= 3;
}
}
#[test]
#[cfg(all(unix, not(any(target_os = "android", target_vendor = "apple"))))] // FIXME: make this work not just on Linux
fn test_follow_name_move_retry2() {
// inspired by: "gnu/tests/tail-2/F-vs-rename.sh"
// Similar to test_follow_name_move2 (move to a name that's already monitored)
// but with `--retry` (`-F`)
/*
$ touch a b
$ ((sleep 1; echo x > a; mv a b; echo x2 > a; echo y >> b; echo z >> a &)>/dev/null 2>&1 &) ; tail -F a b
==> a <==
==> b <==
==> a <==
x
tail: 'a' has become inaccessible: No such file or directory
tail: 'b' has been replaced; following new file
==> b <==
x
tail: 'a' has appeared; following new file
==> a <==
x2
==> b <==
y
==> a <==
z
*/
let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;
let file1 = "a";
let file2 = "b";
let expected_stdout = format!(
"==> {0} <==\n\n==> {1} <==\n\n==> {0} <==\nx\n\n==> {1} <==\
\nx\n\n==> {0} <==\nx2\n\n==> {1} <==\ny\n\n==> {0} <==\nz\n",
file1, file2
);
let mut expected_stderr = format!(
"{0}: '{1}' has become inaccessible: No such file or directory\n\
{0}: '{2}' has been replaced; following new file\n\
{0}: '{1}' has appeared; following new file\n",
ts.util_name, file1, file2
);
let mut args = vec!["-s.1", "--max-unchanged-stats=1", "-F", file1, file2];
let mut delay = 500;
for _ in 0..2 {
at.touch(file1);
at.touch(file2);
let mut p = ts.ucmd().set_stdin(Stdio::null()).args(&args).run_no_wait();
sleep(Duration::from_millis(delay));
at.truncate(file1, "x\n");
sleep(Duration::from_millis(delay));
at.rename(file1, file2);
sleep(Duration::from_millis(delay));
at.truncate(file1, "x2\n");
sleep(Duration::from_millis(delay));
at.append(file2, "y\n");
sleep(Duration::from_millis(delay));
at.append(file1, "z\n");
sleep(Duration::from_millis(delay));
p.kill().unwrap();
let (buf_stdout, buf_stderr) = take_stdout_stderr(&mut p);
assert_eq!(buf_stdout, expected_stdout);
assert_eq!(buf_stderr, expected_stderr);
at.remove(file1);
at.remove(file2);
args.push("--use-polling");
delay *= 3;
// NOTE: Switch the first and second line because the events come in this order from
// `notify::PollWatcher`. However, for GNU's tail, the order between polling and not
// polling does not change.
expected_stderr = format!(
"{0}: '{2}' has been replaced; following new file\n\
{0}: '{1}' has become inaccessible: No such file or directory\n\
{0}: '{1}' has appeared; following new file\n",
ts.util_name, file1, file2
);
}
}
@ -2211,7 +2433,7 @@ fn test_illegal_seek() {
assert_eq!(
buf_stderr,
"tail: 'FILE' has been replaced; following new file\n\
tail: FILE: cannot seek to offset 0: Illegal seek\n"
tail: FILE: cannot seek to offset 0: Illegal seek\n"
);
assert_eq!(p.wait().unwrap().code().unwrap(), 1);
}

View file

@ -164,6 +164,14 @@ sed -i -e "s|rm: cannot remove 'a/1'|rm: cannot remove 'a'|g" tests/rm/rm2.sh
sed -i -e "s|removed directory 'a/'|removed directory 'a'|g" tests/rm/v-slash.sh
# overlay-headers.sh test intends to check for inotify events,
# however there's a bug because `---dis` is an alias for: `---disable-inotify`
sed -i -e "s|---dis ||g" tests/tail-2/overlay-headers.sh
# F-headers.sh test sometime fails (but only in CI),
# just testing inotify should make it more stable
sed -i -e "s| '---disable-inotify'||g" tests/tail-2/F-headers.sh
test -f "${UU_BUILD_DIR}/getlimits" || cp src/getlimits "${UU_BUILD_DIR}"
# When decoding an invalid base32/64 string, gnu writes everything it was able to decode until
@ -200,6 +208,7 @@ sed -i -e "s/provoked error./provoked error\ncat pat |sort -u > pat/" tests/misc
sed -i -e "s/ln: 'f' and 'f' are the same file/ln: failed to link 'f' to 'f': Same file/g" tests/ln/hard-backup.sh
sed -i -e "s/failed to access 'no-such-dir'\":/failed to link 'no-such-dir'\"/" -e "s/link-to-dir: hard link not allowed for directory/failed to link 'link-to-dir' to/" -e "s|link-to-dir/: hard link not allowed for directory|failed to link 'link-to-dir/' to|" tests/ln/hard-to-sym.sh
# GNU sleep accepts some crazy string, not sure we should match this behavior
sed -i -e "s/timeout 10 sleep 0x.002p1/#timeout 10 sleep 0x.002p1/" tests/misc/sleep.sh