diff --git a/crates/fig_desktop/src/install.rs b/crates/fig_desktop/src/install.rs index a7dd40d81..06497479f 100644 --- a/crates/fig_desktop/src/install.rs +++ b/crates/fig_desktop/src/install.rs @@ -115,7 +115,7 @@ pub async fn run_install(ctx: Arc, ignore_immediate_update: bool) { use tokio::time::timeout; // Check for updates but timeout after 3 seconds to avoid making the user wait too long // todo: don't download the index file twice - match timeout(Duration::from_secs(3), check_for_updates(true)).await { + match timeout(Duration::from_secs(3), check_for_updates(true, true)).await { Ok(Ok(Some(_))) => { crate::update::check_for_update(true, true).await; }, diff --git a/crates/fig_desktop/src/tray.rs b/crates/fig_desktop/src/tray.rs index 9819c93bc..a25fae463 100644 --- a/crates/fig_desktop/src/tray.rs +++ b/crates/fig_desktop/src/tray.rs @@ -1,21 +1,17 @@ use std::borrow::Cow; use cfg_if::cfg_if; +use fig_install::index::get_file_type; use fig_install::{ InstallComponents, UpdateOptions, }; -use fig_os_shim::{ - Context, - Os, -}; +use fig_os_shim::Context; use fig_remote_ipc::figterm::FigtermState; use fig_util::consts::PRODUCT_NAME; use fig_util::manifest::{ FileType, Variant, - bundle_metadata, - manifest, }; use fig_util::url::USER_MANUAL; use muda::{ @@ -101,6 +97,7 @@ fn tray_update(proxy: &EventLoopProxy) { ignore_rollout: true, interactive: true, relaunch_dashboard: true, + is_auto_update: false, }, ) .await; @@ -140,24 +137,15 @@ fn tray_update(proxy: &EventLoopProxy) { /// continuing. /// /// Returns `true` if we should continue with updating, `false` otherwise. -/// -/// Currently only the Linux flow gets affected, since some bundles (eg, `AppImage`) are able to -/// update and others (packages like `deb`) cannot. async fn should_continue_with_update(ctx: &Context, proxy: &EventLoopProxy) -> bool { - if !(ctx.platform().os() == Os::Linux && manifest().variant == Variant::Full) { - return true; - } - - match fig_install::check_for_updates(true).await { + match fig_install::check_for_updates(true, false).await { Ok(Some(pkg)) => { - let file_type = bundle_metadata(&ctx) + let file_type = get_file_type(ctx, &Variant::Full) .await - .map_err(|err| error!(?err, "Failed to get bundle metadata")) - .ok() - .flatten() - .map(|md| md.packaged_as); - // Only AppImage is able to self-update. - if file_type == Some(FileType::AppImage) { + .map_err(|err| error!(?err, "Failed to get file type")) + .ok(); + // Only AppImage and dmg is able to self-update. + if file_type == Some(FileType::AppImage) || file_type == Some(FileType::Dmg) { let (tx, mut rx) = tokio::sync::mpsc::channel(1); proxy .send_event( diff --git a/crates/fig_desktop/src/update.rs b/crates/fig_desktop/src/update.rs index 68d47d5e3..ebeed07a9 100644 --- a/crates/fig_desktop/src/update.rs +++ b/crates/fig_desktop/src/update.rs @@ -101,6 +101,7 @@ pub async fn check_for_update(show_webview: bool, relaunch_dashboard: bool) -> b ignore_rollout: false, interactive: show_webview, relaunch_dashboard, + is_auto_update: true, }) .await { diff --git a/crates/fig_desktop_api/src/requests/update.rs b/crates/fig_desktop_api/src/requests/update.rs index 622585df2..be2b82db5 100644 --- a/crates/fig_desktop_api/src/requests/update.rs +++ b/crates/fig_desktop_api/src/requests/update.rs @@ -20,13 +20,14 @@ pub async fn update_application(request: UpdateApplicationRequest) -> RequestRes ignore_rollout: request.ignore_rollout.unwrap_or(true), interactive: request.interactive.unwrap_or(true), relaunch_dashboard: request.relaunch_dashboard.unwrap_or(true), + is_auto_update: false, }, )); RequestResult::success() } pub async fn check_for_updates(_request: CheckForUpdatesRequest) -> RequestResult { - fig_install::check_for_updates(true) + fig_install::check_for_updates(true, false) .await .map(|res| { Box::new(ServerOriginatedSubMessage::CheckForUpdatesResponse( diff --git a/crates/fig_install/src/index.rs b/crates/fig_install/src/index.rs index 91ac3c5ac..1e862c652 100644 --- a/crates/fig_install/src/index.rs +++ b/crates/fig_install/src/index.rs @@ -101,6 +101,7 @@ impl Index { /// than the currently installed version*. This is useful to check if an update exists for the /// given target and variant without filtering on file type, e.g. in the case of Linux desktop /// bundles. + #[allow(clippy::too_many_arguments)] pub fn find_next_version( &self, target_triple: &TargetTriple, @@ -108,6 +109,7 @@ impl Index { file_type: Option<&FileType>, current_version: &str, ignore_rollout: bool, + is_auto_update: bool, threshold_override: Option, ) -> Result, Error> { if !self.supported.iter().any(|support| { @@ -137,6 +139,7 @@ impl Index { Some(rollout) => rollout.start <= right_now, None => true, }) + .filter(|version| !is_auto_update || !version.disable_autoupdate) .collect::>(); valid_versions.sort_unstable_by(|lhs, rhs| lhs.version.cmp(&rhs.version)); @@ -252,20 +255,22 @@ struct Support { file_type: Option, } -#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] pub(crate) struct RemoteVersion { pub version: Version, pub rollout: Option, pub packages: Vec, + #[serde(default)] + pub disable_autoupdate: bool, } -#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] pub(crate) struct Rollout { start: u64, end: u64, } -#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Package { #[serde(deserialize_with = "deser_enum_other")] @@ -306,7 +311,7 @@ pub struct UpdatePackage { pub cli_path: Option, } -#[derive(Deserialize, Serialize, PartialEq, Eq, EnumString, Debug, Display)] +#[derive(Deserialize, Serialize, PartialEq, Eq, EnumString, Debug, Clone, Display)] #[serde(rename_all = "camelCase")] #[strum(serialize_all = "camelCase")] pub enum PackageArchitecture { @@ -360,14 +365,21 @@ pub async fn check_for_updates( variant: &Variant, file_type: Option<&FileType>, ignore_rollout: bool, + is_auto_update: bool, ) -> Result, Error> { const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION"); - pull(&channel) - .await? - .find_next_version(target_triple, variant, file_type, CURRENT_VERSION, ignore_rollout, None) + pull(&channel).await?.find_next_version( + target_triple, + variant, + file_type, + CURRENT_VERSION, + ignore_rollout, + is_auto_update, + None, + ) } -pub(crate) async fn get_file_type(ctx: &Context, variant: &Variant) -> Result { +pub async fn get_file_type(ctx: &Context, variant: &Variant) -> Result { match ctx.platform().os() { fig_os_shim::Os::Mac => Ok(FileType::Dmg), fig_os_shim::Os::Linux => match variant { @@ -429,6 +441,7 @@ mod tests { &Variant::Full, Some(FileType::Dmg).as_ref(), false, + false, ) .await .unwrap(); @@ -515,7 +528,8 @@ mod tests { "sha256": "5a6abea56bfa91bd58d49fe40322058d0efea825f7e19f7fb7db1c204ae625b6", "size": 76836772, } - ] + ], + "disable_autoupdate": true, }, { "version": "2.0.0", @@ -560,6 +574,11 @@ mod tests { assert_eq!(index.versions.len(), 4); + assert!( + !index.versions[1].disable_autoupdate, + "missing disable_autoupdate field should default to false" + ); + // check the 1.0.0 entry matches assert_eq!(index.versions[2], RemoteVersion { version: Version::new(1, 0, 0), @@ -588,6 +607,7 @@ mod tests { cli_path: None, } ], + disable_autoupdate: true, }); } @@ -604,6 +624,7 @@ mod tests { Some(&FileType::TarZst), "1.2.1", true, + false, None, ) .unwrap(); @@ -619,6 +640,7 @@ mod tests { Some(&FileType::TarZst), "1.2.0", true, + false, None, ) .unwrap() @@ -635,6 +657,7 @@ mod tests { Some(&FileType::TarZst), "1.2.1", true, + false, None, ); assert!(next.is_err()); @@ -649,10 +672,50 @@ mod tests { None, "1.0.5", true, + false, None, ) .unwrap() .expect("should have update package"); assert_eq!(next.version.to_string().as_str(), "1.2.1"); } + + #[test] + fn index_autoupdate_does_not_update_into_disabled() { + let mut index = load_test_index(); + + let next = index + .find_next_version( + &TargetTriple::X86_64UnknownLinuxGnu, + &Variant::Full, + None, + "1.0.5", + true, + true, + None, + ) + .unwrap() + .expect("should have update package"); + assert_eq!(next.version.to_string().as_str(), "1.2.0"); + + // Push a newer update that does not have autoupdate disabled + let mut last = index.versions.last().cloned().unwrap(); + last.version = Version::from_str("2.0.0").unwrap(); + last.disable_autoupdate = false; + index.versions.push(last); + + let next = index + .find_next_version( + &TargetTriple::X86_64UnknownLinuxGnu, + &Variant::Full, + None, + "1.0.5", + true, + true, + None, + ) + .unwrap() + .expect("should have update package"); + assert_eq!(next.version.to_string().as_str(), "2.0.0"); + } } diff --git a/crates/fig_install/src/lib.rs b/crates/fig_install/src/lib.rs index 95d5abab2..09bc68cb2 100644 --- a/crates/fig_install/src/lib.rs +++ b/crates/fig_install/src/lib.rs @@ -144,7 +144,7 @@ pub fn get_max_channel() -> Channel { .unwrap() } -pub async fn check_for_updates(ignore_rollout: bool) -> Result, Error> { +pub async fn check_for_updates(ignore_rollout: bool, is_auto_update: bool) -> Result, Error> { let manifest = manifest(); let ctx = Context::new(); let file_type = match (&manifest.variant, ctx.platform().os()) { @@ -157,6 +157,7 @@ pub async fn check_for_updates(ignore_rollout: bool) -> Result Result { info!("Checking for updates..."); - if let Some(update) = check_for_updates(ignore_rollout).await? { + if let Some(update) = check_for_updates(ignore_rollout, is_auto_update).await? { info!("Found update: {}", update.version); debug!("Update info: {:?}", update); diff --git a/crates/fig_install/test_files/test-index.json b/crates/fig_install/test_files/test-index.json index 435e4a319..72d1871e1 100644 --- a/crates/fig_install/test_files/test-index.json +++ b/crates/fig_install/test_files/test-index.json @@ -487,6 +487,7 @@ { "version": "1.2.1", + "disable_autoupdate": true, "packages": [ { "kind": "deb", diff --git a/crates/fig_util/src/manifest.rs b/crates/fig_util/src/manifest.rs index b560c6d56..0027fc23e 100644 --- a/crates/fig_util/src/manifest.rs +++ b/crates/fig_util/src/manifest.rs @@ -49,7 +49,7 @@ pub enum ManagedBy { /// The target triplet, describes a platform on which the project is build for. Note that this also /// includes "fake" targets like `universal-apple-darwin` as provided by [Tauri](https://tauri.app/v1/guides/building/macos/#binary-targets) -#[derive(Deserialize, Serialize, PartialEq, Eq, EnumString, Debug, Display)] +#[derive(Deserialize, Serialize, PartialEq, Eq, EnumString, Debug, Clone, Display)] pub enum TargetTriple { #[serde(rename = "universal-apple-darwin")] #[strum(serialize = "universal-apple-darwin")] diff --git a/crates/figterm/src/update.rs b/crates/figterm/src/update.rs index 043413ccc..2a03afcb1 100644 --- a/crates/figterm/src/update.rs +++ b/crates/figterm/src/update.rs @@ -55,7 +55,7 @@ pub fn check_for_update(context: &Context) { } tokio::spawn(async { - match fig_install::check_for_updates(false).await { + match fig_install::check_for_updates(false, true).await { Ok(Some(pkg)) => { if let Err(err) = fig_settings::state::set_value(UPDATE_AVAILABLE_KEY, pkg.version.to_string()) { warn!(?err, "Error setting {UPDATE_AVAILABLE_KEY}: {err}"); diff --git a/crates/q_cli/src/cli/debug/mod.rs b/crates/q_cli/src/cli/debug/mod.rs index 2d65dbb14..4a6acdefc 100644 --- a/crates/q_cli/src/cli/debug/mod.rs +++ b/crates/q_cli/src/cli/debug/mod.rs @@ -218,6 +218,8 @@ pub enum DebugSubcommand { #[arg(short = 'r', long)] enable_rollout: bool, #[arg(short, long)] + is_auto_update: bool, + #[arg(short, long)] override_threshold: Option, #[arg(short, long)] file_type: String, @@ -748,6 +750,7 @@ impl DebugSubcommand { variant, version: current_version, enable_rollout, + is_auto_update, override_threshold, file_type, } => { @@ -765,6 +768,7 @@ impl DebugSubcommand { Some(&FileType::from_str(file_type)?), current_version, !enable_rollout, + *is_auto_update, *override_threshold, ); diff --git a/crates/q_cli/src/cli/update.rs b/crates/q_cli/src/cli/update.rs index 86f669e0f..43ce6742b 100644 --- a/crates/q_cli/src/cli/update.rs +++ b/crates/q_cli/src/cli/update.rs @@ -102,6 +102,7 @@ impl UpdateArgs { ignore_rollout: !rollout, interactive: !non_interactive, relaunch_dashboard: *relaunch_dashboard, + is_auto_update: false, }, ) .await; @@ -129,7 +130,10 @@ impl UpdateArgs { } async fn try_linux_update() -> Result { - match (fig_install::check_for_updates(true).await, bundle_metadata().await) { + match ( + fig_install::check_for_updates(true, false).await, + bundle_metadata().await, + ) { (ref update_result @ Ok(Some(ref pkg)), Some(file_type)) => { if file_type == FileType::AppImage { let should_continue = dialoguer::Select::with_theme(&dialoguer_theme()) diff --git a/packages/dashboard-app/src/data/preferences.tsx b/packages/dashboard-app/src/data/preferences.tsx index 7a9763d9b..76d13380a 100644 --- a/packages/dashboard-app/src/data/preferences.tsx +++ b/packages/dashboard-app/src/data/preferences.tsx @@ -42,8 +42,7 @@ const generalPreferences = [ { id: "app.disableAutoupdates", title: "Automatic updates", - description: - "Asynchronously check for updates when launching a new shell session.", + description: "Automatically update when new versions are released.", type: "boolean", default: false, inverted: true,