From e7e249c43c47e2ca260bb2249a92084b44a4270d Mon Sep 17 00:00:00 2001 From: Ed Minnix Date: Thu, 15 Sep 2022 21:40:06 -0400 Subject: [PATCH 01/28] Add custom errors --- Cargo.toml | 1 + src/errors.rs | 12 ++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 src/errors.rs diff --git a/Cargo.toml b/Cargo.toml index b21580b..4165ba2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,4 @@ license = "MIT OR Apache-2.0" [dependencies] clap = { version = "3.2.21", features = ["derive"] } +thiserror = "1.0.35" diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..ecfbac2 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,12 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum HeadTailError { + #[error("IO error: {0}")] + IOError(#[from] std::io::Error), + + #[error("File watcher error: {0}")] + FileWatcherError(#[from] notify::Error), +} + +pub type Result = std::result::Result; From 69483545871f58803bc28ed4f34a2096998019ad Mon Sep 17 00:00:00 2001 From: Ed Minnix Date: Thu, 15 Sep 2022 21:40:45 -0400 Subject: [PATCH 02/28] Add `Send` requirement to input & output stream methods In order to use file watching, it is necessary to be able to share data between threads. Therefore, adding the `Send` requirement to the `input_stream` and `output_stream` methods of `Opts` lets us share the streams. --- Cargo.toml | 1 + src/opts.rs | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4165ba2..07d8591 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,4 +14,5 @@ license = "MIT OR Apache-2.0" [dependencies] clap = { version = "3.2.21", features = ["derive"] } +notify = "5.0.0" thiserror = "1.0.35" diff --git a/src/opts.rs b/src/opts.rs index 598fbe5..976be46 100644 --- a/src/opts.rs +++ b/src/opts.rs @@ -44,8 +44,8 @@ impl Opts { } /// Stream to receive input from. Either the file passed, or stdin otherwise. - pub fn input_stream(&self) -> std::io::Result> { - let stream: Box = match self.filename { + pub fn input_stream(&self) -> std::io::Result> { + let stream: Box = match self.filename { Some(ref filename) => { let file = OpenOptions::new().read(true).open(filename)?; Box::new(BufReader::new(file)) @@ -56,8 +56,8 @@ impl Opts { } /// Stream to write output to. Either the file passed, or stdout otherwise. - pub fn output_stream(&self) -> std::io::Result> { - let stream: Box = match self.outfile { + pub fn output_stream(&self) -> std::io::Result> { + let stream: Box = match self.outfile { Some(ref filename) => { let file = OpenOptions::new().write(true).create(true).open(filename)?; Box::new(BufWriter::new(file)) From 18919682ee63a8778980f1fc776405bb750bc7ff Mon Sep 17 00:00:00 2001 From: Ed Minnix Date: Thu, 15 Sep 2022 21:44:49 -0400 Subject: [PATCH 03/28] Change to using the custom error type --- src/lib.rs | 13 +++++++------ src/main.rs | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6862f0d..f36147e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,16 @@ +pub mod errors; pub mod opts; use std::{ collections::VecDeque, - io::{BufRead, ErrorKind, Result, Write}, + io::{self, BufRead, ErrorKind, Write}, time::Duration, }; +use errors::Result; use opts::Opts; -fn careful_write(writer: &mut dyn Write, line: &str) -> Result<()> { +fn careful_write(writer: &mut dyn Write, line: &str) -> io::Result<()> { if let Err(e) = writer.write(line.as_bytes()) { if e.kind() == ErrorKind::BrokenPipe { return Ok(()); @@ -28,15 +30,15 @@ pub fn headtail(opts: &Opts) -> Result<()> { let mut line_num = 0; loop { let mut line = String::new(); - match reader.read_line(&mut line) { - Ok(0) => { + match reader.read_line(&mut line)? { + 0 => { for tail_line in &tail_buffer { careful_write(&mut writer, tail_line)?; } let _ = writer.flush(); break; } - Ok(_) => { + _ => { if opts.head > line_num { line_num += 1; careful_write(&mut writer, &line)?; @@ -48,7 +50,6 @@ pub fn headtail(opts: &Opts) -> Result<()> { } } } - Err(e) => return Err(e), } } diff --git a/src/main.rs b/src/main.rs index d8ada5e..cd5dbb8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use std::io::Result; +use headtail::errors::Result; use headtail::{headtail, opts::Opts}; From 08ebb62bf0ddf66212085d6fee57c2b90bb6c3db Mon Sep 17 00:00:00 2001 From: Ed Minnix Date: Thu, 15 Sep 2022 21:45:13 -0400 Subject: [PATCH 04/28] Switch to using file watcher --- src/lib.rs | 77 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 59 insertions(+), 18 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f36147e..6eed631 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,9 +4,12 @@ pub mod opts; use std::{ collections::VecDeque, io::{self, BufRead, ErrorKind, Write}, + path::Path, time::Duration, }; +use notify::{event::EventKind, Event, Watcher}; + use errors::Result; use opts::Opts; @@ -54,28 +57,66 @@ pub fn headtail(opts: &Opts) -> Result<()> { } // Keep following(?) - + // + // To avoid wasted CPU cycles, we can use a file system watcher (e.g. + // `inotify(7)` on Linux). + // + // The `notify` crate provides several optimized file system watchers using + // functionality built into operating systems. Should an optimized watcher + // not be available, `notify` will default to a polling watcher. if opts.follow && opts.filename.is_some() { - let sleep_interval = Duration::from_secs_f64(opts.sleep_interval); let mut line = String::new(); - loop { - line.clear(); - match reader.read_line(&mut line) { - Ok(0) => { - // This is a busy loop, so add a little sleep to make it less CPU hungry - std::thread::sleep(sleep_interval); - } - Ok(_) => { - careful_write(&mut writer, &line)?; - let _ = writer.flush(); - } - Err(e) => { - println!("The error is {:?}", e.kind()); - return Err(e); + + // Use a channel for synchronization between the watcher and the main + // thread. + let (tx, rx) = std::sync::mpsc::channel(); + + // If using a polling watcher, respect the `--sleep-interval` argument. + let sleep_interval = Duration::from_secs_f64(opts.sleep_interval); + let config = notify::Config::default().with_poll_interval(sleep_interval); + + // Setup the file watcher + let mut watcher = + notify::RecommendedWatcher::new(move |res: notify::Result| match res { + Ok(event) if is_modification(&event) => { + line.clear(); + match reader.read_line(&mut line) { + Ok(0) => {} + Ok(_) => { + match careful_write(&mut writer, &line) { + Ok(_) => {} + Err(e) => eprintln!("Write error: {:?}", e.kind()), + } + let _ = writer.flush(); + } + Err(e) => { + eprintln!("The error is {:?}", e.kind()); + } + } + tx.send(()).unwrap(); } - } - } + Ok(_) => {} + Err(e) => eprintln!("Watcher error: {:?}", e), + }, config)?; + + watcher.watch( + Path::new(&opts.filename.as_ref().unwrap()), + notify::RecursiveMode::NonRecursive, + )?; + + // Loop over the messages in the channel. This will block the main + // thread without sleeping. + // + // TODO: use the channel to communicate errors with the watching thread, + // and stop the process. + for _ in rx {} } Ok(()) } + +/// Determine if an event is a modification +#[inline] +fn is_modification(event: &Event) -> bool { + matches!(event.kind, EventKind::Modify(_)) +} From 7ad8ca11e915b1f2e562a580b85853781dff2c25 Mon Sep 17 00:00:00 2001 From: Ed Minnix Date: Thu, 15 Sep 2022 21:54:03 -0400 Subject: [PATCH 05/28] Clippy/formatting fixes --- src/lib.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6eed631..26dc884 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -76,8 +76,8 @@ pub fn headtail(opts: &Opts) -> Result<()> { let config = notify::Config::default().with_poll_interval(sleep_interval); // Setup the file watcher - let mut watcher = - notify::RecommendedWatcher::new(move |res: notify::Result| match res { + let mut watcher = notify::RecommendedWatcher::new( + move |res: notify::Result| match res { Ok(event) if is_modification(&event) => { line.clear(); match reader.read_line(&mut line) { @@ -97,7 +97,9 @@ pub fn headtail(opts: &Opts) -> Result<()> { } Ok(_) => {} Err(e) => eprintln!("Watcher error: {:?}", e), - }, config)?; + }, + config, + )?; watcher.watch( Path::new(&opts.filename.as_ref().unwrap()), From e0c11f287ebec4f44883d8d4b6d348b0057c796e Mon Sep 17 00:00:00 2001 From: Edward Minnix III Date: Fri, 16 Sep 2022 13:19:27 -0400 Subject: [PATCH 06/28] Remove unnecessary double-reference Co-authored-by: Nathan Stocks --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 26dc884..0824bd4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -102,7 +102,7 @@ pub fn headtail(opts: &Opts) -> Result<()> { )?; watcher.watch( - Path::new(&opts.filename.as_ref().unwrap()), + Path::new(opts.filename.as_ref().unwrap()), notify::RecursiveMode::NonRecursive, )?; From a05cf053b976e01c02924be10582af935bfb1820 Mon Sep 17 00:00:00 2001 From: Ed Minnix Date: Fri, 16 Sep 2022 15:08:58 -0400 Subject: [PATCH 07/28] Change careful_write to return headtail::errors::Result --- src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0824bd4..d71e403 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,7 @@ pub mod opts; use std::{ collections::VecDeque, - io::{self, BufRead, ErrorKind, Write}, + io::{BufRead, ErrorKind, Write}, path::Path, time::Duration, }; @@ -13,12 +13,12 @@ use notify::{event::EventKind, Event, Watcher}; use errors::Result; use opts::Opts; -fn careful_write(writer: &mut dyn Write, line: &str) -> io::Result<()> { +fn careful_write(writer: &mut dyn Write, line: &str) -> Result<()> { if let Err(e) = writer.write(line.as_bytes()) { if e.kind() == ErrorKind::BrokenPipe { return Ok(()); } else { - return Err(e); + return Err(errors::HeadTailError::IOError(e)); } } Ok(()) @@ -85,7 +85,7 @@ pub fn headtail(opts: &Opts) -> Result<()> { Ok(_) => { match careful_write(&mut writer, &line) { Ok(_) => {} - Err(e) => eprintln!("Write error: {:?}", e.kind()), + Err(e) => eprintln!("Write error: {:?}", e), } let _ = writer.flush(); } From 10cc6842cfa481f206205aa41887cd5cb45b338d Mon Sep 17 00:00:00 2001 From: Nathan Stocks Date: Mon, 19 Sep 2022 16:17:48 -0600 Subject: [PATCH 08/28] make usage of HeadTailError explicit --- src/errors.rs | 2 -- src/lib.rs | 8 ++++---- src/main.rs | 6 ++---- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/errors.rs b/src/errors.rs index ecfbac2..a3d06a1 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -8,5 +8,3 @@ pub enum HeadTailError { #[error("File watcher error: {0}")] FileWatcherError(#[from] notify::Error), } - -pub type Result = std::result::Result; diff --git a/src/lib.rs b/src/lib.rs index d71e403..4578215 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,23 +8,23 @@ use std::{ time::Duration, }; +use errors::HeadTailError; use notify::{event::EventKind, Event, Watcher}; -use errors::Result; use opts::Opts; -fn careful_write(writer: &mut dyn Write, line: &str) -> Result<()> { +fn careful_write(writer: &mut dyn Write, line: &str) -> Result<(), HeadTailError> { if let Err(e) = writer.write(line.as_bytes()) { if e.kind() == ErrorKind::BrokenPipe { return Ok(()); } else { - return Err(errors::HeadTailError::IOError(e)); + return Err(HeadTailError::IOError(e)); } } Ok(()) } -pub fn headtail(opts: &Opts) -> Result<()> { +pub fn headtail(opts: &Opts) -> Result<(), HeadTailError> { let mut reader = opts.input_stream()?; let mut writer = opts.output_stream()?; diff --git a/src/main.rs b/src/main.rs index cd5dbb8..087ccfc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,6 @@ -use headtail::errors::Result; +use headtail::{errors::HeadTailError, headtail, opts::Opts}; -use headtail::{headtail, opts::Opts}; - -fn main() -> Result<()> { +fn main() -> Result<(), HeadTailError> { let opts = Opts::parse_args(); //println!("{opts:#?}"); headtail(&opts)?; From 87110aa87194557c4d51c282a8ed65c2163d0323 Mon Sep 17 00:00:00 2001 From: Nathan Stocks Date: Mon, 19 Sep 2022 16:22:43 -0600 Subject: [PATCH 09/28] use the from/into implementation provided by thiserror --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 4578215..55adb5b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,7 +18,7 @@ fn careful_write(writer: &mut dyn Write, line: &str) -> Result<(), HeadTailError if e.kind() == ErrorKind::BrokenPipe { return Ok(()); } else { - return Err(HeadTailError::IOError(e)); + return Err(e.into()); } } Ok(()) From 4dc8d5b54321f3f9aa1c8c09c5b02358737d0959 Mon Sep 17 00:00:00 2001 From: Nathan Stocks Date: Mon, 19 Sep 2022 16:29:14 -0600 Subject: [PATCH 10/28] update help with info about the notify watcher --- src/opts.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/opts.rs b/src/opts.rs index 976be46..bda10b0 100644 --- a/src/opts.rs +++ b/src/opts.rs @@ -24,13 +24,16 @@ pub struct Opts { )] pub tail: usize, #[clap( - help = "Wait for additional data to be appended to a file. Ignored if standard input is a pipe.", + help = "Wait for additional data to be appended to a file. Ignored if standard input is a \ + pipe. If a `notify`-compatible filesystem watcher is available, that will be used. If \ + not, we will fall back to a polling watcher.", short = 'f', long = "follow" )] pub follow: bool, #[clap(short = 's', long = "sleep-interval", default_value_t = 0.025)] - /// When following a file, sleep this amount in seconds between polling for changes. + /// When following a file, sleep this amount in seconds between polling for changes. Ignored if + /// a `notify`-compatible watcher is available. pub sleep_interval: f64, /// Write output to file From b6e2024e31e210ed8d07b350aa143478e6fc3669 Mon Sep 17 00:00:00 2001 From: Ed Minnix Date: Tue, 20 Sep 2022 10:39:02 -0400 Subject: [PATCH 11/28] Make check for modification inline --- src/lib.rs | 41 +++++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 55adb5b..632ed91 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -77,26 +77,29 @@ pub fn headtail(opts: &Opts) -> Result<(), HeadTailError> { // Setup the file watcher let mut watcher = notify::RecommendedWatcher::new( - move |res: notify::Result| match res { - Ok(event) if is_modification(&event) => { - line.clear(); - match reader.read_line(&mut line) { - Ok(0) => {} - Ok(_) => { - match careful_write(&mut writer, &line) { - Ok(_) => {} - Err(e) => eprintln!("Write error: {:?}", e), + move |res: notify::Result| { + use EventKind::Modify; + match res { + Ok(event) if matches!(event.kind, Modify(_)) => { + line.clear(); + match reader.read_line(&mut line) { + Ok(0) => {} + Ok(_) => { + match careful_write(&mut writer, &line) { + Ok(_) => {} + Err(e) => eprintln!("Write error: {:?}", e), + } + let _ = writer.flush(); + } + Err(e) => { + eprintln!("The error is {:?}", e.kind()); } - let _ = writer.flush(); - } - Err(e) => { - eprintln!("The error is {:?}", e.kind()); } + tx.send(()).unwrap(); } - tx.send(()).unwrap(); + Ok(_) => {} + Err(e) => eprintln!("Watcher error: {:?}", e), } - Ok(_) => {} - Err(e) => eprintln!("Watcher error: {:?}", e), }, config, )?; @@ -116,9 +119,3 @@ pub fn headtail(opts: &Opts) -> Result<(), HeadTailError> { Ok(()) } - -/// Determine if an event is a modification -#[inline] -fn is_modification(event: &Event) -> bool { - matches!(event.kind, EventKind::Modify(_)) -} From d8f4440042c1e98823c2aea9e9928910664bd131 Mon Sep 17 00:00:00 2001 From: Ed Minnix Date: Tue, 20 Sep 2022 11:07:40 -0400 Subject: [PATCH 12/28] Remove (?) from following documentation --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 632ed91..3aa791a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,7 +56,7 @@ pub fn headtail(opts: &Opts) -> Result<(), HeadTailError> { } } - // Keep following(?) + // Keep following // // To avoid wasted CPU cycles, we can use a file system watcher (e.g. // `inotify(7)` on Linux). From 18ff66caef77e2987dac13f48f8003b6be05ed37 Mon Sep 17 00:00:00 2001 From: Ed Minnix Date: Tue, 20 Sep 2022 11:08:36 -0400 Subject: [PATCH 13/28] Add event filter TODO note to fs watcher We will later want to better handle files moving and related changes in the file system. --- src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 3aa791a..d5d15f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -80,6 +80,9 @@ pub fn headtail(opts: &Opts) -> Result<(), HeadTailError> { move |res: notify::Result| { use EventKind::Modify; match res { + // TODO: This is probably the place to detect if a file was + // renamed/removed and a new one of the same name created + // (in a later PR) Ok(event) if matches!(event.kind, Modify(_)) => { line.clear(); match reader.read_line(&mut line) { From 9b29f170a2b128a5e8cbb15ed9aeba99b96d7cca Mon Sep 17 00:00:00 2001 From: Nathan Stocks Date: Tue, 20 Sep 2022 09:33:13 -0600 Subject: [PATCH 14/28] huge refactor, debugging integration test --- Cargo.toml | 8 +++- README.md | 16 +++++-- src/lib.rs | 116 ++++++++++++++++++++++++++++++++++--------------- src/main.rs | 1 + src/opts.rs | 2 +- tests/basic.rs | 65 ++++++++++++++++++++++++--- 6 files changed, 163 insertions(+), 45 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 07d8591..c5ad78c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,12 @@ license = "MIT OR Apache-2.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -clap = { version = "3.2.21", features = ["derive"] } +clap = { version = "3.2.21", features = [ "derive" ] } +crossbeam-channel = "0.5.6" +env_logger = "0.9.1" +log = "0.4.17" notify = "5.0.0" thiserror = "1.0.35" + +[dev-dependencies] +tempfile = "3.3.0" diff --git a/README.md b/README.md index d842db1..a6d1ebe 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,6 @@ You need to [have Rust installed](https://www.rust-lang.org/tools/install). ```shell # Install latest *release* version of headtail $ cargo install headtail - -# Install local development version of headtail from inside the git repo -$ cargo install --path . ``` ``` @@ -44,6 +41,19 @@ $ headtail somebigfile.txt -f See `headtail -h` for a full list of command-line options. +## Development + +``` +# Run locally with arguments +$ cargo run -- YOUR_ARGS_GO_HERE + +# Enable debug logging +$ RUST_LOG=trace cargo run -- YOUR_ARGS_GO_HERE + +# Install local development version of headtail into your ~/.cargo/bin +$ cargo install --path . +``` + ## Software License Distributed under the terms of both the MIT license and the Apache License (Version 2.0). diff --git a/src/lib.rs b/src/lib.rs index 55adb5b..a6ac011 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ use std::{ }; use errors::HeadTailError; +use log::trace; use notify::{event::EventKind, Event, Watcher}; use opts::Opts; @@ -65,38 +66,90 @@ pub fn headtail(opts: &Opts) -> Result<(), HeadTailError> { // functionality built into operating systems. Should an optimized watcher // not be available, `notify` will default to a polling watcher. if opts.follow && opts.filename.is_some() { - let mut line = String::new(); - - // Use a channel for synchronization between the watcher and the main - // thread. - let (tx, rx) = std::sync::mpsc::channel(); + // Use a channel to send lines read back to the main thread + // TODO: 1024 is an arbitrary number. Let's benchmark different values. + let (tx, rx) = crossbeam_channel::bounded::>(1024); // If using a polling watcher, respect the `--sleep-interval` argument. let sleep_interval = Duration::from_secs_f64(opts.sleep_interval); let config = notify::Config::default().with_poll_interval(sleep_interval); // Setup the file watcher + let opts2 = opts.clone(); // TODO: Refactor so we don't need to clone opts let mut watcher = notify::RecommendedWatcher::new( - move |res: notify::Result| match res { - Ok(event) if is_modification(&event) => { - line.clear(); - match reader.read_line(&mut line) { - Ok(0) => {} - Ok(_) => { - match careful_write(&mut writer, &line) { - Ok(_) => {} - Err(e) => eprintln!("Write error: {:?}", e), + move |res: notify::Result| { + match res { + Ok(event) => { + match event.kind { + EventKind::Any => trace!("EventKind::Any encountered"), + EventKind::Modify(m) => { + // TODO: Should(can?) we handle the truncation of a file? On macOS + // file truncation shows up as an EventKind::Modify(Metadata(Any)), + // which seems like could apply to events other than truncation. + trace!(target: "following file", "modified: {:?}", m); + let mut line = String::new(); + match reader.read_line(&mut line) { + Ok(0) => {} + Ok(_) => { + // If the main thread has closed the channel, it will soon cause + // us to exit cleanly, so we can ignore the error. + let _ = tx.send(Ok(line)); + } + Err(e) => { + // Can ignore channel send error for the same reason as above... + trace!(target: "following file", "normal read error"); + let _ = tx.send(Err(e.into())); + } + } + } + EventKind::Create(_) => { + trace!(target: "following file", "detected file (re)creation"); + // The file has been recreated, so we need to re-open the input stream, + // read *everything* that is in the new file, and resume tailing. + let result = opts2.input_stream(); + if result.is_ok() { + trace!(target: "following file", "succeeded reopening file"); + reader = result.unwrap(); + } else { + // Can ignore channel send error for the same reason as above... + let _ = tx.send(Err(result.err().unwrap().into())); + } + loop { + let mut line = String::new(); + match reader.read_line(&mut line) { + Ok(0) => { + trace!(target: "following file", "catchup done"); + break; + } + Ok(_) => { + trace!(target: "following file", "catchup read line"); + // If the main thread has closed the channel, it will soon cause us to + // exit cleanly, so we can ignore the error. + let _ = tx.send(Ok(line)); + } + Err(e) => { + // Can ignore sending error for same reason as 👆🏻 + let _ = tx.send(Err(e.into())); + break; + } + } + } + } + EventKind::Remove(r) => { + trace!(target: "following file", "file removed: {:?}", r) + } + // We are being explicit about the variants we are ignoring just in case we + // need to research them. + EventKind::Access(_) => {} + EventKind::Other => { + trace!(target: "following file", "EventKind::Other encountered") } - let _ = writer.flush(); - } - Err(e) => { - eprintln!("The error is {:?}", e.kind()); - } + }; + } + Err(e) => { + let _ = tx.send(Err(e.into())); } - tx.send(()).unwrap(); } - Ok(_) => {} - Err(e) => eprintln!("Watcher error: {:?}", e), }, config, )?; @@ -106,19 +159,14 @@ pub fn headtail(opts: &Opts) -> Result<(), HeadTailError> { notify::RecursiveMode::NonRecursive, )?; - // Loop over the messages in the channel. This will block the main - // thread without sleeping. - // - // TODO: use the channel to communicate errors with the watching thread, - // and stop the process. - for _ in rx {} + // Loop over the lines sent from the `notify` watcher over a channel. This will block the + // main thread without sleeping. + for result in rx { + let line = result?; + careful_write(&mut writer, &line)?; + let _ = writer.flush(); + } } Ok(()) } - -/// Determine if an event is a modification -#[inline] -fn is_modification(event: &Event) -> bool { - matches!(event.kind, EventKind::Modify(_)) -} diff --git a/src/main.rs b/src/main.rs index 087ccfc..62337a6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ use headtail::{errors::HeadTailError, headtail, opts::Opts}; fn main() -> Result<(), HeadTailError> { + env_logger::init(); let opts = Opts::parse_args(); //println!("{opts:#?}"); headtail(&opts)?; diff --git a/src/opts.rs b/src/opts.rs index bda10b0..1a5f229 100644 --- a/src/opts.rs +++ b/src/opts.rs @@ -5,7 +5,7 @@ use std::{ use clap::Parser; -#[derive(Debug, clap::Parser)] +#[derive(Clone, Debug, clap::Parser)] pub struct Opts { #[clap(help = "Read from a file instead of stdin")] pub filename: Option, diff --git a/tests/basic.rs b/tests/basic.rs index e3e6bb1..03ed2ec 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -1,6 +1,14 @@ -use std::fs::File; -use std::io::{BufRead, BufReader}; -use std::process::Command; +use std::fs::{File, OpenOptions}; +use std::io::{BufRead, BufReader, Result, Write}; +use std::process::{Command, Stdio}; +use std::time::Duration; + +use tempfile::NamedTempFile; + +fn number_of_input_lines() -> usize { + let f = BufReader::new(File::open("tests/files/input.txt").unwrap()); + f.lines().count() +} #[test] fn help() { @@ -160,7 +168,52 @@ fn overlapping_head_and_tail() { // TODO: Add test for -f/--follow -fn number_of_input_lines() -> usize { - let f = BufReader::new(File::open("tests/files/input.txt").unwrap()); - f.lines().count() +#[test] +fn follow_detects_recreation() -> Result<()> { + // create a temporary file + // Write + println!("1"); + let mut tmpfile = NamedTempFile::new()?; + write!(tmpfile, "first file\n")?; + let tmpfilename = tmpfile.into_temp_path(); + println!("2"); + + let mut cmd = Command::new(env!("CARGO_BIN_EXE_headtail")) + .arg(&tmpfilename) + .arg("--follow") + .stdout(Stdio::piped()) + .spawn()?; + println!("3"); + + std::fs::remove_file(&tmpfilename)?; + println!("4"); + + let mut newfile = OpenOptions::new() + .create(true) + .read(true) + .write(true) + .open(tmpfilename)?; + println!("5"); + + write!(newfile, "second file\n")?; + println!("6"); + + let _ = newfile.flush(); + drop(newfile); + println!("7"); + + std::thread::sleep(Duration::from_millis(100)); + cmd.kill()?; + println!("8"); + + match cmd.wait_with_output() { + Ok(output) => { + let stdout = String::from_utf8_lossy(&output.stdout); + println!("AAAAAAAAA {} BBBBBBBB {}", stdout, output.status.success()); + assert!(stdout.contains("first file")); + assert!(stdout.contains("second file")); + } + Err(e) => println!("Error: {}", e), + } + Ok(()) } From 040c149babbe506963199e5222e0bff5714cab9e Mon Sep 17 00:00:00 2001 From: Nathan Stocks Date: Wed, 21 Sep 2022 15:17:00 -0600 Subject: [PATCH 15/28] figured out test problems --- src/lib.rs | 12 +++++-- tests/{basic.rs => integration.rs} | 57 +++++++++++++++++------------- 2 files changed, 41 insertions(+), 28 deletions(-) rename tests/{basic.rs => integration.rs} (81%) diff --git a/src/lib.rs b/src/lib.rs index 9f9fa79..32e4be0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,6 +45,7 @@ pub fn headtail(opts: &Opts) -> Result<(), HeadTailError> { _ => { if opts.head > line_num { line_num += 1; + trace!(target: "head line", "read line: {}", line.trim_end()); careful_write(&mut writer, &line)?; let _ = writer.flush(); } else { @@ -103,7 +104,7 @@ pub fn headtail(opts: &Opts) -> Result<(), HeadTailError> { } } EventKind::Create(_) => { - trace!(target: "following file", "detected file (re)creation"); + trace!(target: "following file", "detected possible file (re)creation"); // The file has been recreated, so we need to re-open the input stream, // read *everything* that is in the new file, and resume tailing. let result = opts2.input_stream(); @@ -111,8 +112,13 @@ pub fn headtail(opts: &Opts) -> Result<(), HeadTailError> { trace!(target: "following file", "succeeded reopening file"); reader = result.unwrap(); } else { + let err = result.err().unwrap(); + if let ErrorKind::NotFound = err.kind() { + trace!(target: "following file", "cannot find file...aborting reopen"); + return; + } // Can ignore channel send error for the same reason as above... - let _ = tx.send(Err(result.err().unwrap().into())); + let _ = tx.send(Err(err.into())); } loop { let mut line = String::new(); @@ -122,7 +128,7 @@ pub fn headtail(opts: &Opts) -> Result<(), HeadTailError> { break; } Ok(_) => { - trace!(target: "following file", "catchup read line"); + trace!(target: "following file", "catchup read line: {}", line.trim_end()); // If the main thread has closed the channel, it will soon cause us to // exit cleanly, so we can ignore the error. let _ = tx.send(Ok(line)); diff --git a/tests/basic.rs b/tests/integration.rs similarity index 81% rename from tests/basic.rs rename to tests/integration.rs index 03ed2ec..46831f3 100644 --- a/tests/basic.rs +++ b/tests/integration.rs @@ -170,48 +170,55 @@ fn overlapping_head_and_tail() { #[test] fn follow_detects_recreation() -> Result<()> { + let wait_duration = Duration::from_millis(100); // 4 times higher than minimum required for my machine - cleancut + let first_file_contents = "first file\n"; + let second_file_contents = "second file\n"; + // create a temporary file - // Write - println!("1"); - let mut tmpfile = NamedTempFile::new()?; - write!(tmpfile, "first file\n")?; - let tmpfilename = tmpfile.into_temp_path(); - println!("2"); + let just_for_name_file = NamedTempFile::new()?; + let tmpfilename = just_for_name_file.path().to_owned(); + drop(just_for_name_file); + // give filesystem time to really delete the file + std::thread::sleep(wait_duration); + + let mut tmpfile = File::create(&tmpfilename)?; + write!(tmpfile, "{}", first_file_contents)?; + tmpfile.flush(); + drop(tmpfile); + + // give filesystem time to write file contents and close file + std::thread::sleep(wait_duration); let mut cmd = Command::new(env!("CARGO_BIN_EXE_headtail")) .arg(&tmpfilename) .arg("--follow") .stdout(Stdio::piped()) .spawn()?; - println!("3"); - std::fs::remove_file(&tmpfilename)?; - println!("4"); + // Give headtail sufficient time to open the file and read it + std::thread::sleep(wait_duration); - let mut newfile = OpenOptions::new() - .create(true) - .read(true) - .write(true) - .open(tmpfilename)?; - println!("5"); + // give filesystem time to really delete the file + std::fs::remove_file(&tmpfilename)?; - write!(newfile, "second file\n")?; - println!("6"); + std::thread::sleep(wait_duration); - let _ = newfile.flush(); + let mut newfile = File::create(&tmpfilename)?; + write!(newfile, "{}", second_file_contents)?; + newfile.flush(); drop(newfile); - println!("7"); - std::thread::sleep(Duration::from_millis(100)); + // give filesystem time to write file contents and close file + std::thread::sleep(wait_duration); + cmd.kill()?; - println!("8"); match cmd.wait_with_output() { Ok(output) => { - let stdout = String::from_utf8_lossy(&output.stdout); - println!("AAAAAAAAA {} BBBBBBBB {}", stdout, output.status.success()); - assert!(stdout.contains("first file")); - assert!(stdout.contains("second file")); + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let mut combined = first_file_contents.to_owned(); + combined.push_str(second_file_contents); + assert_eq!(combined, stdout); } Err(e) => println!("Error: {}", e), } From 350bf0b024b5fead3f78627514a33a80bfc06b4d Mon Sep 17 00:00:00 2001 From: Nathan Stocks Date: Wed, 21 Sep 2022 15:19:21 -0600 Subject: [PATCH 16/28] raise delay for CI's sake --- tests/integration.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration.rs b/tests/integration.rs index 46831f3..da9be2b 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -170,7 +170,7 @@ fn overlapping_head_and_tail() { #[test] fn follow_detects_recreation() -> Result<()> { - let wait_duration = Duration::from_millis(100); // 4 times higher than minimum required for my machine - cleancut + let wait_duration = Duration::from_millis(250); // 10 times higher than minimum required for my machine - cleancut let first_file_contents = "first file\n"; let second_file_contents = "second file\n"; From 4083438be93d6d06b51258545c9489451c2c2be3 Mon Sep 17 00:00:00 2001 From: Nathan Stocks Date: Wed, 21 Sep 2022 15:21:57 -0600 Subject: [PATCH 17/28] raise delay for CI's sake...again --- tests/integration.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration.rs b/tests/integration.rs index da9be2b..6e84c46 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -170,7 +170,7 @@ fn overlapping_head_and_tail() { #[test] fn follow_detects_recreation() -> Result<()> { - let wait_duration = Duration::from_millis(250); // 10 times higher than minimum required for my machine - cleancut + let wait_duration = Duration::from_millis(500); // 20 times higher than minimum required for my machine - cleancut let first_file_contents = "first file\n"; let second_file_contents = "second file\n"; From ece07224e7db4e4f99f99ff01b51fe636b67ea10 Mon Sep 17 00:00:00 2001 From: Nathan Stocks Date: Wed, 21 Sep 2022 15:24:18 -0600 Subject: [PATCH 18/28] fix warning, raise delay again --- src/lib.rs | 2 +- tests/integration.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 32e4be0..71fbc18 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,7 @@ use std::{ use errors::HeadTailError; use log::trace; -use notify::{event::EventKind, Event, Watcher}; +use notify::{event::EventKind, Watcher}; use opts::Opts; diff --git a/tests/integration.rs b/tests/integration.rs index 6e84c46..dba98d4 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -170,7 +170,7 @@ fn overlapping_head_and_tail() { #[test] fn follow_detects_recreation() -> Result<()> { - let wait_duration = Duration::from_millis(500); // 20 times higher than minimum required for my machine - cleancut + let wait_duration = Duration::from_millis(750); // 20 times higher than minimum required for my machine - cleancut let first_file_contents = "first file\n"; let second_file_contents = "second file\n"; From f86edc32b5858000bef323c388f4f47fa1be21cd Mon Sep 17 00:00:00 2001 From: Nathan Stocks Date: Wed, 21 Sep 2022 15:28:47 -0600 Subject: [PATCH 19/28] switch ci to useful debugging output --- .github/workflows/ci.yml | 57 ++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d24ded..3c5a2b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [ "main" ] + branches: ["main"] pull_request: - branches: [ "main" ] + branches: ["main"] workflow_dispatch: env: @@ -12,33 +12,32 @@ env: jobs: test: - runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - - name: Fetch cargo registry cache - uses: actions/cache@v3 - continue-on-error: false - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - target/ - Cargo.lock - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - - name: Build - run: cargo build --verbose - - - name: rustfmt & clippy - run: | - rustup component add clippy rustfmt - cargo clippy --workspace - cargo fmt --all -- --check - - - name: Run tests - run: cargo test --verbose + - uses: actions/checkout@v3 + + - name: Fetch cargo registry cache + uses: actions/cache@v3 + continue-on-error: false + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + Cargo.lock + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Build + run: cargo build + + - name: rustfmt & clippy + run: | + rustup component add clippy rustfmt + cargo clippy --workspace + cargo fmt --all -- --check + + - name: Run tests + run: RUST_LOG=trace cargo test From 7756e0260864f99aee70e2c10ca66cdb4a466ac0 Mon Sep 17 00:00:00 2001 From: Nathan Stocks Date: Wed, 21 Sep 2022 15:33:15 -0600 Subject: [PATCH 20/28] silence more warnings, turn on more useful debugging output --- .github/workflows/ci.yml | 2 +- tests/integration.rs | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c5a2b1..b069a23 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,4 +40,4 @@ jobs: cargo fmt --all -- --check - name: Run tests - run: RUST_LOG=trace cargo test + run: RUST_LOG=trace cargo test -- --show-output diff --git a/tests/integration.rs b/tests/integration.rs index dba98d4..d9f0d34 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1,4 +1,4 @@ -use std::fs::{File, OpenOptions}; +use std::fs::File; use std::io::{BufRead, BufReader, Result, Write}; use std::process::{Command, Stdio}; use std::time::Duration; @@ -183,7 +183,7 @@ fn follow_detects_recreation() -> Result<()> { let mut tmpfile = File::create(&tmpfilename)?; write!(tmpfile, "{}", first_file_contents)?; - tmpfile.flush(); + let _ = tmpfile.flush(); drop(tmpfile); // give filesystem time to write file contents and close file @@ -193,6 +193,7 @@ fn follow_detects_recreation() -> Result<()> { .arg(&tmpfilename) .arg("--follow") .stdout(Stdio::piped()) + .stderr(Stdio::piped()) .spawn()?; // Give headtail sufficient time to open the file and read it @@ -205,7 +206,7 @@ fn follow_detects_recreation() -> Result<()> { let mut newfile = File::create(&tmpfilename)?; write!(newfile, "{}", second_file_contents)?; - newfile.flush(); + let _ = newfile.flush(); drop(newfile); // give filesystem time to write file contents and close file @@ -215,6 +216,10 @@ fn follow_detects_recreation() -> Result<()> { match cmd.wait_with_output() { Ok(output) => { + println!( + "stderr was: `{}`", + String::from_utf8_lossy(&output.stderr).to_string() + ); let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let mut combined = first_file_contents.to_owned(); combined.push_str(second_file_contents); From e0a7e80d15824191cdcf32a8c58d06520a07d10d Mon Sep 17 00:00:00 2001 From: Nathan Stocks Date: Wed, 21 Sep 2022 15:46:25 -0600 Subject: [PATCH 21/28] reduce delay, fix clippy warning, add more OSs to ci --- .github/workflows/ci.yml | 5 ++++- src/lib.rs | 22 ++++++++++++---------- tests/integration.rs | 2 +- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b069a23..5cfc74c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,10 @@ env: jobs: test: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [macos-latest, windows-latest, ubuntu-latest] steps: - uses: actions/checkout@v3 diff --git a/src/lib.rs b/src/lib.rs index 71fbc18..ccc1693 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -108,17 +108,19 @@ pub fn headtail(opts: &Opts) -> Result<(), HeadTailError> { // The file has been recreated, so we need to re-open the input stream, // read *everything* that is in the new file, and resume tailing. let result = opts2.input_stream(); - if result.is_ok() { - trace!(target: "following file", "succeeded reopening file"); - reader = result.unwrap(); - } else { - let err = result.err().unwrap(); - if let ErrorKind::NotFound = err.kind() { - trace!(target: "following file", "cannot find file...aborting reopen"); - return; + match result { + Ok(new_reader) => { + trace!(target: "following file", "succeeded reopening file"); + reader = new_reader; + } + Err(e) => { + if let ErrorKind::NotFound = e.kind() { + trace!(target: "following file", "cannot find file...aborting reopen"); + return; + } + // Can ignore channel send error for the same reason as above... + let _ = tx.send(Err(e.into())); } - // Can ignore channel send error for the same reason as above... - let _ = tx.send(Err(err.into())); } loop { let mut line = String::new(); diff --git a/tests/integration.rs b/tests/integration.rs index d9f0d34..7df9a5c 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -170,7 +170,7 @@ fn overlapping_head_and_tail() { #[test] fn follow_detects_recreation() -> Result<()> { - let wait_duration = Duration::from_millis(750); // 20 times higher than minimum required for my machine - cleancut + let wait_duration = Duration::from_millis(125); // 5 times higher than minimum required for my machine - cleancut let first_file_contents = "first file\n"; let second_file_contents = "second file\n"; From cb168fa9cffedb2fc9c73611cdf5f03775ff645d Mon Sep 17 00:00:00 2001 From: Nathan Stocks Date: Wed, 21 Sep 2022 16:03:30 -0600 Subject: [PATCH 22/28] don't fail fast --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5cfc74c..34fe709 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,7 @@ jobs: test: runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [macos-latest, windows-latest, ubuntu-latest] From 97c86d72223029042d74cdfaefa607505ce2ad42 Mon Sep 17 00:00:00 2001 From: Nathan Stocks Date: Wed, 21 Sep 2022 16:11:45 -0600 Subject: [PATCH 23/28] ignore test on linux ci --- .github/workflows/ci.yml | 4 +++- tests/integration.rs | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 34fe709..f8826c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,4 +44,6 @@ jobs: cargo fmt --all -- --check - name: Run tests - run: RUST_LOG=trace cargo test -- --show-output + run: cargo test -- --show-output + env: + RUST_LOG: trace diff --git a/tests/integration.rs b/tests/integration.rs index 7df9a5c..eabfd0e 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -170,6 +170,12 @@ fn overlapping_head_and_tail() { #[test] fn follow_detects_recreation() -> Result<()> { + if let Ok(ci_var) = std::env::var("CI") { + if !ci_var.is_empty() && cfg!(linux) { + eprintln!("WARNING: Ignoring follow_detects_recreation test on Linux CI since this feature doesn't work with the fallback polling strategy.") + return Ok(()) + } + } let wait_duration = Duration::from_millis(125); // 5 times higher than minimum required for my machine - cleancut let first_file_contents = "first file\n"; let second_file_contents = "second file\n"; From c26243b04607aa501816e6f7131c8504466a762f Mon Sep 17 00:00:00 2001 From: Nathan Stocks Date: Wed, 21 Sep 2022 16:14:03 -0600 Subject: [PATCH 24/28] don't forget semicolons --- tests/integration.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration.rs b/tests/integration.rs index eabfd0e..e997f09 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -172,8 +172,8 @@ fn overlapping_head_and_tail() { fn follow_detects_recreation() -> Result<()> { if let Ok(ci_var) = std::env::var("CI") { if !ci_var.is_empty() && cfg!(linux) { - eprintln!("WARNING: Ignoring follow_detects_recreation test on Linux CI since this feature doesn't work with the fallback polling strategy.") - return Ok(()) + eprintln!("WARNING: Ignoring follow_detects_recreation test on Linux CI since this feature doesn't work with the fallback polling strategy."); + return Ok(()); } } let wait_duration = Duration::from_millis(125); // 5 times higher than minimum required for my machine - cleancut From 78926bc2450e18af38aca84923f82cb86dcf8718 Mon Sep 17 00:00:00 2001 From: Nathan Stocks Date: Wed, 21 Sep 2022 16:16:53 -0600 Subject: [PATCH 25/28] fix the cfg macro --- tests/integration.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration.rs b/tests/integration.rs index e997f09..b017767 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -171,7 +171,7 @@ fn overlapping_head_and_tail() { #[test] fn follow_detects_recreation() -> Result<()> { if let Ok(ci_var) = std::env::var("CI") { - if !ci_var.is_empty() && cfg!(linux) { + if !ci_var.is_empty() && cfg!(target_os = "linux") { eprintln!("WARNING: Ignoring follow_detects_recreation test on Linux CI since this feature doesn't work with the fallback polling strategy."); return Ok(()); } From 148b48d7faefb4d93962684905dc6d93effff1d5 Mon Sep 17 00:00:00 2001 From: Nathan Stocks Date: Wed, 21 Sep 2022 16:22:16 -0600 Subject: [PATCH 26/28] remove windows from CI, since it doesn't pass all tests yet --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f8826c6..c68e1fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-latest, windows-latest, ubuntu-latest] + os: [macos-latest, ubuntu-latest] steps: - uses: actions/checkout@v3 From ff14037b8dec9ccd012c082f935e98513d088ec8 Mon Sep 17 00:00:00 2001 From: Nathan Stocks Date: Wed, 21 Sep 2022 16:28:41 -0600 Subject: [PATCH 27/28] add todo about possible race condition between initial headtail and following --- src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index ccc1693..0dac98c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -162,6 +162,8 @@ pub fn headtail(opts: &Opts) -> Result<(), HeadTailError> { config, )?; + // TODO: Figure out what to do about the possibility of a race condition between the initial + // headtail and the following. See https://github.com/CleanCut/headtail/pull/17/files#r973220630 watcher.watch( Path::new(opts.filename.as_ref().unwrap()), notify::RecursiveMode::NonRecursive, From 22561533b3ba3a7fa2986a61db443e8a6ac02a8e Mon Sep 17 00:00:00 2001 From: Nathan Stocks Date: Wed, 21 Sep 2022 16:34:33 -0600 Subject: [PATCH 28/28] update changelog --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1afd693..1f63472 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ ## [Unreleased] - ReleaseDate +### Added + +- CI now runs on macOS in addition to Linux. Now we just need someone to help us [support Windows](https://github.com/CleanCut/headtail/issues/21)! + +### Improved + +- We now use a notify-based watcher (inotify on Linux, etc.) when available to avoid polling. +- Internal improvements. + ## [0.3.0] - 2022-09-15 ### Added