1use std::fs::File;
9use std::io;
10use std::path::Path;
11use std::time::{Duration, Instant};
12
13mod lifetime;
14mod memory;
15mod os_version;
16#[cfg(unix)]
17mod unix;
18#[cfg(unix)]
19use unix as platform;
20#[cfg(windows)]
21mod windows;
22#[cfg(windows)]
23use windows as platform;
24
25pub use lifetime::{
26 KillOnCloseJob, exit_without_handlers, stop_std_handle_inheritance, watch_parent,
27};
28pub use memory::{MemoryCapacity, memory_capacity};
29#[cfg(any(target_os = "linux", target_os = "android"))]
30pub use memory::{cgroup_v2_usage, memory_capacity_at};
31pub use os_version::os_version;
32
33pub fn try_lock_exclusive(file: &File) -> io::Result<bool> {
51 platform::try_lock_exclusive(file)
52}
53
54pub fn same_file(file: &File, path: &Path) -> io::Result<bool> {
61 platform::same_file(file, path)
62}
63
64pub fn same_file_at(a: &Path, b: &Path) -> io::Result<bool> {
70 platform::same_file_at(a, b)
71}
72
73#[derive(Clone, Copy, Debug, Eq, PartialEq)]
75pub enum PublishMethod {
76 RenameNoReplace,
80 PlaceholderRename,
83 RenameReplace,
85}
86
87impl PublishMethod {
88 #[must_use]
90 pub const fn as_str(self) -> &'static str {
91 match self {
92 Self::RenameNoReplace => "rename-no-replace",
93 Self::PlaceholderRename => "placeholder-rename",
94 Self::RenameReplace => "rename-replace",
95 }
96 }
97}
98
99#[derive(Clone, Copy, Debug, Eq, PartialEq)]
101pub struct Published {
102 pub method: PublishMethod,
104 pub retries: u32,
106}
107
108#[derive(Clone, Copy)]
110pub struct RetryPolicy<'a> {
111 pub budget: Duration,
113 pub on_wait: &'a dyn Fn() -> bool,
115}
116
117fn keep_waiting() -> bool {
118 true
119}
120
121impl RetryPolicy<'static> {
122 pub const NONE: Self = Self {
124 budget: Duration::ZERO,
125 on_wait: &keep_waiting,
126 };
127}
128
129impl std::fmt::Debug for RetryPolicy<'_> {
130 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131 formatter
132 .debug_struct("RetryPolicy")
133 .field("budget", &self.budget)
134 .finish_non_exhaustive()
135 }
136}
137
138const RETRY_FIRST_DELAY: Duration = Duration::from_millis(50);
140const RETRY_MAX_DELAY: Duration = Duration::from_secs(1);
141
142pub fn with_publish_retries<T>(
148 retry: &RetryPolicy<'_>,
149 step: impl FnMut() -> io::Result<T>,
150) -> io::Result<(T, u32)> {
151 retrying(retry, is_retryable_publish_error, step)
152}
153
154fn retrying<T>(
155 retry: &RetryPolicy<'_>,
156 retryable: impl Fn(&io::Error) -> bool,
157 mut step: impl FnMut() -> io::Result<T>,
158) -> io::Result<(T, u32)> {
159 let started = Instant::now();
160 let mut delay = RETRY_FIRST_DELAY;
161 let mut retries = 0_u32;
162 loop {
163 match step() {
164 Ok(value) => return Ok((value, retries)),
165 Err(error) => {
166 let elapsed = started.elapsed();
167 if !retryable(&error) || elapsed >= retry.budget || !(retry.on_wait)() {
168 return Err(error);
169 }
170 std::thread::sleep(delay.min(retry.budget.saturating_sub(elapsed)));
171 retries = retries.saturating_add(1);
172 delay = (delay * 2).min(RETRY_MAX_DELAY);
173 }
174 }
175 }
176}
177
178#[must_use]
186pub fn is_retryable_publish_error(error: &io::Error) -> bool {
187 platform::is_retryable_publish_error(error)
188}
189
190pub fn publish_no_replace(
204 from: &Path,
205 to: &Path,
206 retry: &RetryPolicy<'_>,
207) -> io::Result<Published> {
208 publish_no_replace_with(from, to, retry, &platform::rename_exclusive)
209}
210
211fn publish_no_replace_with(
213 from: &Path,
214 to: &Path,
215 retry: &RetryPolicy<'_>,
216 rename_exclusive: &dyn Fn(&Path, &Path) -> io::Result<()>,
217) -> io::Result<Published> {
218 let (method, retries) = with_publish_retries(retry, || match rename_exclusive(from, to) {
219 Ok(()) => Ok(PublishMethod::RenameNoReplace),
220 Err(error) if platform::flag_refused(&error) => {
221 publish_through_placeholder(from, to).map(|()| PublishMethod::PlaceholderRename)
222 }
223 Err(error) => Err(error),
224 })?;
225 Ok(Published { method, retries })
226}
227
228fn publish_through_placeholder(from: &Path, to: &Path) -> io::Result<()> {
234 let placeholder = std::fs::OpenOptions::new()
235 .write(true)
236 .create_new(true)
237 .open(to)?;
238 let identity = platform::file_identity(&placeholder);
239 drop(placeholder);
240 let renamed = std::fs::rename(from, to);
241 if renamed.is_err()
242 && let Ok(identity) = identity
243 && platform::path_identity(to).ok().flatten() == Some(identity)
244 {
245 let _ = std::fs::remove_file(to);
246 }
247 renamed
248}
249
250pub fn publish_replace(from: &Path, to: &Path, retry: &RetryPolicy<'_>) -> io::Result<Published> {
256 let ((), retries) = with_publish_retries(retry, || std::fs::rename(from, to))?;
257 Ok(Published {
258 method: PublishMethod::RenameReplace,
259 retries,
260 })
261}
262
263pub fn sync_directory(dir: &Path) -> io::Result<()> {
269 #[cfg(unix)]
270 {
271 platform::sync_directory(dir)
272 }
273 #[cfg(not(unix))]
274 {
275 let _ = dir;
276 Ok(())
277 }
278}
279
280#[derive(Clone, Copy, Debug, Eq, PartialEq)]
282pub enum NameUnit {
283 Bytes,
285 Utf16,
287}
288
289#[derive(Clone, Copy, Debug, Eq, PartialEq)]
291pub struct NameLimit {
292 pub max: usize,
294 pub unit: NameUnit,
296}
297
298impl NameLimit {
299 #[must_use]
301 pub fn length(self, name: &str) -> usize {
302 match self.unit {
303 NameUnit::Bytes => name.len(),
304 NameUnit::Utf16 => name.encode_utf16().count(),
305 }
306 }
307}
308
309#[must_use]
312pub fn name_limit(dir: &Path) -> NameLimit {
313 platform::name_limit(dir)
314}
315
316#[derive(Clone, Copy, Debug, Eq, PartialEq)]
319pub struct Termination {
320 name: &'static str,
321 exit_code: u8,
322 closing: bool,
323}
324
325impl Termination {
326 #[must_use]
328 pub const fn new(name: &'static str, exit_code: u8, closing: bool) -> Self {
329 Self {
330 name,
331 exit_code,
332 closing,
333 }
334 }
335
336 #[must_use]
338 pub const fn name(self) -> &'static str {
339 self.name
340 }
341
342 #[must_use]
345 pub const fn exit_code(self) -> u8 {
346 self.exit_code
347 }
348
349 #[must_use]
352 pub const fn closing(self) -> bool {
353 self.closing
354 }
355}
356
357pub fn on_termination(handler: impl Fn(Termination) + Send + Sync + 'static) -> io::Result<()> {
368 platform::on_termination(Box::new(handler))
369}
370
371#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
376pub struct AwakeInstant(#[cfg(windows)] u64, #[cfg(not(windows))] Instant);
377
378impl AwakeInstant {
379 #[must_use]
381 pub fn now() -> Self {
382 #[cfg(windows)]
383 {
384 Self(platform::unbiased_interrupt_time())
385 }
386 #[cfg(not(windows))]
387 {
388 Self(Instant::now())
389 }
390 }
391
392 #[must_use]
394 pub fn duration_since(self, earlier: Self) -> Duration {
395 #[cfg(windows)]
396 {
397 Duration::from_nanos(self.0.saturating_sub(earlier.0).saturating_mul(100))
398 }
399 #[cfg(not(windows))]
400 {
401 self.0.saturating_duration_since(earlier.0)
402 }
403 }
404
405 #[must_use]
407 pub fn elapsed(self) -> Duration {
408 Self::now().duration_since(self)
409 }
410
411 #[must_use]
413 pub fn checked_sub(self, duration: Duration) -> Option<Self> {
414 #[cfg(windows)]
415 {
416 let units = u64::try_from(duration.as_nanos() / 100).ok()?;
417 self.0.checked_sub(units).map(Self)
418 }
419 #[cfg(not(windows))]
420 {
421 self.0.checked_sub(duration).map(Self)
422 }
423 }
424}
425
426#[must_use]
429pub fn quarantined(path: &Path) -> bool {
430 #[cfg(target_os = "macos")]
431 {
432 platform::quarantined(path)
433 }
434 #[cfg(not(target_os = "macos"))]
435 {
436 let _ = path;
437 false
438 }
439}
440
441pub fn utf8_arguments(
447 arguments: impl Iterator<Item = std::ffi::OsString>,
448) -> Result<Vec<String>, String> {
449 arguments
450 .map(|argument| {
451 argument.into_string().map_err(|argument| {
452 format!(
453 "argument {:?} is not valid UTF-8",
454 argument.to_string_lossy()
455 )
456 })
457 })
458 .collect()
459}
460
461#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
467pub struct ProcessMemory {
468 pub resident_bytes: Option<u64>,
470 pub peak_resident_bytes: Option<u64>,
472 pub footprint_bytes: Option<u64>,
474 pub peak_footprint_bytes: Option<u64>,
476}
477
478#[must_use]
481pub fn file_version(path: &Path) -> Option<String> {
482 #[cfg(windows)]
483 {
484 platform::file_version(path)
485 }
486 #[cfg(not(windows))]
487 {
488 let _ = path;
489 None
490 }
491}
492
493#[must_use]
495pub fn process_memory() -> ProcessMemory {
496 #[cfg(any(unix, windows))]
497 {
498 platform::process_memory()
499 }
500 #[cfg(not(any(unix, windows)))]
501 {
502 ProcessMemory::default()
503 }
504}
505
506#[must_use]
510pub fn thread_cpu_work() -> Option<u64> {
511 #[cfg(any(unix, windows))]
512 {
513 platform::thread_cpu_work()
514 }
515 #[cfg(not(any(unix, windows)))]
516 {
517 None
518 }
519}
520
521pub fn prefer_fast_cores() {
525 #[cfg(any(target_os = "macos", windows))]
526 platform::prefer_fast_cores();
527}
528
529pub fn limit_heap_arenas(arenas: u16) {
534 #[cfg(all(target_os = "linux", target_env = "gnu"))]
535 platform::limit_heap_arenas(arenas);
536 #[cfg(not(all(target_os = "linux", target_env = "gnu")))]
537 let _ = arenas;
538}
539
540#[must_use]
542pub fn process_cpu_time() -> Option<std::time::Duration> {
543 #[cfg(any(unix, windows))]
544 {
545 platform::process_cpu_time()
546 }
547 #[cfg(not(any(unix, windows)))]
548 {
549 None
550 }
551}
552
553pub fn write_diagnostic(line: std::fmt::Arguments<'_>) {
556 use std::io::Write;
557 let _ = writeln!(io::stderr().lock(), "{line}");
558}
559
560#[macro_export]
562macro_rules! diagnostic {
563 ($($argument:tt)*) => {
564 $crate::write_diagnostic(::std::format_args!($($argument)*))
565 };
566}
567
568#[cfg(test)]
569mod tests {
570 use super::*;
571 use std::fs::{self, OpenOptions};
572
573 #[test]
574 fn the_awake_clock_advances_with_time_and_never_goes_back() {
575 let started = AwakeInstant::now();
576 std::thread::sleep(Duration::from_millis(200));
577 let elapsed = started.elapsed();
578 assert!(
579 elapsed >= Duration::from_millis(150) && elapsed < Duration::from_secs(5),
580 "{elapsed:?} for a 200 ms sleep"
581 );
582 let mut last = AwakeInstant::now();
583 for _ in 0..10_000 {
584 let now = AwakeInstant::now();
585 assert!(now >= last, "{now:?} after {last:?}");
586 last = now;
587 }
588 assert_eq!(
589 started
590 .checked_sub(Duration::from_millis(1))
591 .map(|earlier| started.duration_since(earlier)),
592 Some(Duration::from_millis(1))
593 );
594 }
595
596 #[test]
597 #[cfg(any(target_os = "linux", target_os = "macos", windows))]
598 fn the_process_reports_its_resident_memory() {
599 let memory = process_memory();
600 let resident = memory.resident_bytes.expect("resident bytes");
601 let peak = memory.peak_resident_bytes.expect("peak resident bytes");
602 assert!(resident > 1 << 20, "{memory:?}");
603 assert!(peak >= resident, "{memory:?}");
604 }
605
606 #[test]
607 #[cfg(any(unix, windows))]
608 fn the_process_cpu_time_grows_with_its_work() {
609 let started = process_cpu_time().expect("process CPU time");
610 let mut state = 1_u64;
611 let busy = std::time::Instant::now();
612 while busy.elapsed() < std::time::Duration::from_millis(200) {
613 state = std::hint::black_box(state.wrapping_mul(6_364_136_223_846_793_005));
614 }
615 let worked = process_cpu_time()
616 .expect("process CPU time")
617 .saturating_sub(started);
618 assert!(
620 worked >= std::time::Duration::from_millis(150),
621 "{worked:?} of CPU time for 200 ms of work"
622 );
623 }
624
625 #[test]
626 #[cfg(any(unix, windows))]
627 fn the_thread_counts_the_work_it_does_and_not_the_time_it_sleeps() {
628 let started = thread_cpu_work().expect("thread work");
629 std::thread::sleep(std::time::Duration::from_millis(200));
630 let slept = thread_cpu_work().expect("thread work") - started;
631 let mut state = 1_u64;
632 let busy = std::time::Instant::now();
633 while busy.elapsed() < std::time::Duration::from_millis(200) {
634 state = std::hint::black_box(state.wrapping_mul(6_364_136_223_846_793_005));
635 }
636 let worked = thread_cpu_work().expect("thread work") - started - slept;
637 assert!(worked > slept * 4, "slept {slept}, worked {worked}");
638 }
639
640 fn scratch(name: &str) -> std::path::PathBuf {
641 let path = std::env::temp_dir().join(format!("alpha2-os-{}-{name}", std::process::id()));
642 let _ = fs::remove_file(&path);
643 path
644 }
645
646 fn open(path: &Path) -> File {
647 OpenOptions::new()
648 .read(true)
649 .write(true)
650 .create(true)
651 .truncate(false)
652 .open(path)
653 .expect("open")
654 }
655
656 pub(crate) static FORK: std::sync::Mutex<()> = std::sync::Mutex::new(());
659
660 #[test]
661 fn one_handle_at_a_time_holds_the_lock_and_closing_it_releases_it() {
662 let _fork = FORK
663 .lock()
664 .unwrap_or_else(std::sync::PoisonError::into_inner);
665 let path = scratch("lock");
666 let first = open(&path);
667 let second = open(&path);
668 assert!(try_lock_exclusive(&first).expect("lock"));
669 assert!(!try_lock_exclusive(&second).expect("second lock"));
670 drop(first);
671 assert!(try_lock_exclusive(&second).expect("lock after release"));
672 let _ = fs::remove_file(&path);
673 }
674
675 #[test]
676 fn another_handle_writes_the_whole_of_a_locked_file() {
677 use std::io::{Read, Write};
678 let path = scratch("write");
679 let lock = open(&path);
680 assert!(try_lock_exclusive(&lock).expect("lock"));
681 let mut writer = File::create(&path).expect("writer truncates and opens");
682 writer
683 .write_all(&[7_u8; 4096])
684 .expect("write under the lock");
685 drop(writer);
686 let mut read = Vec::new();
687 File::open(&path)
688 .expect("reader")
689 .read_to_end(&mut read)
690 .expect("read under the lock");
691 assert_eq!(read, vec![7_u8; 4096]);
692 drop(lock);
693 let _ = fs::remove_file(&path);
694 }
695
696 const LOCK_PROBE: &str = "ALPHA2_OS_LOCK_PROBE";
698
699 fn another_process_takes_the_lock(path: &Path) -> bool {
701 let status = std::process::Command::new(std::env::current_exe().expect("test binary"))
702 .args([
703 "--exact",
704 "tests::lock_probe_child",
705 "--ignored",
706 "--test-threads=1",
707 ])
708 .env(LOCK_PROBE, path)
709 .stdout(std::process::Stdio::null())
710 .status()
711 .expect("run the lock probe");
712 match status.code() {
713 Some(0) => true,
714 Some(3) => false,
715 _ => panic!("the lock probe failed: {status}"),
716 }
717 }
718
719 #[test]
721 #[ignore = "run by another_process_takes_the_lock"]
722 fn lock_probe_child() {
723 let path = std::env::var_os(LOCK_PROBE).expect("the parent names the file");
724 let locked = try_lock_exclusive(&open(Path::new(&path))).expect("probe lock");
725 std::process::exit(if locked { 0 } else { 3 });
726 }
727
728 #[test]
731 fn the_lock_stays_with_its_handle_when_another_handle_closes() {
732 let _fork = FORK
733 .lock()
734 .unwrap_or_else(std::sync::PoisonError::into_inner);
735 let path = scratch("other-handle");
736 let lock = open(&path);
737 assert!(try_lock_exclusive(&lock).expect("lock"));
738 drop(File::create(&path).expect("another handle opens and closes"));
739 drop(open(&path));
740 assert!(
741 !another_process_takes_the_lock(&path),
742 "closing another handle released the lock"
743 );
744 drop(lock);
745 assert!(
746 another_process_takes_the_lock(&path),
747 "closing the handle that holds the lock releases it"
748 );
749 let _ = fs::remove_file(&path);
750 }
751
752 #[test]
755 #[cfg(target_os = "linux")]
756 fn the_lock_covers_only_one_byte_no_write_reaches() {
757 use std::os::fd::AsRawFd;
758 let path = scratch("one-byte");
759 let lock = open(&path);
760 assert!(try_lock_exclusive(&lock).expect("lock"));
761 let other = open(&path);
762 let probe = |start: i64, len: i64| {
763 let mut query: libc::flock = unsafe { std::mem::zeroed() };
765 query.l_type = libc::c_short::try_from(libc::F_WRLCK).expect("F_WRLCK");
766 query.l_whence = libc::c_short::try_from(libc::SEEK_SET).expect("SEEK_SET");
767 query.l_start = start;
768 query.l_len = len;
769 let asked =
771 unsafe { libc::fcntl(other.as_raw_fd(), libc::F_OFD_GETLK, &raw mut query) };
772 assert_eq!(asked, 0, "F_OFD_GETLK: {}", io::Error::last_os_error());
773 query
774 };
775 let everything_below = probe(0, i64::MAX);
776 assert_eq!(
777 i32::from(everything_below.l_type),
778 libc::F_UNLCK,
779 "no byte a write can reach is locked"
780 );
781 let last = probe(0, 0);
782 assert_eq!(
783 i32::from(last.l_type),
784 libc::F_WRLCK,
785 "the lock is an OFD lock"
786 );
787 assert_eq!(last.l_start, i64::MAX);
789 drop(lock);
790 let _ = fs::remove_file(&path);
791 }
792
793 #[test]
794 fn a_path_moved_away_and_created_again_is_another_file() {
795 let (path, moved) = (scratch("identity"), scratch("identity-moved"));
796 let file = open(&path);
797 assert!(same_file(&file, &path).expect("same"));
798 fs::rename(&path, &moved).expect("move away");
799 assert!(!same_file(&file, &path).expect("missing"));
800 drop(open(&path));
801 assert!(!same_file(&file, &path).expect("replaced"));
802 assert!(same_file(&file, &moved).expect("moved"));
803 let _ = fs::remove_file(&path);
804 let _ = fs::remove_file(&moved);
805 }
806
807 #[test]
808 fn a_hard_link_and_a_path_through_a_linked_directory_are_the_same_file() {
809 let directory = scratch("identity-paths");
810 let _ = fs::remove_dir_all(&directory);
811 fs::create_dir(&directory).expect("directory");
812 let (file, copy, link) = (
813 directory.join("file"),
814 directory.join("copy"),
815 directory.join("link"),
816 );
817 fs::write(&file, b"source").expect("file");
818 fs::write(©, b"source").expect("copy");
819 fs::hard_link(&file, &link).expect("hard link");
820 assert!(same_file_at(&file, &file).expect("itself"));
821 assert!(same_file_at(&file, &link).expect("hard link"));
822 assert!(!same_file_at(&file, ©).expect("copy"));
823 assert!(!same_file_at(&file, &directory.join("missing")).expect("missing"));
824 let linked = scratch("identity-paths-linked");
825 let _ = fs::remove_file(&linked);
826 #[cfg(unix)]
827 std::os::unix::fs::symlink(&directory, &linked).expect("symlink");
828 #[cfg(windows)]
830 {
831 let _fork = FORK
832 .lock()
833 .unwrap_or_else(std::sync::PoisonError::into_inner);
834 let status = std::process::Command::new("cmd")
835 .arg("/C")
836 .arg("mklink")
837 .arg("/J")
838 .arg(&linked)
839 .arg(&directory)
840 .stdout(std::process::Stdio::null())
841 .status()
842 .expect("run mklink");
843 assert!(status.success(), "mklink /J failed: {status}");
844 }
845 assert!(same_file_at(&file, &linked.join("file")).expect("through the link"));
846 assert!(!same_file_at(©, &linked.join("file")).expect("another file"));
847 #[cfg(unix)]
848 fs::remove_file(&linked).expect("cleanup");
849 #[cfg(windows)]
850 fs::remove_dir(&linked).expect("cleanup");
851 fs::remove_dir_all(&directory).expect("cleanup");
852 }
853
854 #[test]
855 fn a_removed_file_still_open_is_not_the_file_its_path_names() {
856 let path = scratch("identity-removed");
858 let file = open(&path);
859 fs::remove_file(&path).expect("remove");
860 assert!(!same_file(&file, &path).expect("removed"));
861 drop(file);
862 let again = open(&path);
863 assert!(same_file(&again, &path).expect("created again"));
864 drop(again);
865 let _ = fs::remove_file(&path);
866 }
867
868 #[test]
869 fn a_publish_moves_a_file_to_a_free_name_and_never_replaces_one() {
870 let (from, to) = (scratch("publish-from"), scratch("publish-to"));
871 fs::write(&from, b"export").expect("from");
872 fs::write(&to, b"someone else's").expect("to");
873 let refused = publish_no_replace(&from, &to, &RetryPolicy::NONE)
874 .expect_err("an existing file is not replaced");
875 assert_eq!(refused.kind(), io::ErrorKind::AlreadyExists);
876 assert_eq!(fs::read(&to).expect("to"), b"someone else's");
877 assert_eq!(fs::read(&from).expect("from"), b"export");
878 fs::remove_file(&to).expect("free the name");
879 let (probe_from, probe_to) = (scratch("publish-probe-from"), scratch("publish-probe-to"));
881 fs::write(&probe_from, b"probe").expect("probe");
882 let method = match platform::rename_exclusive(&probe_from, &probe_to) {
883 Err(error) if platform::flag_refused(&error) => PublishMethod::PlaceholderRename,
884 answer => {
885 answer.expect("the no-replace rename");
886 PublishMethod::RenameNoReplace
887 }
888 };
889 let _ = (fs::remove_file(&probe_from), fs::remove_file(&probe_to));
890 let published =
891 publish_no_replace(&from, &to, &RetryPolicy::NONE).expect("publish to a free name");
892 assert_eq!(published, Published { method, retries: 0 });
893 assert_eq!(fs::read(&to).expect("to"), b"export");
894 assert!(!from.exists());
895 let _ = fs::remove_file(&to);
896 }
897
898 #[test]
899 fn a_replacing_publish_moves_a_file_over_another() {
900 let (from, to) = (scratch("replace-from"), scratch("replace-to"));
901 fs::write(&from, b"export").expect("from");
902 fs::write(&to, b"old export").expect("to");
903 let published = publish_replace(&from, &to, &RetryPolicy::NONE).expect("replace");
904 assert_eq!(published.method, PublishMethod::RenameReplace);
905 assert_eq!(fs::read(&to).expect("to"), b"export");
906 assert!(!from.exists());
907 let _ = fs::remove_file(&to);
908 }
909
910 fn flag_refused_error() -> io::Error {
912 #[cfg(unix)]
913 return io::Error::from_raw_os_error(libc::EINVAL);
914 #[cfg(windows)]
915 return io::Error::from_raw_os_error(50);
916 }
917
918 #[test]
919 fn a_file_system_without_the_no_replace_flag_publishes_through_a_placeholder() {
920 let (from, to) = (scratch("placeholder-from"), scratch("placeholder-to"));
921 let _ = fs::remove_file(&to);
922 fs::write(&from, b"export").expect("from");
923 let no_flag = |_: &Path, _: &Path| Err(flag_refused_error());
924 let published = publish_no_replace_with(&from, &to, &RetryPolicy::NONE, &no_flag)
925 .expect("publish through a placeholder");
926 assert_eq!(published.method, PublishMethod::PlaceholderRename);
927 assert_eq!(fs::read(&to).expect("to"), b"export");
928 assert!(!from.exists());
929 fs::write(&from, b"second export").expect("from");
930 let refused = publish_no_replace_with(&from, &to, &RetryPolicy::NONE, &no_flag)
931 .expect_err("an existing file is not replaced");
932 assert_eq!(refused.kind(), io::ErrorKind::AlreadyExists);
933 assert_eq!(fs::read(&to).expect("to"), b"export");
934 assert_eq!(fs::read(&from).expect("from"), b"second export");
935 let _ = fs::remove_file(&from);
936 let _ = fs::remove_file(&to);
937 }
938
939 #[test]
942 #[cfg(unix)]
943 fn a_publish_never_leaves_a_second_link_to_the_published_file() {
944 use std::os::unix::fs::MetadataExt;
945 let (from, to) = (scratch("one-link-from"), scratch("one-link-to"));
946 let _ = fs::remove_file(&to);
947 fs::write(&from, b"export").expect("from");
948 let no_flag = |_: &Path, _: &Path| Err(flag_refused_error());
949 publish_no_replace_with(&from, &to, &RetryPolicy::NONE, &no_flag).expect("publish");
950 assert_eq!(fs::metadata(&to).expect("to").nlink(), 1);
951 assert!(!from.exists());
952 let _ = fs::remove_file(&to);
953 }
954
955 #[test]
956 fn a_failure_another_handle_causes_is_tried_again_until_the_budget_ends() {
957 let busy = || io::Error::from(io::ErrorKind::ResourceBusy);
958 let retryable = |error: &io::Error| error.kind() == io::ErrorKind::ResourceBusy;
959 let policy = RetryPolicy {
960 budget: Duration::from_secs(10),
961 on_wait: &|| true,
962 };
963 let mut tries = 0;
964 let (value, retries) = retrying(&policy, retryable, || {
965 tries += 1;
966 if tries < 3 { Err(busy()) } else { Ok(tries) }
967 })
968 .expect("the third try succeeds");
969 assert_eq!((value, retries), (3, 2));
970
971 let short = RetryPolicy {
972 budget: Duration::from_millis(200),
973 on_wait: &|| true,
974 };
975 let started = Instant::now();
976 let error = retrying(&short, retryable, || Err::<(), _>(busy())).expect_err("never");
977 assert_eq!(error.kind(), io::ErrorKind::ResourceBusy);
978 let elapsed = started.elapsed();
979 assert!(
980 elapsed >= Duration::from_millis(200) && elapsed < Duration::from_secs(5),
981 "{elapsed:?}"
982 );
983
984 let cancelled = RetryPolicy {
985 budget: Duration::from_secs(10),
986 on_wait: &|| false,
987 };
988 let mut tries = 0;
989 assert!(
990 retrying(&cancelled, retryable, || {
991 tries += 1;
992 Err::<(), _>(busy())
993 })
994 .is_err()
995 );
996 assert_eq!(tries, 1, "a cancelled wait stops trying");
997
998 let mut tries = 0;
999 assert!(
1000 retrying(&policy, retryable, || {
1001 tries += 1;
1002 Err::<(), _>(io::Error::from(io::ErrorKind::NotFound))
1003 })
1004 .is_err()
1005 );
1006 assert_eq!(tries, 1, "another error is not tried again");
1007 }
1008
1009 #[test]
1010 #[cfg(unix)]
1011 fn a_directory_syncs() {
1012 sync_directory(&std::env::temp_dir()).expect("sync the temporary directory");
1013 }
1014
1015 #[test]
1016 fn the_name_limit_of_the_temporary_directory_is_known() {
1017 let limit = name_limit(&std::env::temp_dir());
1018 assert!(limit.max >= 143, "{limit:?}");
1019 assert_eq!(limit.length("ab"), 2);
1020 assert_eq!(
1021 limit.length("\u{d55c}"),
1022 if limit.unit == NameUnit::Bytes { 3 } else { 1 }
1023 );
1024 }
1025
1026 #[cfg(windows)]
1028 fn long_directory(label: &str) -> std::path::PathBuf {
1029 let root = scratch(label);
1030 let _ = fs::remove_dir_all(&root);
1031 let directory = root.join("a".repeat(120)).join("b".repeat(120));
1032 fs::create_dir_all(&directory).expect("long directory");
1033 directory
1034 }
1035
1036 #[test]
1037 #[cfg(windows)]
1038 fn a_publish_reaches_a_path_longer_than_max_path() {
1039 let directory = long_directory("long-path");
1040 let (from, to) = (directory.join("from.mp4"), directory.join("to.mp4"));
1041 assert!(!to.as_os_str().to_string_lossy().starts_with(r"\\?\"));
1042 assert!(to.as_os_str().len() > 260);
1043 fs::write(&from, b"export").expect("from");
1044 let published = publish_no_replace(&from, &to, &RetryPolicy::NONE).expect("no replace");
1045 assert_eq!(published.method, PublishMethod::RenameNoReplace);
1046 fs::write(&from, b"second export").expect("from");
1047 publish_replace(&from, &to, &RetryPolicy::NONE).expect("replace");
1048 assert_eq!(fs::read(&to).expect("to"), b"second export");
1049 let _ = fs::remove_dir_all(scratch("long-path"));
1050 }
1051
1052 #[cfg(windows)]
1054 fn hold_without_delete_sharing(path: &Path) -> File {
1055 use std::os::windows::fs::OpenOptionsExt;
1056 OpenOptions::new()
1057 .read(true)
1058 .share_mode(1)
1059 .open(path)
1060 .expect("hold the file")
1061 }
1062
1063 #[test]
1064 #[cfg(windows)]
1065 fn a_publish_waits_for_a_reader_without_delete_sharing() {
1066 let policy = RetryPolicy {
1067 budget: Duration::from_secs(10),
1068 on_wait: &|| true,
1069 };
1070 let (from, to) = (scratch("held-from"), scratch("held-to"));
1071 let _ = fs::remove_file(&to);
1072 fs::write(&from, b"export").expect("from");
1073 fs::write(&to, b"old export").expect("to");
1074 let holder = hold_without_delete_sharing(&to);
1075 let released = std::thread::spawn(move || {
1076 std::thread::sleep(Duration::from_millis(300));
1077 drop(holder);
1078 });
1079 let published = publish_replace(&from, &to, &policy).expect("replace once released");
1080 released.join().expect("holder");
1081 assert!(published.retries >= 1, "{published:?}");
1082 assert_eq!(fs::read(&to).expect("to"), b"export");
1083 fs::remove_file(&to).expect("free the name");
1084
1085 fs::write(&from, b"export").expect("from");
1086 let holder = hold_without_delete_sharing(&from);
1087 let released = std::thread::spawn(move || {
1088 std::thread::sleep(Duration::from_millis(300));
1089 drop(holder);
1090 });
1091 let published = publish_no_replace(&from, &to, &policy).expect("publish once released");
1092 released.join().expect("holder");
1093 assert!(published.retries >= 1, "{published:?}");
1094 fs::remove_file(&to).expect("free the name");
1095
1096 fs::write(&from, b"export").expect("from");
1097 let holder = hold_without_delete_sharing(&from);
1098 let short = RetryPolicy {
1099 budget: Duration::from_millis(500),
1100 on_wait: &|| true,
1101 };
1102 let error = publish_no_replace(&from, &to, &short).expect_err("never released");
1103 assert!(is_retryable_publish_error(&error), "{error}");
1104 drop(holder);
1105 assert!(from.exists(), "a refused publish keeps its source");
1106 let _ = fs::remove_file(&from);
1107 }
1108
1109 #[test]
1110 #[cfg(windows)]
1111 fn two_names_of_one_file_have_one_identity_and_two_files_two() {
1112 let directory = scratch("file-id");
1113 let _ = fs::remove_dir_all(&directory);
1114 fs::create_dir(&directory).expect("directory");
1115 let (file, link, copy) = (
1116 directory.join("file"),
1117 directory.join("link"),
1118 directory.join("copy"),
1119 );
1120 fs::write(&file, b"source").expect("file");
1121 fs::write(©, b"source").expect("copy");
1122 fs::hard_link(&file, &link).expect("hard link");
1123 let id = |path: &Path| {
1124 platform::file_id_info(&File::open(path).expect("open")).expect("FileIdInfo")
1125 };
1126 assert_eq!(id(&file), id(&link));
1127 assert_ne!(id(&file), id(©));
1128 assert_eq!(
1129 platform::identity(&File::open(&file).expect("open")).expect("identity"),
1130 id(&file),
1131 "NTFS answers FileIdInfo, so the 64-bit index is not used"
1132 );
1133 fs::remove_dir_all(&directory).expect("cleanup");
1134 }
1135}