lingxia-update 0.6.0

LingXia update domain models and shared policy helpers
Documentation
use crate::config::update_config;
use crate::{
    BoxFuture, LxAppUpdateQuery, ReleaseType, RuntimeCompatibilityError, UpdatePackageInfo,
    UpdateTarget, Version,
};
use std::collections::HashSet;
use std::sync::{Mutex, OnceLock};
use std::time::Duration;
use tokio::time::timeout;

use super::error::UpdateError;

const FOREGROUND_UPDATE_CHECK_TIMEOUT: Duration = Duration::from_secs(3);

pub trait LxAppUpdateHost: Clone + Send + Sync + 'static {
    fn spawn_detached(&self, task: BoxFuture<'static, ()>);
    fn target_appid(&self) -> &str;
    fn channel(&self) -> ReleaseType;
    fn runtime_version(&self) -> &str;
    fn current_version_hint(&self) -> Option<String>;
    fn installed_version<'a>(&'a self) -> BoxFuture<'a, Result<Option<String>, UpdateError>>;
    fn is_installed<'a>(&'a self) -> BoxFuture<'a, Result<bool, UpdateError>>;
    fn check_latest_update<'a>(
        &'a self,
        current_version: Option<&'a str>,
    ) -> BoxFuture<'a, Result<Option<UpdatePackageInfo>, UpdateError>>;
    fn check_exact_update<'a>(
        &'a self,
        target_version: &'a str,
    ) -> BoxFuture<'a, Result<Option<UpdatePackageInfo>, UpdateError>>;
    fn has_downloaded_update<'a>(
        &'a self,
        version: &'a str,
    ) -> BoxFuture<'a, Result<bool, UpdateError>>;
    fn download_update<'a>(
        &'a self,
        update: &'a UpdatePackageInfo,
    ) -> BoxFuture<'a, Result<(), UpdateError>>;
    fn wait_for_or_start_force_download<'a>(
        &'a self,
        update: &'a UpdatePackageInfo,
    ) -> BoxFuture<'a, Result<(), UpdateError>>;
    fn emit_update_ready(&self, version: &str, is_force_update: bool) -> Result<(), UpdateError>;
    fn emit_update_failed(
        &self,
        update: &UpdatePackageInfo,
        error: &str,
    ) -> Result<(), UpdateError>;
    fn is_bundled_available(&self) -> bool;
    fn register_builtin_bundle(&self) -> Result<(), UpdateError>;
    fn has_update_provider(&self) -> bool;
    fn log_warning(&self, detail: &str);
}

pub fn lxapp_update_scope_key(target_appid: &str, release_type: ReleaseType) -> String {
    UpdateTarget::lxapp(
        target_appid,
        release_type,
        LxAppUpdateQuery::latest(None::<String>),
    )
    .scope_key()
}

struct ActiveLxAppUpdateCheck {
    scope: String,
}

impl Drop for ActiveLxAppUpdateCheck {
    fn drop(&mut self) {
        if let Ok(mut active) = active_lxapp_update_checks().lock() {
            active.remove(&self.scope);
        }
    }
}

fn active_lxapp_update_checks() -> &'static Mutex<HashSet<String>> {
    static ACTIVE_CHECKS: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
    ACTIVE_CHECKS.get_or_init(|| Mutex::new(HashSet::new()))
}

fn try_begin_lxapp_update_check(scope: String) -> Option<ActiveLxAppUpdateCheck> {
    let mut active = active_lxapp_update_checks()
        .lock()
        .unwrap_or_else(|err| err.into_inner());
    if !active.insert(scope.clone()) {
        return None;
    }
    Some(ActiveLxAppUpdateCheck { scope })
}

async fn with_foreground_update_timeout<T, F>(future: F, context: &str) -> Result<T, UpdateError>
where
    F: std::future::Future<Output = Result<T, UpdateError>>,
{
    match timeout(FOREGROUND_UPDATE_CHECK_TIMEOUT, future).await {
        Ok(result) => result,
        Err(_) => Err(UpdateError::runtime(format!(
            "{} timed out after {}s",
            context,
            FOREGROUND_UPDATE_CHECK_TIMEOUT.as_secs()
        ))),
    }
}

fn runtime_compatibility_to_update_error(error: RuntimeCompatibilityError) -> UpdateError {
    match error {
        RuntimeCompatibilityError::InvalidCurrentRuntimeVersion { .. } => {
            UpdateError::runtime(error.to_string())
        }
        RuntimeCompatibilityError::InvalidRequiredRuntimeVersion { .. }
        | RuntimeCompatibilityError::RequiresRuntimeUpgrade { .. } => {
            UpdateError::unsupported(error.to_string())
        }
    }
}

fn ensure_runtime_version_compatible<H: LxAppUpdateHost>(
    host: &H,
    pkg: &UpdatePackageInfo,
) -> Result<(), UpdateError> {
    pkg.ensure_runtime_compatible(host.runtime_version(), host.target_appid())
        .map_err(runtime_compatibility_to_update_error)
}

pub fn spawn_background_update_check<H: LxAppUpdateHost>(host: H, current_version: Option<String>) {
    let runner = host.clone();
    host.spawn_detached(Box::pin(async move {
        let scope = lxapp_update_scope_key(runner.target_appid(), runner.channel());
        let Some(_active_check) = try_begin_lxapp_update_check(scope) else {
            return;
        };

        let resolved_current_version = match current_version {
            Some(version) => Some(version),
            None => match runner.installed_version().await {
                Ok(version) => version,
                Err(error) => {
                    runner.log_warning(&format!(
                        "Failed to resolve installed version for {}: {}",
                        runner.target_appid(),
                        error
                    ));
                    None
                }
            },
        };

        let update = match runner
            .check_latest_update(resolved_current_version.as_deref())
            .await
        {
            Ok(update) => update,
            Err(error) => {
                runner.log_warning(&format!(
                    "Background update check failed for {}: {}",
                    runner.target_appid(),
                    error
                ));
                None
            }
        };

        let Some(pkg) = update else {
            return;
        };

        if !UpdatePackageInfo::should_replace_version(
            &pkg.version,
            resolved_current_version.as_deref(),
        ) {
            return;
        }

        if let Err(error) = ensure_runtime_version_compatible(&runner, &pkg) {
            let _ = runner.emit_update_failed(&pkg, &error.to_string());
            return;
        }

        match runner.has_downloaded_update(&pkg.version).await {
            Ok(true) => {
                let _ = runner.emit_update_ready(&pkg.version, pkg.is_force_update);
            }
            Ok(false) => match runner.download_update(&pkg).await {
                Ok(()) => {
                    let _ = runner.emit_update_ready(&pkg.version, pkg.is_force_update);
                }
                Err(error) => {
                    let _ = runner.emit_update_failed(&pkg, &error.to_string());
                }
            },
            Err(error) => {
                let _ = runner.emit_update_failed(&pkg, &error.to_string());
            }
        }
    }));
}

pub async fn ensure_first_install<H: LxAppUpdateHost>(host: &H) -> Result<(), UpdateError> {
    if host.channel() != ReleaseType::Release {
        return Ok(());
    }

    if host.is_installed().await? {
        return Ok(());
    }

    if host.is_bundled_available() {
        host.register_builtin_bundle()?;
        return Ok(());
    }

    if !host.has_update_provider() {
        return Err(UpdateError::unsupported(format!(
            "lxapp '{}' is not installed; remote install unavailable",
            host.target_appid()
        )));
    }

    let pkg = with_foreground_update_timeout(
        host.check_latest_update(None),
        &format!("first install update check for {}", host.target_appid()),
    )
    .await?
    .ok_or_else(|| {
        UpdateError::not_found(format!(
            "lxapp '{}' package not found ({})",
            host.target_appid(),
            host.channel().as_str()
        ))
    })?;

    ensure_runtime_version_compatible(host, &pkg)?;
    host.download_update(&pkg).await
}

pub async fn ensure_target_version_ready<H: LxAppUpdateHost>(
    host: &H,
    target_version: &str,
) -> Result<(), UpdateError> {
    let target_version = target_version.trim();
    if target_version.is_empty() {
        return Err(UpdateError::invalid_parameter(
            "targetVersion cannot be empty",
        ));
    }

    let target_semver = Version::parse(target_version).map_err(|_| {
        UpdateError::invalid_parameter(format!(
            "targetVersion must be semantic version: {}",
            target_version
        ))
    })?;

    let is_installed = host.is_installed().await?;
    let current_version = if is_installed {
        host.installed_version().await?
    } else {
        None
    };

    if host.channel() == ReleaseType::Release && update_config().force_update_gate {
        match with_foreground_update_timeout(
            host.check_latest_update(current_version.as_deref()),
            &format!("force-update gate check for {}", host.target_appid()),
        )
        .await
        {
            Ok(Some(pkg)) if pkg.is_force_update => {
                let force_version = Version::parse(&pkg.version).map_err(|_| {
                    UpdateError::unsupported(format!(
                        "invalid forced update version '{}' for {}",
                        pkg.version,
                        host.target_appid()
                    ))
                })?;
                if target_semver < force_version {
                    return Err(UpdateError::unsupported(format!(
                        "targetVersion {} is lower than required forced version {} for {} ({})",
                        target_version,
                        pkg.version,
                        host.target_appid(),
                        host.channel().as_str()
                    )));
                }
            }
            Ok(_) => {}
            Err(error) => {
                host.log_warning(&format!(
                    "targetVersion force-update check failed (fail-open) for {}: {}",
                    host.target_appid(),
                    error
                ));
            }
        }
    }

    if current_version.as_deref() == Some(target_version) {
        return Ok(());
    }

    let pkg = with_foreground_update_timeout(
        host.check_exact_update(target_version),
        &format!(
            "exact version update check for {}@{}",
            host.target_appid(),
            target_version
        ),
    )
    .await?
    .ok_or_else(|| {
        UpdateError::not_found(format!(
            "No package available for {}@{} ({})",
            host.target_appid(),
            target_version,
            host.channel().as_str()
        ))
    })?;

    ensure_runtime_version_compatible(host, &pkg)?;

    if host.has_downloaded_update(&pkg.version).await? {
        return Ok(());
    }

    host.download_update(&pkg).await
}

pub async fn ensure_force_update_for_installed<H: LxAppUpdateHost>(
    host: &H,
) -> Result<(), UpdateError> {
    if host.channel() != ReleaseType::Release {
        return Ok(());
    }

    if !update_config().force_update_gate {
        return Ok(());
    }

    if !host.is_installed().await? {
        return Ok(());
    }

    let current_version = match host.installed_version().await? {
        Some(version) => version,
        None => {
            host.log_warning(&format!(
                "Installed lxapp has no recorded version; skip force-update gating: {}",
                host.target_appid()
            ));
            return Ok(());
        }
    };

    let update = match with_foreground_update_timeout(
        host.check_latest_update(Some(current_version.as_str())),
        &format!(
            "installed app force-update check for {}",
            host.target_appid()
        ),
    )
    .await
    {
        Ok(update) => update,
        Err(error) => {
            host.log_warning(&format!(
                "force-update check failed (fail-open) for {}: {}",
                host.target_appid(),
                error
            ));
            return Ok(());
        }
    };

    let Some(pkg) = update else {
        return Ok(());
    };

    if let Err(error) = ensure_runtime_version_compatible(host, &pkg) {
        if pkg.is_force_update {
            return Err(error);
        }
        host.log_warning(&format!(
            "optional update blocked by runtime version gate for {}: {}",
            host.target_appid(),
            error
        ));
        return Ok(());
    }

    if !pkg.is_force_update || pkg.version == current_version {
        return Ok(());
    }

    if host.has_downloaded_update(&pkg.version).await? {
        return Ok(());
    }

    host.wait_for_or_start_force_download(&pkg).await
}