From 3e8c1c0ce9018a226fd61434626a6f61cd5dda63 Mon Sep 17 00:00:00 2001 From: botandbang <337765928+botandbang@users.noreply.github.com> Date: Sat, 7 Nov 2026 13:34:57 -0500 Subject: [PATCH 1/3] fix(locallibrary): wait for a new Windows volume before writing FAT32 Format-Volume returns, and the partition gets its letter, a moment before Windows can open the volume, so write_fat32's open failed with ERROR_NOT_READY. It now retries that error for up to 10 seconds, as the volume lock after it already does. The disk-image test harness asked Get-DiskImage about an image whose folder was gone, which fails even with SilentlyContinue. A file that's gone isn't attached, since Windows holds an attached image's file open. Fixes LOC-255 --- .../changes/loc-255-disk-image-windows.md | 3 +++ .../src/format/tests/disk_image.rs | 5 ++++ crates/locallibrary/src/format/windows.rs | 23 ++++++++++++++--- .../locallibrary/src/format/windows/tests.rs | 25 +++++++++++++++++++ 4 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 apps/locallibrary/changes/loc-255-disk-image-windows.md diff --git a/apps/locallibrary/changes/loc-255-disk-image-windows.md b/apps/locallibrary/changes/loc-255-disk-image-windows.md new file mode 100644 index 000000000..4e8985544 --- /dev/null +++ b/apps/locallibrary/changes/loc-255-disk-image-windows.md @@ -0,0 +1,3 @@ +### Fixed + +- `format` to FAT32 on Windows waits for the new volume to be ready instead of failing with "The device is not ready". diff --git a/crates/locallibrary/src/format/tests/disk_image.rs b/crates/locallibrary/src/format/tests/disk_image.rs index 3df81d498..d56fad4fd 100644 --- a/crates/locallibrary/src/format/tests/disk_image.rs +++ b/crates/locallibrary/src/format/tests/disk_image.rs @@ -235,7 +235,12 @@ mod os { format!(r"\\.\PhysicalDrive{}", n.trim()) } + /// Windows keeps an attached image's file open, so one that's gone isn't attached. Asked about + /// a file that's gone, `Get-DiskImage` fails even with `SilentlyContinue`. pub fn attached(file: &Path) -> Option { + if !file.exists() { + return None; + } let path = quote(&file.to_string_lossy()); let n = powershell(&format!( "$i = Get-DiskImage -ImagePath {path} -ErrorAction SilentlyContinue\nif ($i.Attached) {{ $i.Number }}" diff --git a/crates/locallibrary/src/format/windows.rs b/crates/locallibrary/src/format/windows.rs index 02f08a7bf..b5df45353 100644 --- a/crates/locallibrary/src/format/windows.rs +++ b/crates/locallibrary/src/format/windows.rs @@ -234,10 +234,7 @@ fn write_fat32(letter: char, layout: &fat32::Layout, label: &str, ctx: &Ctx<'_>) use std::os::windows::io::AsRawHandle; let path = format!(r"\\.\{letter}:"); - let mut f = std::fs::OpenOptions::new() - .read(true) - .write(true) - .open(&path) + let mut f = when_ready(|| std::fs::OpenOptions::new().read(true).write(true).open(&path)) .map_err(|e| Error::Io { path: path.clone().into(), source: e })?; // Explorer and indexers open new volumes briefly; retry the lock for a few seconds. let mut tries = 0; @@ -260,6 +257,24 @@ fn write_fat32(letter: char, layout: &fat32::Layout, label: &str, ctx: &Ctx<'_>) Ok(()) } +/// `open` once the volume it opens is ready: a volume Windows has just formatted and given a +/// letter answers ERROR_NOT_READY for a moment before it can be opened. Waits up to 10 seconds. +fn when_ready(mut open: impl FnMut() -> std::io::Result) -> std::io::Result { + const ERROR_NOT_READY: i32 = 21; + // The tests don't wait. + let pause = std::time::Duration::from_millis(if cfg!(test) { 0 } else { 250 }); + let mut tries = 0; + loop { + match open() { + Err(e) if e.raw_os_error() == Some(ERROR_NOT_READY) && tries < 40 => { + tries += 1; + std::thread::sleep(pause); + } + done => return done, + } + } +} + #[cfg(not(windows))] fn write_fat32(_: char, _: &fat32::Layout, _: &str, _: &Ctx<'_>) -> Result<()> { Err(Error::invalid("raw volume access is only implemented on Windows")) diff --git a/crates/locallibrary/src/format/windows/tests.rs b/crates/locallibrary/src/format/windows/tests.rs index 259e3943b..419cf3459 100644 --- a/crates/locallibrary/src/format/windows/tests.rs +++ b/crates/locallibrary/src/format/windows/tests.rs @@ -535,3 +535,28 @@ fn prop_fat32_is_laid_out_over_the_partition_printed(tc: TestCase) { other => panic!("{size:?}: {other:?}"), } } + +/// A volume that isn't ready yet is opened once it is; any other error, or one that stays not +/// ready, is returned. +#[test] +fn a_new_volume_is_opened_once_its_ready() { + let not_ready = || std::io::Error::from_raw_os_error(21); + let mut calls = 0; + let opened = when_ready(|| { + calls += 1; + if calls < 3 { Err(not_ready()) } else { Ok(calls) } + }); + assert_eq!(opened.unwrap(), 3); + let mut calls = 0; + let denied = when_ready::<()>(|| { + calls += 1; + Err(std::io::Error::from_raw_os_error(5)) + }); + assert_eq!((denied.unwrap_err().raw_os_error(), calls), (Some(5), 1)); + let mut calls = 0; + let never = when_ready::<()>(|| { + calls += 1; + Err(not_ready()) + }); + assert_eq!((never.unwrap_err().raw_os_error(), calls), (Some(21), 41)); +} From e5247dd54bae7fcf42f7eb07150b9e718dd14d88 Mon Sep 17 00:00:00 2001 From: botandbang <337765928+botandbang@users.noreply.github.com> Date: Sun, 8 Nov 2026 16:55:07 -0500 Subject: [PATCH 2/3] fix(locallibrary): wait for a new Windows volume in tests that format a real disk image --- crates/locallibrary/src/format/windows.rs | 13 +++++++------ crates/locallibrary/src/format/windows/tests.rs | 6 +++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/crates/locallibrary/src/format/windows.rs b/crates/locallibrary/src/format/windows.rs index b5df45353..23c41320d 100644 --- a/crates/locallibrary/src/format/windows.rs +++ b/crates/locallibrary/src/format/windows.rs @@ -234,8 +234,10 @@ fn write_fat32(letter: char, layout: &fat32::Layout, label: &str, ctx: &Ctx<'_>) use std::os::windows::io::AsRawHandle; let path = format!(r"\\.\{letter}:"); - let mut f = when_ready(|| std::fs::OpenOptions::new().read(true).write(true).open(&path)) - .map_err(|e| Error::Io { path: path.clone().into(), source: e })?; + let mut f = when_ready(std::time::Duration::from_millis(250), || { + std::fs::OpenOptions::new().read(true).write(true).open(&path) + }) + .map_err(|e| Error::Io { path: path.clone().into(), source: e })?; // Explorer and indexers open new volumes briefly; retry the lock for a few seconds. let mut tries = 0; while let Err(e) = ffi::ioctl(f.as_raw_handle(), ffi::FSCTL_LOCK_VOLUME) { @@ -258,11 +260,10 @@ fn write_fat32(letter: char, layout: &fat32::Layout, label: &str, ctx: &Ctx<'_>) } /// `open` once the volume it opens is ready: a volume Windows has just formatted and given a -/// letter answers ERROR_NOT_READY for a moment before it can be opened. Waits up to 10 seconds. -fn when_ready(mut open: impl FnMut() -> std::io::Result) -> std::io::Result { +/// letter answers ERROR_NOT_READY for a moment before it can be opened. Tries 41 times, `pause` +/// apart. +fn when_ready(pause: std::time::Duration, mut open: impl FnMut() -> std::io::Result) -> std::io::Result { const ERROR_NOT_READY: i32 = 21; - // The tests don't wait. - let pause = std::time::Duration::from_millis(if cfg!(test) { 0 } else { 250 }); let mut tries = 0; loop { match open() { diff --git a/crates/locallibrary/src/format/windows/tests.rs b/crates/locallibrary/src/format/windows/tests.rs index 419cf3459..d83c50351 100644 --- a/crates/locallibrary/src/format/windows/tests.rs +++ b/crates/locallibrary/src/format/windows/tests.rs @@ -542,19 +542,19 @@ fn prop_fat32_is_laid_out_over_the_partition_printed(tc: TestCase) { fn a_new_volume_is_opened_once_its_ready() { let not_ready = || std::io::Error::from_raw_os_error(21); let mut calls = 0; - let opened = when_ready(|| { + let opened = when_ready(std::time::Duration::ZERO, || { calls += 1; if calls < 3 { Err(not_ready()) } else { Ok(calls) } }); assert_eq!(opened.unwrap(), 3); let mut calls = 0; - let denied = when_ready::<()>(|| { + let denied = when_ready::<()>(std::time::Duration::ZERO, || { calls += 1; Err(std::io::Error::from_raw_os_error(5)) }); assert_eq!((denied.unwrap_err().raw_os_error(), calls), (Some(5), 1)); let mut calls = 0; - let never = when_ready::<()>(|| { + let never = when_ready::<()>(std::time::Duration::ZERO, || { calls += 1; Err(not_ready()) }); From 1d2e93dd078beb07d3a9563b8f2c14c5ef0e0035 Mon Sep 17 00:00:00 2001 From: botandbang <337765928+botandbang@users.noreply.github.com> Date: Mon, 9 Nov 2026 18:07:29 -0500 Subject: [PATCH 3/3] fix(locallibrary): write FAT32 through to the disk instead of flushing a dismounted volume write_fat32 flushed the volume after dismounting it. Once a volume is dismounted, the file system answers anything but reads and writes on that handle with STATUS_VOLUME_DISMOUNTED, which Windows reports as ERROR_NOT_READY: "The device is not ready". The open was never the problem, so the wait before it goes, with its test. Writes to a volume handle skip the cache already, so the volume is now opened with FILE_FLAG_WRITE_THROUGH, which takes over the flush's other job of getting each write past the drive's own cache. --- .../changes/loc-255-disk-image-windows.md | 2 +- crates/locallibrary/src/format/windows.rs | 32 ++++++------------- .../locallibrary/src/format/windows/tests.rs | 25 --------------- 3 files changed, 11 insertions(+), 48 deletions(-) diff --git a/apps/locallibrary/changes/loc-255-disk-image-windows.md b/apps/locallibrary/changes/loc-255-disk-image-windows.md index 4e8985544..99b76631e 100644 --- a/apps/locallibrary/changes/loc-255-disk-image-windows.md +++ b/apps/locallibrary/changes/loc-255-disk-image-windows.md @@ -1,3 +1,3 @@ ### Fixed -- `format` to FAT32 on Windows waits for the new volume to be ready instead of failing with "The device is not ready". +- `format` to FAT32 on Windows no longer fails with "The device is not ready" after writing the new file system. diff --git a/crates/locallibrary/src/format/windows.rs b/crates/locallibrary/src/format/windows.rs index 23c41320d..3c3d47b14 100644 --- a/crates/locallibrary/src/format/windows.rs +++ b/crates/locallibrary/src/format/windows.rs @@ -231,13 +231,19 @@ pub fn format(disk: &Disk, t: &Target, ctx: &Ctx<'_>) -> Result> #[cfg(windows)] fn write_fat32(letter: char, layout: &fat32::Layout, label: &str, ctx: &Ctx<'_>) -> Result<()> { + use std::os::windows::fs::OpenOptionsExt; use std::os::windows::io::AsRawHandle; let path = format!(r"\\.\{letter}:"); - let mut f = when_ready(std::time::Duration::from_millis(250), || { - std::fs::OpenOptions::new().read(true).write(true).open(&path) - }) - .map_err(|e| Error::Io { path: path.clone().into(), source: e })?; + // FILE_FLAG_WRITE_THROUGH: each write reaches the disk before it returns. The volume can't be + // flushed once it's dismounted (that fails as "The device is not ready"), and writes to a + // volume handle skip the cache already, so this is all the flushing they need. + let mut f = std::fs::OpenOptions::new() + .read(true) + .write(true) + .custom_flags(0x8000_0000) + .open(&path) + .map_err(|e| Error::Io { path: path.clone().into(), source: e })?; // Explorer and indexers open new volumes briefly; retry the lock for a few seconds. let mut tries = 0; while let Err(e) = ffi::ioctl(f.as_raw_handle(), ffi::FSCTL_LOCK_VOLUME) { @@ -254,28 +260,10 @@ fn write_fat32(letter: char, layout: &fat32::Layout, label: &str, ctx: &Ctx<'_>) .map_err(|e| Error::Io { path: path.clone().into(), source: e })?; let volume_id = u32::from_le_bytes(uuid::Uuid::new_v4().as_bytes()[..4].try_into().expect("4 bytes")); fat32::write(&mut f, 0, layout, label, volume_id, ctx)?; - f.sync_all().map_err(|e| Error::Io { path: path.into(), source: e })?; // Closing the handle unlocks the volume; Windows mounts it as FAT32 on the next access. Ok(()) } -/// `open` once the volume it opens is ready: a volume Windows has just formatted and given a -/// letter answers ERROR_NOT_READY for a moment before it can be opened. Tries 41 times, `pause` -/// apart. -fn when_ready(pause: std::time::Duration, mut open: impl FnMut() -> std::io::Result) -> std::io::Result { - const ERROR_NOT_READY: i32 = 21; - let mut tries = 0; - loop { - match open() { - Err(e) if e.raw_os_error() == Some(ERROR_NOT_READY) && tries < 40 => { - tries += 1; - std::thread::sleep(pause); - } - done => return done, - } - } -} - #[cfg(not(windows))] fn write_fat32(_: char, _: &fat32::Layout, _: &str, _: &Ctx<'_>) -> Result<()> { Err(Error::invalid("raw volume access is only implemented on Windows")) diff --git a/crates/locallibrary/src/format/windows/tests.rs b/crates/locallibrary/src/format/windows/tests.rs index d83c50351..259e3943b 100644 --- a/crates/locallibrary/src/format/windows/tests.rs +++ b/crates/locallibrary/src/format/windows/tests.rs @@ -535,28 +535,3 @@ fn prop_fat32_is_laid_out_over_the_partition_printed(tc: TestCase) { other => panic!("{size:?}: {other:?}"), } } - -/// A volume that isn't ready yet is opened once it is; any other error, or one that stays not -/// ready, is returned. -#[test] -fn a_new_volume_is_opened_once_its_ready() { - let not_ready = || std::io::Error::from_raw_os_error(21); - let mut calls = 0; - let opened = when_ready(std::time::Duration::ZERO, || { - calls += 1; - if calls < 3 { Err(not_ready()) } else { Ok(calls) } - }); - assert_eq!(opened.unwrap(), 3); - let mut calls = 0; - let denied = when_ready::<()>(std::time::Duration::ZERO, || { - calls += 1; - Err(std::io::Error::from_raw_os_error(5)) - }); - assert_eq!((denied.unwrap_err().raw_os_error(), calls), (Some(5), 1)); - let mut calls = 0; - let never = when_ready::<()>(std::time::Duration::ZERO, || { - calls += 1; - Err(not_ready()) - }); - assert_eq!((never.unwrap_err().raw_os_error(), calls), (Some(21), 41)); -}