macOS: attach disk images one at a time, and retry when the helper is busy

diskimages-helper turns an attach away ('Resource temporarily unavailable')
while it is busy with another image. The DMG tests added in #51 run in
parallel and made this a flaky failure (about 1 run in 5 locally); the
launcher hits the same thing when several apps update at once. Attaches now
take a process-wide lock (which the tests' hdiutil create shares) and back
off and retry (1, 2, 4, 8 s) when something outside the launcher holds it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-11 05:05:38 -04:00
1 parent e69164d8e5
commit 8d76d193db
2 files changed
+31 -2

No files matched your search

+29 -2
View File
@@ -732,13 +732,39 @@ pub mod macos {
}
}
/// Held while this process asks diskimages-helper for something (an attach here; the tests'
/// `hdiutil create` too), since it turns concurrent requests away.
pub(crate) static DISK_IMAGES: std::sync::Mutex<()> = std::sync::Mutex::new(());
pub fn install_dmg(app: &CraftApp, dmg: &Path, existing: Option<&Installed>, paths: &Paths, expect: &Expect<'_>) -> Result<PathBuf, String> {
let mount = paths.cache.join(format!("mount-{}", app.id));
let _ = detach(&mount);
std::fs::create_dir_all(&mount).map_err(|e| e.to_string())?;
let (Some(dmg_s), Some(mount_s)) = (dmg.to_str(), mount.to_str()) else { return Err("unusual characters in the cache folder's path".into()) };
process::capture("hdiutil", &["attach", "-nobrowse", "-readonly", "-noautoopen", "-mountpoint", mount_s, dmg_s], Duration::from_secs(120))
.ok_or("couldn't open the disk image")?;
// diskimages-helper refuses an attach while it is busy with another image ("Resource
// temporarily unavailable"): attach one image at a time (several apps may be updating at
// once), and wait a little and try again when something outside the launcher holds it.
let attached = {
let _one_at_a_time = DISK_IMAGES.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let attach = || {
process::capture(
"hdiutil",
&["attach", "-nobrowse", "-readonly", "-noautoopen", "-mountpoint", mount_s, dmg_s],
Duration::from_secs(120),
)
};
let mut attached = attach();
for wait in [1, 2, 4, 8] {
if attached.is_some() {
break;
}
let _ = detach(&mount);
std::thread::sleep(Duration::from_secs(wait));
attached = attach();
}
attached
};
attached.ok_or("couldn't open the disk image")?;
let result = copy_app(app, &mount, existing, expect);
let _ = detach(&mount);
let _ = std::fs::remove_dir(&mount);
@@ -1358,6 +1384,7 @@ mod tests {
let src = dir.join(format!("src-{version}"));
bundle(&src, version);
let dmg = dir.join(format!("photocraft-{version}-macos-universal.dmg"));
let _one_at_a_time = super::macos::DISK_IMAGES.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let ok = std::process::Command::new("hdiutil")
.args(["create", "-quiet", "-format", "UDZO", "-volname", "PhotoCraft", "-srcfolder"])
.arg(&src)
+2
View File
@@ -357,6 +357,7 @@ mod tests {
let src = dir.path().join("src");
bundle(&src, "0.2.0");
let dmg = dir.path().join("artcraft-launcher-0.2.0-macos-universal.dmg");
let one_at_a_time = install::macos::DISK_IMAGES.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let ok = std::process::Command::new("hdiutil")
.args(["create", "-quiet", "-format", "UDZO", "-volname", "ArtCraft Launcher 0.2.0", "-srcfolder"])
.arg(&src)
@@ -365,6 +366,7 @@ mod tests {
.unwrap()
.success();
assert!(ok, "hdiutil create failed");
drop(one_at_a_time);
let current = apps.join("ArtCraft Launcher.app");
let existing = Installed { version: None, source: InstallSource::MacApp, location: current.display().to_string(), package: None };
// An unsigned test bundle: the signature check has its own tests in `install`.