use std::fs::{File, OpenOptions};
use std::path::Path;
use std::io;
use std::time::Duration;
pub struct Options {
open_options: OpenOptions,
retry_flukes: bool,
create_is_atomic: bool,
polling_fallback: Option<Duration>,
}
impl Options {
pub fn with_open_options(open_options: OpenOptions) -> Self {
Options {
open_options,
retry_flukes: false,
create_is_atomic: false,
polling_fallback: None,
}
}
pub fn retry_on_fluke(mut self, retry: bool) -> Self {
self.retry_flukes = retry;
self
}
pub fn polling_fallback_interval(mut self, interval: Duration) -> Self {
self.polling_fallback = Some(interval);
self
}
pub fn assume_create_is_atomic(mut self, is_atomic: bool) -> Self {
self.create_is_atomic = is_atomic;
self
}
#[inline]
pub fn open_when_created<P: AsRef<Path>>(&self, path: P) -> io::Result<File> {
self.internal_open_when_created(path.as_ref())
}
fn internal_open_when_created(&self, path: &Path) -> io::Result<File> {
use inotify::WatchMask;
match inotify::Inotify::init() {
Ok(mut inotify) => {
let mut mask = WatchMask::CLOSE_WRITE | WatchMask::MOVED_TO | WatchMask::DELETE_SELF | WatchMask::ONLYDIR;
if self.create_is_atomic {
mask |= WatchMask::CREATE;
}
match inotify.add_watch(path, mask) {
Ok(_) => (),
Err(error) => return self.try_fallback_open(path, error),
};
self.wait_for_file(inotify, path)
},
Err(error) => self.try_fallback_open(path, error),
}
}
fn try_fallback_open(&self, path: &Path, inotify_error: io::Error) -> io::Result<File> {
loop {
match self.open_options.open(path) {
Ok(file) => return Ok(file),
Err(error) if error.kind() == io::ErrorKind::NotFound => (),
Err(error) => return Err(error),
}
match &self.polling_fallback {
Some(interval) => std::thread::sleep(*interval),
None => return Err(inotify_error),
}
}
}
fn wait_for_file(&self, mut inotify: inotify::Inotify, path: &Path) -> io::Result<File> {
use inotify::EventMask;
let mut buffer = [0; 4096];
let mut not_found_is_ok = true;
loop {
match self.open_options.open(path) {
Ok(file) => return Ok(file),
Err(error) if error.kind() == io::ErrorKind::NotFound && not_found_is_ok => (),
Err(error) => return Err(error),
}
#[cfg(all(test, test_delay_after_check))]
{
std::thread::sleep(std::time::Duration::from_secs(7));
}
let mut found = false;
while !found {
let events = match inotify.read_events_blocking(&mut buffer) {
Ok(events) => events,
Err(error) => return self.try_fallback_open(path, error),
};
for event in events {
if event.mask.contains(EventMask::IGNORED) {
return self.try_fallback_open(path, io::Error::from(io::ErrorKind::NotFound));
}
if event.name == Some(path.as_os_str()) {
found = true;
}
}
}
not_found_is_ok = self.retry_flukes;
}
}
}
pub fn robust_wait_read<P: AsRef<Path>>(path: P) -> io::Result<File> {
let mut open_options = OpenOptions::new();
open_options.read(true);
Options::with_open_options(open_options)
.retry_on_fluke(true)
.polling_fallback_interval(Duration::from_secs(2))
.open_when_created(path)
}
pub fn robust_wait_read_write<P: AsRef<Path>>(path: P) -> io::Result<File> {
let mut open_options = OpenOptions::new();
open_options.read(true).write(true);
Options::with_open_options(open_options)
.retry_on_fluke(true)
.polling_fallback_interval(Duration::from_secs(2))
.open_when_created(path)
}
pub fn robust_wait_read_append<P: AsRef<Path>>(path: P) -> io::Result<File> {
let mut open_options = OpenOptions::new();
open_options.read(true).append(true);
Options::with_open_options(open_options)
.retry_on_fluke(true)
.polling_fallback_interval(Duration::from_secs(2))
.open_when_created(path)
}
#[cfg(test)]
mod tests {
#[test]
fn test_wait() {
use std::io::{Read, Write};
let test_string = "satoshi nakamoto";
let temp_dir = mktemp::Temp::new_dir().unwrap();
let file_path = temp_dir.join("test");
let file_path_thread = file_path.clone();
let thread = std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_secs(2));
let mut file = std::fs::File::create(&file_path_thread).unwrap();
std::thread::sleep(std::time::Duration::from_secs(1));
file.write_all(test_string.as_bytes()).unwrap();
std::thread::sleep(std::time::Duration::from_secs(1));
});
let mut file = super::robust_wait_read(&file_path).unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
assert_eq!(contents, test_string);
thread.join().unwrap();
}
}