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
529#[must_use]
531pub fn process_cpu_time() -> Option<std::time::Duration> {
532 #[cfg(any(unix, windows))]
533 {
534 platform::process_cpu_time()
535 }
536 #[cfg(not(any(unix, windows)))]
537 {
538 None
539 }
540}
541
542pub fn write_diagnostic(line: std::fmt::Arguments<'_>) {
545 use std::io::Write;
546 let _ = writeln!(io::stderr().lock(), "{line}");
547}
548
549#[macro_export]
551macro_rules! diagnostic {
552 ($($argument:tt)*) => {
553 $crate::write_diagnostic(::std::format_args!($($argument)*))
554 };
555}
556
557#[cfg(test)]
558mod tests {
559 use super::*;
560 use std::fs::{self, OpenOptions};
561
562 #[test]
563 fn the_awake_clock_advances_with_time_and_never_goes_back() {
564 let started = AwakeInstant::now();
565 std::thread::sleep(Duration::from_millis(200));
566 let elapsed = started.elapsed();
567 assert!(
568 elapsed >= Duration::from_millis(150) && elapsed < Duration::from_secs(5),
569 "{elapsed:?} for a 200 ms sleep"
570 );
571 let mut last = AwakeInstant::now();
572 for _ in 0..10_000 {
573 let now = AwakeInstant::now();
574 assert!(now >= last, "{now:?} after {last:?}");
575 last = now;
576 }
577 assert_eq!(
578 started
579 .checked_sub(Duration::from_millis(1))
580 .map(|earlier| started.duration_since(earlier)),
581 Some(Duration::from_millis(1))
582 );
583 }
584
585 #[test]
586 #[cfg(any(target_os = "linux", target_os = "macos", windows))]
587 fn the_process_reports_its_resident_memory() {
588 let memory = process_memory();
589 let resident = memory.resident_bytes.expect("resident bytes");
590 let peak = memory.peak_resident_bytes.expect("peak resident bytes");
591 assert!(resident > 1 << 20, "{memory:?}");
592 assert!(peak >= resident, "{memory:?}");
593 }
594
595 #[test]
596 #[cfg(any(unix, windows))]
597 fn the_process_cpu_time_grows_with_its_work() {
598 let started = process_cpu_time().expect("process CPU time");
599 let mut state = 1_u64;
600 let busy = std::time::Instant::now();
601 while busy.elapsed() < std::time::Duration::from_millis(200) {
602 state = std::hint::black_box(state.wrapping_mul(6_364_136_223_846_793_005));
603 }
604 let worked = process_cpu_time()
605 .expect("process CPU time")
606 .saturating_sub(started);
607 assert!(
609 worked >= std::time::Duration::from_millis(150),
610 "{worked:?} of CPU time for 200 ms of work"
611 );
612 }
613
614 #[test]
615 #[cfg(any(unix, windows))]
616 fn the_thread_counts_the_work_it_does_and_not_the_time_it_sleeps() {
617 let started = thread_cpu_work().expect("thread work");
618 std::thread::sleep(std::time::Duration::from_millis(200));
619 let slept = thread_cpu_work().expect("thread work") - started;
620 let mut state = 1_u64;
621 let busy = std::time::Instant::now();
622 while busy.elapsed() < std::time::Duration::from_millis(200) {
623 state = std::hint::black_box(state.wrapping_mul(6_364_136_223_846_793_005));
624 }
625 let worked = thread_cpu_work().expect("thread work") - started - slept;
626 assert!(worked > slept * 4, "slept {slept}, worked {worked}");
627 }
628
629 fn scratch(name: &str) -> std::path::PathBuf {
630 let path = std::env::temp_dir().join(format!("alpha2-os-{}-{name}", std::process::id()));
631 let _ = fs::remove_file(&path);
632 path
633 }
634
635 fn open(path: &Path) -> File {
636 OpenOptions::new()
637 .read(true)
638 .write(true)
639 .create(true)
640 .truncate(false)
641 .open(path)
642 .expect("open")
643 }
644
645 pub(crate) static FORK: std::sync::Mutex<()> = std::sync::Mutex::new(());
648
649 #[test]
650 fn one_handle_at_a_time_holds_the_lock_and_closing_it_releases_it() {
651 let _fork = FORK
652 .lock()
653 .unwrap_or_else(std::sync::PoisonError::into_inner);
654 let path = scratch("lock");
655 let first = open(&path);
656 let second = open(&path);
657 assert!(try_lock_exclusive(&first).expect("lock"));
658 assert!(!try_lock_exclusive(&second).expect("second lock"));
659 drop(first);
660 assert!(try_lock_exclusive(&second).expect("lock after release"));
661 let _ = fs::remove_file(&path);
662 }
663
664 #[test]
665 fn another_handle_writes_the_whole_of_a_locked_file() {
666 use std::io::{Read, Write};
667 let path = scratch("write");
668 let lock = open(&path);
669 assert!(try_lock_exclusive(&lock).expect("lock"));
670 let mut writer = File::create(&path).expect("writer truncates and opens");
671 writer
672 .write_all(&[7_u8; 4096])
673 .expect("write under the lock");
674 drop(writer);
675 let mut read = Vec::new();
676 File::open(&path)
677 .expect("reader")
678 .read_to_end(&mut read)
679 .expect("read under the lock");
680 assert_eq!(read, vec![7_u8; 4096]);
681 drop(lock);
682 let _ = fs::remove_file(&path);
683 }
684
685 const LOCK_PROBE: &str = "ALPHA2_OS_LOCK_PROBE";
687
688 fn another_process_takes_the_lock(path: &Path) -> bool {
690 let status = std::process::Command::new(std::env::current_exe().expect("test binary"))
691 .args([
692 "--exact",
693 "tests::lock_probe_child",
694 "--ignored",
695 "--test-threads=1",
696 ])
697 .env(LOCK_PROBE, path)
698 .stdout(std::process::Stdio::null())
699 .status()
700 .expect("run the lock probe");
701 match status.code() {
702 Some(0) => true,
703 Some(3) => false,
704 _ => panic!("the lock probe failed: {status}"),
705 }
706 }
707
708 #[test]
710 #[ignore = "run by another_process_takes_the_lock"]
711 fn lock_probe_child() {
712 let path = std::env::var_os(LOCK_PROBE).expect("the parent names the file");
713 let locked = try_lock_exclusive(&open(Path::new(&path))).expect("probe lock");
714 std::process::exit(if locked { 0 } else { 3 });
715 }
716
717 #[test]
720 fn the_lock_stays_with_its_handle_when_another_handle_closes() {
721 let _fork = FORK
722 .lock()
723 .unwrap_or_else(std::sync::PoisonError::into_inner);
724 let path = scratch("other-handle");
725 let lock = open(&path);
726 assert!(try_lock_exclusive(&lock).expect("lock"));
727 drop(File::create(&path).expect("another handle opens and closes"));
728 drop(open(&path));
729 assert!(
730 !another_process_takes_the_lock(&path),
731 "closing another handle released the lock"
732 );
733 drop(lock);
734 assert!(
735 another_process_takes_the_lock(&path),
736 "closing the handle that holds the lock releases it"
737 );
738 let _ = fs::remove_file(&path);
739 }
740
741 #[test]
744 #[cfg(target_os = "linux")]
745 fn the_lock_covers_only_one_byte_no_write_reaches() {
746 use std::os::fd::AsRawFd;
747 let path = scratch("one-byte");
748 let lock = open(&path);
749 assert!(try_lock_exclusive(&lock).expect("lock"));
750 let other = open(&path);
751 let probe = |start: i64, len: i64| {
752 let mut query: libc::flock = unsafe { std::mem::zeroed() };
754 query.l_type = libc::c_short::try_from(libc::F_WRLCK).expect("F_WRLCK");
755 query.l_whence = libc::c_short::try_from(libc::SEEK_SET).expect("SEEK_SET");
756 query.l_start = start;
757 query.l_len = len;
758 let asked =
760 unsafe { libc::fcntl(other.as_raw_fd(), libc::F_OFD_GETLK, &raw mut query) };
761 assert_eq!(asked, 0, "F_OFD_GETLK: {}", io::Error::last_os_error());
762 query
763 };
764 let everything_below = probe(0, i64::MAX);
765 assert_eq!(
766 i32::from(everything_below.l_type),
767 libc::F_UNLCK,
768 "no byte a write can reach is locked"
769 );
770 let last = probe(0, 0);
771 assert_eq!(
772 i32::from(last.l_type),
773 libc::F_WRLCK,
774 "the lock is an OFD lock"
775 );
776 assert_eq!(last.l_start, i64::MAX);
778 drop(lock);
779 let _ = fs::remove_file(&path);
780 }
781
782 #[test]
783 fn a_path_moved_away_and_created_again_is_another_file() {
784 let (path, moved) = (scratch("identity"), scratch("identity-moved"));
785 let file = open(&path);
786 assert!(same_file(&file, &path).expect("same"));
787 fs::rename(&path, &moved).expect("move away");
788 assert!(!same_file(&file, &path).expect("missing"));
789 drop(open(&path));
790 assert!(!same_file(&file, &path).expect("replaced"));
791 assert!(same_file(&file, &moved).expect("moved"));
792 let _ = fs::remove_file(&path);
793 let _ = fs::remove_file(&moved);
794 }
795
796 #[test]
797 fn a_hard_link_and_a_path_through_a_linked_directory_are_the_same_file() {
798 let directory = scratch("identity-paths");
799 let _ = fs::remove_dir_all(&directory);
800 fs::create_dir(&directory).expect("directory");
801 let (file, copy, link) = (
802 directory.join("file"),
803 directory.join("copy"),
804 directory.join("link"),
805 );
806 fs::write(&file, b"source").expect("file");
807 fs::write(©, b"source").expect("copy");
808 fs::hard_link(&file, &link).expect("hard link");
809 assert!(same_file_at(&file, &file).expect("itself"));
810 assert!(same_file_at(&file, &link).expect("hard link"));
811 assert!(!same_file_at(&file, ©).expect("copy"));
812 assert!(!same_file_at(&file, &directory.join("missing")).expect("missing"));
813 let linked = scratch("identity-paths-linked");
814 let _ = fs::remove_file(&linked);
815 #[cfg(unix)]
816 std::os::unix::fs::symlink(&directory, &linked).expect("symlink");
817 #[cfg(windows)]
819 {
820 let _fork = FORK
821 .lock()
822 .unwrap_or_else(std::sync::PoisonError::into_inner);
823 let status = std::process::Command::new("cmd")
824 .arg("/C")
825 .arg("mklink")
826 .arg("/J")
827 .arg(&linked)
828 .arg(&directory)
829 .stdout(std::process::Stdio::null())
830 .status()
831 .expect("run mklink");
832 assert!(status.success(), "mklink /J failed: {status}");
833 }
834 assert!(same_file_at(&file, &linked.join("file")).expect("through the link"));
835 assert!(!same_file_at(©, &linked.join("file")).expect("another file"));
836 #[cfg(unix)]
837 fs::remove_file(&linked).expect("cleanup");
838 #[cfg(windows)]
839 fs::remove_dir(&linked).expect("cleanup");
840 fs::remove_dir_all(&directory).expect("cleanup");
841 }
842
843 #[test]
844 fn a_removed_file_still_open_is_not_the_file_its_path_names() {
845 let path = scratch("identity-removed");
847 let file = open(&path);
848 fs::remove_file(&path).expect("remove");
849 assert!(!same_file(&file, &path).expect("removed"));
850 drop(file);
851 let again = open(&path);
852 assert!(same_file(&again, &path).expect("created again"));
853 drop(again);
854 let _ = fs::remove_file(&path);
855 }
856
857 #[test]
858 fn a_publish_moves_a_file_to_a_free_name_and_never_replaces_one() {
859 let (from, to) = (scratch("publish-from"), scratch("publish-to"));
860 fs::write(&from, b"export").expect("from");
861 fs::write(&to, b"someone else's").expect("to");
862 let refused = publish_no_replace(&from, &to, &RetryPolicy::NONE)
863 .expect_err("an existing file is not replaced");
864 assert_eq!(refused.kind(), io::ErrorKind::AlreadyExists);
865 assert_eq!(fs::read(&to).expect("to"), b"someone else's");
866 assert_eq!(fs::read(&from).expect("from"), b"export");
867 fs::remove_file(&to).expect("free the name");
868 let (probe_from, probe_to) = (scratch("publish-probe-from"), scratch("publish-probe-to"));
870 fs::write(&probe_from, b"probe").expect("probe");
871 let method = match platform::rename_exclusive(&probe_from, &probe_to) {
872 Err(error) if platform::flag_refused(&error) => PublishMethod::PlaceholderRename,
873 answer => {
874 answer.expect("the no-replace rename");
875 PublishMethod::RenameNoReplace
876 }
877 };
878 let _ = (fs::remove_file(&probe_from), fs::remove_file(&probe_to));
879 let published =
880 publish_no_replace(&from, &to, &RetryPolicy::NONE).expect("publish to a free name");
881 assert_eq!(published, Published { method, retries: 0 });
882 assert_eq!(fs::read(&to).expect("to"), b"export");
883 assert!(!from.exists());
884 let _ = fs::remove_file(&to);
885 }
886
887 #[test]
888 fn a_replacing_publish_moves_a_file_over_another() {
889 let (from, to) = (scratch("replace-from"), scratch("replace-to"));
890 fs::write(&from, b"export").expect("from");
891 fs::write(&to, b"old export").expect("to");
892 let published = publish_replace(&from, &to, &RetryPolicy::NONE).expect("replace");
893 assert_eq!(published.method, PublishMethod::RenameReplace);
894 assert_eq!(fs::read(&to).expect("to"), b"export");
895 assert!(!from.exists());
896 let _ = fs::remove_file(&to);
897 }
898
899 fn flag_refused_error() -> io::Error {
901 #[cfg(unix)]
902 return io::Error::from_raw_os_error(libc::EINVAL);
903 #[cfg(windows)]
904 return io::Error::from_raw_os_error(50);
905 }
906
907 #[test]
908 fn a_file_system_without_the_no_replace_flag_publishes_through_a_placeholder() {
909 let (from, to) = (scratch("placeholder-from"), scratch("placeholder-to"));
910 let _ = fs::remove_file(&to);
911 fs::write(&from, b"export").expect("from");
912 let no_flag = |_: &Path, _: &Path| Err(flag_refused_error());
913 let published = publish_no_replace_with(&from, &to, &RetryPolicy::NONE, &no_flag)
914 .expect("publish through a placeholder");
915 assert_eq!(published.method, PublishMethod::PlaceholderRename);
916 assert_eq!(fs::read(&to).expect("to"), b"export");
917 assert!(!from.exists());
918 fs::write(&from, b"second export").expect("from");
919 let refused = publish_no_replace_with(&from, &to, &RetryPolicy::NONE, &no_flag)
920 .expect_err("an existing file is not replaced");
921 assert_eq!(refused.kind(), io::ErrorKind::AlreadyExists);
922 assert_eq!(fs::read(&to).expect("to"), b"export");
923 assert_eq!(fs::read(&from).expect("from"), b"second export");
924 let _ = fs::remove_file(&from);
925 let _ = fs::remove_file(&to);
926 }
927
928 #[test]
931 #[cfg(unix)]
932 fn a_publish_never_leaves_a_second_link_to_the_published_file() {
933 use std::os::unix::fs::MetadataExt;
934 let (from, to) = (scratch("one-link-from"), scratch("one-link-to"));
935 let _ = fs::remove_file(&to);
936 fs::write(&from, b"export").expect("from");
937 let no_flag = |_: &Path, _: &Path| Err(flag_refused_error());
938 publish_no_replace_with(&from, &to, &RetryPolicy::NONE, &no_flag).expect("publish");
939 assert_eq!(fs::metadata(&to).expect("to").nlink(), 1);
940 assert!(!from.exists());
941 let _ = fs::remove_file(&to);
942 }
943
944 #[test]
945 fn a_failure_another_handle_causes_is_tried_again_until_the_budget_ends() {
946 let busy = || io::Error::from(io::ErrorKind::ResourceBusy);
947 let retryable = |error: &io::Error| error.kind() == io::ErrorKind::ResourceBusy;
948 let policy = RetryPolicy {
949 budget: Duration::from_secs(10),
950 on_wait: &|| true,
951 };
952 let mut tries = 0;
953 let (value, retries) = retrying(&policy, retryable, || {
954 tries += 1;
955 if tries < 3 { Err(busy()) } else { Ok(tries) }
956 })
957 .expect("the third try succeeds");
958 assert_eq!((value, retries), (3, 2));
959
960 let short = RetryPolicy {
961 budget: Duration::from_millis(200),
962 on_wait: &|| true,
963 };
964 let started = Instant::now();
965 let error = retrying(&short, retryable, || Err::<(), _>(busy())).expect_err("never");
966 assert_eq!(error.kind(), io::ErrorKind::ResourceBusy);
967 let elapsed = started.elapsed();
968 assert!(
969 elapsed >= Duration::from_millis(200) && elapsed < Duration::from_secs(5),
970 "{elapsed:?}"
971 );
972
973 let cancelled = RetryPolicy {
974 budget: Duration::from_secs(10),
975 on_wait: &|| false,
976 };
977 let mut tries = 0;
978 assert!(
979 retrying(&cancelled, retryable, || {
980 tries += 1;
981 Err::<(), _>(busy())
982 })
983 .is_err()
984 );
985 assert_eq!(tries, 1, "a cancelled wait stops trying");
986
987 let mut tries = 0;
988 assert!(
989 retrying(&policy, retryable, || {
990 tries += 1;
991 Err::<(), _>(io::Error::from(io::ErrorKind::NotFound))
992 })
993 .is_err()
994 );
995 assert_eq!(tries, 1, "another error is not tried again");
996 }
997
998 #[test]
999 #[cfg(unix)]
1000 fn a_directory_syncs() {
1001 sync_directory(&std::env::temp_dir()).expect("sync the temporary directory");
1002 }
1003
1004 #[test]
1005 fn the_name_limit_of_the_temporary_directory_is_known() {
1006 let limit = name_limit(&std::env::temp_dir());
1007 assert!(limit.max >= 143, "{limit:?}");
1008 assert_eq!(limit.length("ab"), 2);
1009 assert_eq!(
1010 limit.length("\u{d55c}"),
1011 if limit.unit == NameUnit::Bytes { 3 } else { 1 }
1012 );
1013 }
1014
1015 #[cfg(windows)]
1017 fn long_directory(label: &str) -> std::path::PathBuf {
1018 let root = scratch(label);
1019 let _ = fs::remove_dir_all(&root);
1020 let directory = root.join("a".repeat(120)).join("b".repeat(120));
1021 fs::create_dir_all(&directory).expect("long directory");
1022 directory
1023 }
1024
1025 #[test]
1026 #[cfg(windows)]
1027 fn a_publish_reaches_a_path_longer_than_max_path() {
1028 let directory = long_directory("long-path");
1029 let (from, to) = (directory.join("from.mp4"), directory.join("to.mp4"));
1030 assert!(!to.as_os_str().to_string_lossy().starts_with(r"\\?\"));
1031 assert!(to.as_os_str().len() > 260);
1032 fs::write(&from, b"export").expect("from");
1033 let published = publish_no_replace(&from, &to, &RetryPolicy::NONE).expect("no replace");
1034 assert_eq!(published.method, PublishMethod::RenameNoReplace);
1035 fs::write(&from, b"second export").expect("from");
1036 publish_replace(&from, &to, &RetryPolicy::NONE).expect("replace");
1037 assert_eq!(fs::read(&to).expect("to"), b"second export");
1038 let _ = fs::remove_dir_all(scratch("long-path"));
1039 }
1040
1041 #[cfg(windows)]
1043 fn hold_without_delete_sharing(path: &Path) -> File {
1044 use std::os::windows::fs::OpenOptionsExt;
1045 OpenOptions::new()
1046 .read(true)
1047 .share_mode(1)
1048 .open(path)
1049 .expect("hold the file")
1050 }
1051
1052 #[test]
1053 #[cfg(windows)]
1054 fn a_publish_waits_for_a_reader_without_delete_sharing() {
1055 let policy = RetryPolicy {
1056 budget: Duration::from_secs(10),
1057 on_wait: &|| true,
1058 };
1059 let (from, to) = (scratch("held-from"), scratch("held-to"));
1060 let _ = fs::remove_file(&to);
1061 fs::write(&from, b"export").expect("from");
1062 fs::write(&to, b"old export").expect("to");
1063 let holder = hold_without_delete_sharing(&to);
1064 let released = std::thread::spawn(move || {
1065 std::thread::sleep(Duration::from_millis(300));
1066 drop(holder);
1067 });
1068 let published = publish_replace(&from, &to, &policy).expect("replace once released");
1069 released.join().expect("holder");
1070 assert!(published.retries >= 1, "{published:?}");
1071 assert_eq!(fs::read(&to).expect("to"), b"export");
1072 fs::remove_file(&to).expect("free the name");
1073
1074 fs::write(&from, b"export").expect("from");
1075 let holder = hold_without_delete_sharing(&from);
1076 let released = std::thread::spawn(move || {
1077 std::thread::sleep(Duration::from_millis(300));
1078 drop(holder);
1079 });
1080 let published = publish_no_replace(&from, &to, &policy).expect("publish once released");
1081 released.join().expect("holder");
1082 assert!(published.retries >= 1, "{published:?}");
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 short = RetryPolicy {
1088 budget: Duration::from_millis(500),
1089 on_wait: &|| true,
1090 };
1091 let error = publish_no_replace(&from, &to, &short).expect_err("never released");
1092 assert!(is_retryable_publish_error(&error), "{error}");
1093 drop(holder);
1094 assert!(from.exists(), "a refused publish keeps its source");
1095 let _ = fs::remove_file(&from);
1096 }
1097
1098 #[test]
1099 #[cfg(windows)]
1100 fn two_names_of_one_file_have_one_identity_and_two_files_two() {
1101 let directory = scratch("file-id");
1102 let _ = fs::remove_dir_all(&directory);
1103 fs::create_dir(&directory).expect("directory");
1104 let (file, link, copy) = (
1105 directory.join("file"),
1106 directory.join("link"),
1107 directory.join("copy"),
1108 );
1109 fs::write(&file, b"source").expect("file");
1110 fs::write(©, b"source").expect("copy");
1111 fs::hard_link(&file, &link).expect("hard link");
1112 let id = |path: &Path| {
1113 platform::file_id_info(&File::open(path).expect("open")).expect("FileIdInfo")
1114 };
1115 assert_eq!(id(&file), id(&link));
1116 assert_ne!(id(&file), id(©));
1117 assert_eq!(
1118 platform::identity(&File::open(&file).expect("open")).expect("identity"),
1119 id(&file),
1120 "NTFS answers FileIdInfo, so the 64-bit index is not used"
1121 );
1122 fs::remove_dir_all(&directory).expect("cleanup");
1123 }
1124}