|
|
|
|
@ -796,10 +796,10 @@ fn update_key() -> String {
|
|
|
|
|
/// Windows installer. `Err` says why this copy is not one an installer made:
|
|
|
|
|
/// it was built from source, or unpacked by a package manager from the
|
|
|
|
|
/// portable archive, and is updated the way it came.
|
|
|
|
|
fn installed_by(bundle: Option<BundleType>, exe: &Path) -> Result<&'static str, String> {
|
|
|
|
|
fn installed_by(bundle: Option<BundleType>, exe: &Path, read_only: bool) -> Result<&'static str, String> {
|
|
|
|
|
match bundle {
|
|
|
|
|
Some(BundleType::App) if !exe.parent().is_some_and(|d| d.ends_with("Contents/MacOS")) => Err("this copy was built from source, not installed as an app, so it does not update itself".into()),
|
|
|
|
|
Some(BundleType::App) if exe.starts_with("/Volumes") => Err("this copy runs from its disk image, which cannot be changed; move Quantum Studio to Applications and open it there".into()),
|
|
|
|
|
Some(BundleType::App) if read_only => Err("this copy runs from its disk image, or another place that cannot be changed; move Quantum Studio to Applications and open it there".into()),
|
|
|
|
|
Some(BundleType::App) => Ok("app"),
|
|
|
|
|
Some(BundleType::AppImage) => Ok("appimage"),
|
|
|
|
|
Some(BundleType::Deb) => Ok("deb"),
|
|
|
|
|
@ -810,14 +810,27 @@ fn installed_by(bundle: Option<BundleType>, exe: &Path) -> Result<&'static str,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Whether `path` is on a volume that cannot be written: a mounted disk image, or where
|
|
|
|
|
/// Gatekeeper's App Translocation runs an app it has not seen moved. An app there cannot be
|
|
|
|
|
/// replaced, so it cannot update itself; on any other volume, an external drive among them, it can.
|
|
|
|
|
#[cfg(target_os = "macos")]
|
|
|
|
|
fn on_read_only_volume(path: &Path) -> bool {
|
|
|
|
|
rustix::fs::statvfs(path).is_ok_and(|v| v.f_flag.contains(rustix::fs::StatVfsMountFlags::RDONLY))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_os = "macos"))]
|
|
|
|
|
fn on_read_only_volume(_path: &Path) -> bool {
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Whether this copy updates itself: `Ok` names the installer it came from,
|
|
|
|
|
/// `Err` says why it does not. It does only when this build carries the
|
|
|
|
|
/// release's public key and the copy came from its own installer.
|
|
|
|
|
fn updates_itself(key: &str, bundle: Option<BundleType>, exe: &Path) -> Result<&'static str, String> {
|
|
|
|
|
fn updates_itself(key: &str, bundle: Option<BundleType>, exe: &Path, read_only: bool) -> Result<&'static str, String> {
|
|
|
|
|
if key.trim().is_empty() {
|
|
|
|
|
return Err("this build carries no update key, so it does not update itself; newer versions are on the release page".into());
|
|
|
|
|
}
|
|
|
|
|
installed_by(bundle, exe)
|
|
|
|
|
installed_by(bundle, exe, read_only)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// `update-status`: the installer this copy came from (or null) and whether
|
|
|
|
|
@ -826,8 +839,9 @@ fn updates_itself(key: &str, bundle: Option<BundleType>, exe: &Path) -> Result<&
|
|
|
|
|
fn update_status() -> Result<String, String> {
|
|
|
|
|
let exe = tauri::utils::platform::current_exe().map_err(|e| e.to_string())?;
|
|
|
|
|
let bundle = tauri::utils::platform::bundle_type();
|
|
|
|
|
let it = updates_itself(&update_key(), bundle.clone(), &exe);
|
|
|
|
|
Ok(json!({ "installed_by": installed_by(bundle, &exe).ok(), "updates_itself": it.is_ok(), "why_not": it.err() }).to_string())
|
|
|
|
|
let read_only = on_read_only_volume(&exe);
|
|
|
|
|
let it = updates_itself(&update_key(), bundle.clone(), &exe, read_only);
|
|
|
|
|
Ok(json!({ "installed_by": installed_by(bundle, &exe, read_only).ok(), "updates_itself": it.is_ok(), "why_not": it.err() }).to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Text that the update manifest carries as base64, decoded.
|
|
|
|
|
@ -866,7 +880,7 @@ struct PendingUpdate(Mutex<Option<tauri_plugin_updater::Update>>);
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
async fn update_check(app: tauri::AppHandle, pending: tauri::State<'_, PendingUpdate>) -> Result<String, String> {
|
|
|
|
|
let exe = tauri::utils::platform::current_exe().map_err(|e| e.to_string())?;
|
|
|
|
|
if let Err(reason) = updates_itself(&update_key(), tauri::utils::platform::bundle_type(), &exe) {
|
|
|
|
|
if let Err(reason) = updates_itself(&update_key(), tauri::utils::platform::bundle_type(), &exe, on_read_only_volume(&exe)) {
|
|
|
|
|
return Ok(json!({ "state": "off", "reason": reason }).to_string());
|
|
|
|
|
}
|
|
|
|
|
let current = app.package_info().version.to_string();
|
|
|
|
|
@ -1185,25 +1199,38 @@ mod tests {
|
|
|
|
|
fn only_a_copy_installed_by_its_own_installer_updates_itself() {
|
|
|
|
|
let app = Path::new("/Applications/Quantum Studio.app/Contents/MacOS/wai-quantum-desktop");
|
|
|
|
|
let key = b64(TEST_KEY);
|
|
|
|
|
assert_eq!(updates_itself(&key, Some(BundleType::App), app), Ok("app"));
|
|
|
|
|
assert_eq!(updates_itself(&key, Some(BundleType::AppImage), Path::new("/home/a/Quantum Studio.AppImage")), Ok("appimage"));
|
|
|
|
|
assert_eq!(updates_itself(&key, Some(BundleType::Deb), Path::new("/usr/bin/wai-quantum-desktop")), Ok("deb"));
|
|
|
|
|
assert_eq!(updates_itself(&key, Some(BundleType::Rpm), Path::new("/usr/bin/wai-quantum-desktop")), Ok("rpm"));
|
|
|
|
|
assert_eq!(updates_itself(&key, Some(BundleType::Nsis), Path::new("C:/Users/a/AppData/Local/Quantum Studio/wai-quantum-desktop.exe")), Ok("nsis"));
|
|
|
|
|
assert_eq!(updates_itself(&key, Some(BundleType::App), app, false), Ok("app"));
|
|
|
|
|
// An app on an external drive is under /Volumes too, and it can update itself.
|
|
|
|
|
let external = Path::new("/Volumes/Scratch/Applications/Quantum Studio.app/Contents/MacOS/wai-quantum-desktop");
|
|
|
|
|
assert_eq!(updates_itself(&key, Some(BundleType::App), external, false), Ok("app"));
|
|
|
|
|
assert_eq!(updates_itself(&key, Some(BundleType::AppImage), Path::new("/home/a/Quantum Studio.AppImage"), false), Ok("appimage"));
|
|
|
|
|
assert_eq!(updates_itself(&key, Some(BundleType::Deb), Path::new("/usr/bin/wai-quantum-desktop"), false), Ok("deb"));
|
|
|
|
|
assert_eq!(updates_itself(&key, Some(BundleType::Rpm), Path::new("/usr/bin/wai-quantum-desktop"), false), Ok("rpm"));
|
|
|
|
|
assert_eq!(updates_itself(&key, Some(BundleType::Nsis), Path::new("C:/Users/a/AppData/Local/Quantum Studio/wai-quantum-desktop.exe"), false), Ok("nsis"));
|
|
|
|
|
let off = |r: Result<&str, String>, why: &str| assert!(r.as_ref().is_err_and(|e| e.contains(why)), "{r:?} should say {why:?}");
|
|
|
|
|
off(updates_itself("", Some(BundleType::App), app), "carries no update key");
|
|
|
|
|
off(updates_itself(&key, Some(BundleType::App), Path::new("/src/target/release/wai-quantum-desktop")), "built from source");
|
|
|
|
|
off(updates_itself(&key, Some(BundleType::App), Path::new("/Volumes/Quantum Studio/Quantum Studio.app/Contents/MacOS/wai-quantum-desktop")), "disk image");
|
|
|
|
|
off(updates_itself(&key, None, Path::new("C:/Users/a/scoop/apps/wai-quantum-desktop/current/wai-quantum-desktop.exe")), "updated the way it came");
|
|
|
|
|
off(updates_itself("", Some(BundleType::App), app, false), "carries no update key");
|
|
|
|
|
off(updates_itself(&key, Some(BundleType::App), Path::new("/src/target/release/wai-quantum-desktop"), false), "built from source");
|
|
|
|
|
// A disk image can be mounted anywhere: what says so is that the volume cannot be written.
|
|
|
|
|
off(updates_itself(&key, Some(BundleType::App), Path::new("/private/tmp/m/Quantum Studio.app/Contents/MacOS/wai-quantum-desktop"), true), "disk image");
|
|
|
|
|
off(updates_itself(&key, None, Path::new("C:/Users/a/scoop/apps/wai-quantum-desktop/current/wai-quantum-desktop.exe"), false), "updated the way it came");
|
|
|
|
|
// How a copy was installed does not depend on the key.
|
|
|
|
|
assert_eq!(installed_by(Some(BundleType::AppImage), Path::new("/a")), Ok("appimage"));
|
|
|
|
|
assert!(installed_by(None, Path::new("/a")).is_err());
|
|
|
|
|
assert_eq!(installed_by(Some(BundleType::AppImage), Path::new("/a"), false), Ok("appimage"));
|
|
|
|
|
assert!(installed_by(None, Path::new("/a"), false).is_err());
|
|
|
|
|
// This test's own binary was built by cargo: no installer made it.
|
|
|
|
|
let s: Value = serde_json::from_str(&update_status().unwrap()).unwrap();
|
|
|
|
|
assert_eq!((s["installed_by"].as_str(), s["updates_itself"].as_bool()), (None, Some(false)), "{s}");
|
|
|
|
|
assert!(s["why_not"].is_string(), "{s}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The system volume is read-only on every Mac that runs this, and a temporary folder is not.
|
|
|
|
|
#[cfg(target_os = "macos")]
|
|
|
|
|
#[test]
|
|
|
|
|
fn a_read_only_volume_is_told_from_a_writable_one() {
|
|
|
|
|
assert!(on_read_only_volume(Path::new("/System/Library/CoreServices")));
|
|
|
|
|
assert!(!on_read_only_volume(&std::env::temp_dir()));
|
|
|
|
|
assert!(!on_read_only_volume(Path::new("/no/such/path")), "not known to be read-only");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The updater asks one address, the update manifest the website serves,
|
|
|
|
|
/// over https, and installs only an update signed for the version it
|
|
|
|
|
/// announces; the installers sign updates only when the release asks
|
|
|
|
|
|