use std::fs::File;
use std::io::{self, Read};
use std::path::Path;
use std::time::Duration;
pub const MAX_OPEN_TRIES: u32 = 64;
const MAX_PAUSE_MS: u64 = 20;
const ERROR_ACCESS_DENIED: i32 = 5;
const ERROR_SHARING_VIOLATION: i32 = 32;
#[cfg_attr(not(windows), allow(dead_code))]
pub(crate) fn is_replace_refusal(code: Option<i32>) -> bool {
matches!(
code,
Some(ERROR_ACCESS_DENIED) | Some(ERROR_SHARING_VIOLATION)
)
}
#[cfg_attr(not(windows), allow(dead_code))]
pub(crate) fn pause_after(try_index: u32) -> Duration {
Duration::from_millis((u64::from(try_index) + 1).min(MAX_PAUSE_MS))
}
#[cfg_attr(not(windows), allow(dead_code))]
pub(crate) fn open_with_retry<T>(
tries: u32,
mut open: impl FnMut() -> io::Result<T>,
mut pause: impl FnMut(Duration),
) -> io::Result<T> {
let mut attempt = 0u32;
loop {
match open() {
Err(error)
if attempt + 1 < tries.max(1) && is_replace_refusal(error.raw_os_error()) =>
{
pause(pause_after(attempt));
attempt += 1;
}
other => return other,
}
}
}
pub fn open_replaced(path: &Path) -> io::Result<File> {
#[cfg(windows)]
{
open_with_retry(MAX_OPEN_TRIES, || File::open(path), std::thread::sleep)
}
#[cfg(not(windows))]
{
File::open(path)
}
}
pub fn read_replaced(path: &Path) -> io::Result<Vec<u8>> {
let mut file = open_replaced(path)?;
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)?;
Ok(bytes)
}
pub fn read_to_string_replaced(path: &Path) -> io::Result<String> {
let mut file = open_replaced(path)?;
let mut text = String::new();
file.read_to_string(&mut text)?;
Ok(text)
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::{Cell, RefCell};
fn os(code: i32) -> io::Error {
io::Error::from_raw_os_error(code)
}
#[test]
fn only_access_denied_and_sharing_violation_are_refusals() {
assert!(is_replace_refusal(Some(5)));
assert!(is_replace_refusal(Some(32)));
for other in [2, 3, 33, 80, 123, 1224] {
assert!(!is_replace_refusal(Some(other)), "{other}");
}
assert!(!is_replace_refusal(None));
}
#[test]
fn the_pause_grows_by_a_millisecond_and_stops_at_twenty() {
assert_eq!(pause_after(0), Duration::from_millis(1));
assert_eq!(pause_after(1), Duration::from_millis(2));
assert_eq!(pause_after(19), Duration::from_millis(20));
assert_eq!(pause_after(63), Duration::from_millis(20));
assert_eq!(pause_after(u32::MAX), Duration::from_millis(20));
}
#[test]
fn the_worst_case_wait_is_bounded() {
let total: Duration = (0..MAX_OPEN_TRIES - 1).map(pause_after).sum();
assert!(total <= Duration::from_secs(2), "{total:?}");
}
#[test]
fn a_refusal_is_retried_until_the_open_succeeds() {
let calls = Cell::new(0u32);
let pauses = RefCell::new(Vec::new());
let got = open_with_retry(
MAX_OPEN_TRIES,
|| {
calls.set(calls.get() + 1);
match calls.get() {
1 => Err(os(5)),
2 => Err(os(32)),
_ => Ok("opened"),
}
},
|d| pauses.borrow_mut().push(d),
);
assert_eq!(got.unwrap(), "opened");
assert_eq!(calls.get(), 3);
assert_eq!(
*pauses.borrow(),
vec![Duration::from_millis(1), Duration::from_millis(2)]
);
}
#[test]
fn a_persistent_refusal_stops_after_the_last_try_and_is_returned() {
let calls = Cell::new(0u32);
let got: io::Result<()> = open_with_retry(
MAX_OPEN_TRIES,
|| {
calls.set(calls.get() + 1);
Err(os(5))
},
|_| {},
);
assert_eq!(calls.get(), MAX_OPEN_TRIES);
assert_eq!(got.unwrap_err().raw_os_error(), Some(5));
}
#[test]
fn any_other_error_ends_the_loop_at_once() {
let calls = Cell::new(0u32);
let got: io::Result<()> = open_with_retry(
MAX_OPEN_TRIES,
|| {
calls.set(calls.get() + 1);
Err(io::Error::new(io::ErrorKind::NotFound, "gone"))
},
|_| panic!("no pause for a non-refusal"),
);
assert_eq!(calls.get(), 1);
assert_eq!(got.unwrap_err().kind(), io::ErrorKind::NotFound);
}
#[test]
fn zero_tries_still_opens_once() {
let calls = Cell::new(0u32);
let got: io::Result<()> = open_with_retry(
0,
|| {
calls.set(calls.get() + 1);
Err(os(32))
},
|_| {},
);
assert_eq!(calls.get(), 1);
assert!(got.is_err());
}
#[test]
fn reads_an_ordinary_file_and_reports_a_missing_one() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("f.txt");
std::fs::write(&path, "hello").unwrap();
assert_eq!(read_replaced(&path).unwrap(), b"hello");
assert_eq!(read_to_string_replaced(&path).unwrap(), "hello");
let missing = open_replaced(&dir.path().join("none")).unwrap_err();
assert_eq!(missing.kind(), io::ErrorKind::NotFound);
}
}