diff --git a/Cargo.lock b/Cargo.lock index c0cf8d330..0ad3ab3b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -942,8 +942,10 @@ name = "tail" version = "0.0.1" dependencies = [ "getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", "uucore 0.0.1", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] diff --git a/src/tail/Cargo.toml b/src/tail/Cargo.toml index 878265f2f..a5af8e7bf 100644 --- a/src/tail/Cargo.toml +++ b/src/tail/Cargo.toml @@ -9,7 +9,9 @@ path = "tail.rs" [dependencies] getopts = "*" +kernel32-sys = "*" libc = "*" +winapi = "*" uucore = { path="../uucore" } [[bin]] diff --git a/src/tail/README.md b/src/tail/README.md index f5c890ad2..9bd15e725 100644 --- a/src/tail/README.md +++ b/src/tail/README.md @@ -4,7 +4,6 @@ Rudimentary tail implementation. ### Flags with features * `--max-unchanged-stats` : with `--follow=name`, reopen a FILE which has not changed size after N (default 5) iterations to see if it has been unlinked or renamed (this is the usual case of rotated log files). With inotify, this option is rarely useful. -* `--pid` : with `-f`, terminate after process ID, PID dies * `--quiet` : never output headers giving file names * `--retry` : keep trying to open a file even when it is or becomes inaccessible; useful when follow‐ing by name, i.e., with `--follow=name` diff --git a/src/tail/platform/mod.rs b/src/tail/platform/mod.rs new file mode 100644 index 000000000..c043bafbe --- /dev/null +++ b/src/tail/platform/mod.rs @@ -0,0 +1,20 @@ +/* + * This file is part of the uutils coreutils package. + * + * (c) Alexander Batischev + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +#[cfg(unix)] +pub use self::unix::{Pid, supports_pid_checks, ProcessChecker}; + +#[cfg(windows)] +pub use self::windows::{Pid, supports_pid_checks, ProcessChecker}; + +#[cfg(unix)] +mod unix; + +#[cfg(windows)] +mod windows; diff --git a/src/tail/platform/unix.rs b/src/tail/platform/unix.rs new file mode 100644 index 000000000..c54306da1 --- /dev/null +++ b/src/tail/platform/unix.rs @@ -0,0 +1,45 @@ +/* + * This file is part of the uutils coreutils package. + * + * (c) Alexander Batischev + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +extern crate libc; + +use std::io::Error; + +pub type Pid = libc::pid_t; + +pub struct ProcessChecker { pid: self::Pid } + +impl ProcessChecker { + pub fn new(process_id: self::Pid) -> ProcessChecker { + ProcessChecker { pid: process_id } + } + + // Borrowing mutably to be aligned with Windows implementation + pub fn is_dead(&mut self) -> bool { + unsafe { + libc::kill(self.pid, 0) != 0 && get_errno() != libc::EPERM + } + } +} + +impl Drop for ProcessChecker { + fn drop(&mut self) { + } +} + +pub fn supports_pid_checks(pid: self::Pid) -> bool { + unsafe { + !(libc::kill(pid, 0) != 0 && get_errno() == libc::ENOSYS) + } +} + +#[inline] +fn get_errno() -> i32 { + Error::last_os_error().raw_os_error().unwrap() +} diff --git a/src/tail/platform/windows.rs b/src/tail/platform/windows.rs new file mode 100644 index 000000000..2f66f53a3 --- /dev/null +++ b/src/tail/platform/windows.rs @@ -0,0 +1,57 @@ +/* + * This file is part of the uutils coreutils package. + * + * (c) Alexander Batischev + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +extern crate winapi; +extern crate kernel32; + +use self::kernel32::{OpenProcess, CloseHandle, WaitForSingleObject}; +use self::winapi::minwindef::DWORD; +use self::winapi::winbase::{WAIT_OBJECT_0, WAIT_FAILED}; +use self::winapi::winnt::{HANDLE, SYNCHRONIZE}; + +pub type Pid = DWORD; + +pub struct ProcessChecker { + dead: bool, + handle: HANDLE +} + +impl ProcessChecker { + pub fn new(process_id: self::Pid) -> ProcessChecker { + #[allow(non_snake_case)] + let FALSE = 0i32; + let h = unsafe { + OpenProcess(SYNCHRONIZE, FALSE, process_id as DWORD) + }; + ProcessChecker { dead: h.is_null(), handle: h } + } + + pub fn is_dead(&mut self) -> bool { + if !self.dead { + self.dead = unsafe { + let status = WaitForSingleObject(self.handle, 0); + status == WAIT_OBJECT_0 || status == WAIT_FAILED + } + } + + self.dead + } +} + +impl Drop for ProcessChecker { + fn drop(&mut self) { + unsafe { + CloseHandle(self.handle); + } + } +} + +pub fn supports_pid_checks(_pid: self::Pid) -> bool { + true +} diff --git a/src/tail/tail.rs b/src/tail/tail.rs index 9e665540c..b7e169424 100755 --- a/src/tail/tail.rs +++ b/src/tail/tail.rs @@ -4,6 +4,7 @@ * This file is part of the uutils coreutils package. * * (c) Morten Olsen Lysgaard + * (c) Alexander Batischev * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -11,10 +12,13 @@ */ extern crate getopts; +extern crate libc; #[macro_use] extern crate uucore; +mod platform; + use std::collections::VecDeque; use std::error::Error; use std::fmt; @@ -38,6 +42,7 @@ struct Settings { sleep_msec: u32, beginning: bool, follow: bool, + pid: platform::Pid, } impl Default for Settings { @@ -47,6 +52,7 @@ impl Default for Settings { sleep_msec: 1000, beginning: false, follow: false, + pid: 0, } } } @@ -68,6 +74,7 @@ pub fn uumain(args: Vec) -> i32 { opts.optopt("n", "lines", "Number of lines to print", "k"); opts.optflag("f", "follow", "Print the file as it grows"); opts.optopt("s", "sleep-interval", "Number or seconds to sleep between polling the file when running with -f", "n"); + opts.optopt("", "pid", "with -f, terminate after process ID, PID dies", "PID"); opts.optflag("z", "zero-terminated", "Line delimiter is NUL, not newline"); opts.optflag("h", "help", "help"); opts.optflag("V", "version", "version"); @@ -101,6 +108,22 @@ pub fn uumain(args: Vec) -> i32 { }; } + if let Some(pid_str) = given_options.opt_str("pid") { + if let Ok(pid) = pid_str.parse() { + settings.pid = pid; + if pid != 0 { + if !settings.follow { + show_warning!("PID ignored; --pid=PID is useful only when following"); + } + + if !platform::supports_pid_checks(pid) { + show_warning!("--pid=PID is not supported on this system"); + settings.pid = 0; + } + } + } + } + match given_options.opt_str("n") { Some(n) => { let mut slice: &str = n.as_ref(); @@ -320,10 +343,15 @@ const BLOCK_SIZE: u64 = 1 << 16; fn follow(readers: &mut [BufReader], filenames: &[String], settings: &Settings) { assert!(settings.follow); let mut last = readers.len() - 1; + let mut read_some = false; + let mut process = platform::ProcessChecker::new(settings.pid); loop { sleep(Duration::new(0, settings.sleep_msec*1000)); + let pid_is_dead = !read_some && settings.pid != 0 && process.is_dead(); + read_some = false; + for (i, reader) in readers.iter_mut().enumerate() { // Print all new content since the last pass loop { @@ -331,6 +359,7 @@ fn follow(readers: &mut [BufReader], filenames: &[String], settings: match reader.read_line(&mut datum) { Ok(0) => break, Ok(_) => { + read_some = true; if i != last { println!("\n==> {} <==", filenames[i]); last = i; @@ -341,6 +370,10 @@ fn follow(readers: &mut [BufReader], filenames: &[String], settings: } } } + + if pid_is_dead { + break; + } } } diff --git a/tests/test_tail.rs b/tests/test_tail.rs index 06b96b2fd..dea1a56f8 100644 --- a/tests/test_tail.rs +++ b/tests/test_tail.rs @@ -2,8 +2,11 @@ extern crate uu_tail; use common::util::*; use std::char::from_digit; -use std::io::Write; use self::uu_tail::parse_size; +use std::io::Write; +use std::process::{Command, Stdio}; +use std::thread::sleep; +use std::time::Duration; static FOOBAR_TXT: &'static str = "foobar.txt"; @@ -74,6 +77,48 @@ fn test_follow_stdin() { new_ucmd!().arg("-f").pipe_in_fixture(FOOBAR_TXT).run().stdout_is_fixture("follow_stdin.expected"); } +#[test] +fn test_follow_with_pid() { + let (at, mut ucmd) = at_and_ucmd!(); + + #[cfg(unix)] + let dummy_cmd = "sh"; + #[cfg(windows)] + let dummy_cmd = "cmd"; + + let mut dummy = Command::new(dummy_cmd).stdout(Stdio::null()).spawn().unwrap(); + let pid = dummy.id(); + + let mut child = ucmd.arg("-f").arg(format!("--pid={}", pid)).arg(FOOBAR_TXT).arg(FOOBAR_2_TXT).run_no_wait(); + + let expected = at.read("foobar_follow_multiple.expected"); + assert_eq!(read_size(&mut child, expected.len()), expected); + + let first_append = "trois\n"; + at.append(FOOBAR_2_TXT, first_append); + assert_eq!(read_size(&mut child, first_append.len()), first_append); + + let second_append = "doce\ntrece\n"; + let expected = at.read("foobar_follow_multiple_appended.expected"); + at.append(FOOBAR_TXT, second_append); + assert_eq!(read_size(&mut child, expected.len()), expected); + + // kill the dummy process and give tail time to notice this + dummy.kill().unwrap(); + let _ = dummy.wait(); + sleep(Duration::from_secs(1)); + + let third_append = "should\nbe\nignored\n"; + at.append(FOOBAR_TXT, third_append); + assert_eq!(read_size(&mut child, 1), "\u{0}"); + + // On Unix, trying to kill a process that's already dead is fine; on Windows it's an error. + #[cfg(unix)] + child.kill().unwrap(); + #[cfg(windows)] + assert_eq!(child.kill().is_err(), true); +} + #[test] fn test_single_big_args() { const FILE: &'static str = "single_big_args.txt";