Skip to main content

lingxia_update/
lxapp.rs

1use crate::config::update_config;
2use crate::{
3    BoxFuture, LxAppUpdateQuery, ReleaseType, RuntimeCompatibilityError, UpdatePackageInfo,
4    UpdateTarget, Version,
5};
6use std::collections::HashSet;
7use std::sync::{Mutex, OnceLock};
8use std::time::Duration;
9use tokio::time::timeout;
10
11use super::error::UpdateError;
12
13// Outer ceiling. The actual HTTP timeouts are owned by the registered
14// `UpdateProvider`; this wrapper exists only as a fail-safe when a
15// misbehaving provider forgets to set its own deadline. Keep it strictly
16// larger than any reasonable provider timeout so this layer never preempts
17// the provider's own error reporting.
18const FOREGROUND_UPDATE_CHECK_TIMEOUT: Duration = Duration::from_secs(15);
19
20pub trait LxAppUpdateHost: Clone + Send + Sync + 'static {
21    fn spawn_detached(&self, task: BoxFuture<'static, ()>);
22    fn target_appid(&self) -> &str;
23    fn channel(&self) -> ReleaseType;
24    /// Whether the target's bundle is managed by the update system. False for a
25    /// bundle served live from a local path, which has no installed package.
26    fn is_ota_managed(&self) -> bool;
27    fn runtime_version(&self) -> &str;
28    fn current_version_hint(&self) -> Option<String>;
29    fn installed_version<'a>(&'a self) -> BoxFuture<'a, Result<Option<String>, UpdateError>>;
30    fn is_installed<'a>(&'a self) -> BoxFuture<'a, Result<bool, UpdateError>>;
31    fn check_latest_update<'a>(
32        &'a self,
33        current_version: Option<&'a str>,
34    ) -> BoxFuture<'a, Result<Option<UpdatePackageInfo>, UpdateError>>;
35    fn check_exact_update<'a>(
36        &'a self,
37        target_version: &'a str,
38    ) -> BoxFuture<'a, Result<Option<UpdatePackageInfo>, UpdateError>>;
39    fn has_downloaded_update<'a>(
40        &'a self,
41        version: &'a str,
42    ) -> BoxFuture<'a, Result<bool, UpdateError>>;
43    fn download_update<'a>(
44        &'a self,
45        update: &'a UpdatePackageInfo,
46    ) -> BoxFuture<'a, Result<(), UpdateError>>;
47    fn wait_for_or_start_force_download<'a>(
48        &'a self,
49        update: &'a UpdatePackageInfo,
50    ) -> BoxFuture<'a, Result<(), UpdateError>>;
51    fn emit_update_ready(&self, version: &str, is_force_update: bool) -> Result<(), UpdateError>;
52    fn emit_update_failed(
53        &self,
54        update: &UpdatePackageInfo,
55        error: &str,
56    ) -> Result<(), UpdateError>;
57    fn is_bundled_available(&self) -> bool;
58    fn register_builtin_bundle(&self) -> Result<(), UpdateError>;
59    fn has_update_provider(&self) -> bool;
60    fn log_warning(&self, detail: &str);
61}
62
63pub fn lxapp_update_scope_key(target_appid: &str, release_type: ReleaseType) -> String {
64    UpdateTarget::lxapp(
65        target_appid,
66        release_type,
67        LxAppUpdateQuery::latest(None::<String>),
68    )
69    .scope_key()
70}
71
72struct ActiveLxAppUpdateCheck {
73    scope: String,
74}
75
76impl Drop for ActiveLxAppUpdateCheck {
77    fn drop(&mut self) {
78        if let Ok(mut active) = active_lxapp_update_checks().lock() {
79            active.remove(&self.scope);
80        }
81    }
82}
83
84fn active_lxapp_update_checks() -> &'static Mutex<HashSet<String>> {
85    static ACTIVE_CHECKS: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
86    ACTIVE_CHECKS.get_or_init(|| Mutex::new(HashSet::new()))
87}
88
89fn try_begin_lxapp_update_check(scope: String) -> Option<ActiveLxAppUpdateCheck> {
90    let mut active = active_lxapp_update_checks()
91        .lock()
92        .unwrap_or_else(|err| err.into_inner());
93    if !active.insert(scope.clone()) {
94        return None;
95    }
96    Some(ActiveLxAppUpdateCheck { scope })
97}
98
99async fn with_foreground_update_timeout<T, F>(future: F, context: &str) -> Result<T, UpdateError>
100where
101    F: std::future::Future<Output = Result<T, UpdateError>>,
102{
103    match timeout(FOREGROUND_UPDATE_CHECK_TIMEOUT, future).await {
104        Ok(result) => result,
105        Err(_) => Err(UpdateError::runtime(format!(
106            "{} timed out after {}s",
107            context,
108            FOREGROUND_UPDATE_CHECK_TIMEOUT.as_secs()
109        ))),
110    }
111}
112
113fn runtime_compatibility_to_update_error(error: RuntimeCompatibilityError) -> UpdateError {
114    match error {
115        RuntimeCompatibilityError::InvalidCurrentRuntimeVersion { .. } => {
116            UpdateError::runtime(error.to_string())
117        }
118        RuntimeCompatibilityError::InvalidRequiredRuntimeVersion { .. }
119        | RuntimeCompatibilityError::RequiresRuntimeUpgrade { .. } => {
120            UpdateError::unsupported(error.to_string())
121        }
122    }
123}
124
125fn ensure_runtime_version_compatible<H: LxAppUpdateHost>(
126    host: &H,
127    pkg: &UpdatePackageInfo,
128) -> Result<(), UpdateError> {
129    pkg.ensure_runtime_compatible(host.runtime_version(), host.target_appid())
130        .map_err(runtime_compatibility_to_update_error)
131}
132
133pub fn spawn_background_update_check<H: LxAppUpdateHost>(host: H, current_version: Option<String>) {
134    let runner = host.clone();
135    host.spawn_detached(Box::pin(async move {
136        let scope = lxapp_update_scope_key(runner.target_appid(), runner.channel());
137        let Some(_active_check) = try_begin_lxapp_update_check(scope) else {
138            return;
139        };
140
141        let resolved_current_version = match current_version {
142            Some(version) => Some(version),
143            None => match runner.installed_version().await {
144                Ok(version) => version,
145                Err(error) => {
146                    runner.log_warning(&format!(
147                        "Failed to resolve installed version for {}: {}",
148                        runner.target_appid(),
149                        error
150                    ));
151                    None
152                }
153            },
154        };
155
156        let update = match runner
157            .check_latest_update(resolved_current_version.as_deref())
158            .await
159        {
160            Ok(update) => update,
161            Err(error) => {
162                runner.log_warning(&format!(
163                    "Background update check failed for {}: {}",
164                    runner.target_appid(),
165                    error
166                ));
167                None
168            }
169        };
170
171        let Some(pkg) = update else {
172            return;
173        };
174
175        if !UpdatePackageInfo::should_replace_version(
176            &pkg.version,
177            resolved_current_version.as_deref(),
178        ) {
179            return;
180        }
181
182        if let Err(error) = ensure_runtime_version_compatible(&runner, &pkg) {
183            let _ = runner.emit_update_failed(&pkg, &error.to_string());
184            return;
185        }
186
187        match runner.has_downloaded_update(&pkg.version).await {
188            Ok(true) => {
189                let _ = runner.emit_update_ready(&pkg.version, pkg.is_force_update);
190            }
191            Ok(false) => match runner.download_update(&pkg).await {
192                Ok(()) => {
193                    let _ = runner.emit_update_ready(&pkg.version, pkg.is_force_update);
194                }
195                Err(error) => {
196                    let _ = runner.emit_update_failed(&pkg, &error.to_string());
197                }
198            },
199            Err(error) => {
200                let _ = runner.emit_update_failed(&pkg, &error.to_string());
201            }
202        }
203    }));
204}
205
206pub async fn ensure_first_install<H: LxAppUpdateHost>(host: &H) -> Result<(), UpdateError> {
207    if !host.is_ota_managed() {
208        return Ok(());
209    }
210
211    if host.is_installed().await? {
212        return Ok(());
213    }
214
215    if host.is_bundled_available() {
216        host.register_builtin_bundle()?;
217        return Ok(());
218    }
219
220    if !host.has_update_provider() {
221        return Err(UpdateError::unsupported(format!(
222            "lxapp '{}' is not installed; remote install unavailable",
223            host.target_appid()
224        )));
225    }
226
227    let pkg = with_foreground_update_timeout(
228        host.check_latest_update(None),
229        &format!("first install update check for {}", host.target_appid()),
230    )
231    .await?
232    .ok_or_else(|| {
233        UpdateError::not_found(format!(
234            "lxapp '{}' package not found ({})",
235            host.target_appid(),
236            host.channel().as_str()
237        ))
238    })?;
239
240    ensure_runtime_version_compatible(host, &pkg)?;
241    host.download_update(&pkg).await
242}
243
244pub async fn ensure_target_version_ready<H: LxAppUpdateHost>(
245    host: &H,
246    target_version: &str,
247) -> Result<(), UpdateError> {
248    let target_version = target_version.trim();
249    if target_version.is_empty() {
250        return Err(UpdateError::invalid_parameter(
251            "targetVersion cannot be empty",
252        ));
253    }
254
255    let target_semver = Version::parse(target_version).map_err(|_| {
256        UpdateError::invalid_parameter(format!(
257            "targetVersion must be semantic version: {}",
258            target_version
259        ))
260    })?;
261
262    let is_installed = host.is_installed().await?;
263    let current_version = if is_installed {
264        host.installed_version().await?
265    } else {
266        None
267    };
268
269    if host.is_ota_managed() && update_config().force_update_gate {
270        match with_foreground_update_timeout(
271            host.check_latest_update(current_version.as_deref()),
272            &format!("force-update gate check for {}", host.target_appid()),
273        )
274        .await
275        {
276            Ok(Some(pkg)) if pkg.is_force_update => {
277                let force_version = Version::parse(&pkg.version).map_err(|_| {
278                    UpdateError::unsupported(format!(
279                        "invalid forced update version '{}' for {}",
280                        pkg.version,
281                        host.target_appid()
282                    ))
283                })?;
284                if target_semver < force_version {
285                    return Err(UpdateError::unsupported(format!(
286                        "targetVersion {} is lower than required forced version {} for {} ({})",
287                        target_version,
288                        pkg.version,
289                        host.target_appid(),
290                        host.channel().as_str()
291                    )));
292                }
293            }
294            Ok(_) => {}
295            Err(error) => {
296                host.log_warning(&format!(
297                    "targetVersion force-update check failed (fail-open) for {}: {}",
298                    host.target_appid(),
299                    error
300                ));
301            }
302        }
303    }
304
305    if current_version.as_deref() == Some(target_version) {
306        return Ok(());
307    }
308
309    let pkg = with_foreground_update_timeout(
310        host.check_exact_update(target_version),
311        &format!(
312            "exact version update check for {}@{}",
313            host.target_appid(),
314            target_version
315        ),
316    )
317    .await?
318    .ok_or_else(|| {
319        UpdateError::not_found(format!(
320            "No package available for {}@{} ({})",
321            host.target_appid(),
322            target_version,
323            host.channel().as_str()
324        ))
325    })?;
326
327    ensure_runtime_version_compatible(host, &pkg)?;
328
329    if host.has_downloaded_update(&pkg.version).await? {
330        return Ok(());
331    }
332
333    host.download_update(&pkg).await
334}
335
336pub async fn ensure_force_update_for_installed<H: LxAppUpdateHost>(
337    host: &H,
338) -> Result<(), UpdateError> {
339    if !host.is_ota_managed() {
340        return Ok(());
341    }
342
343    if !update_config().force_update_gate {
344        return Ok(());
345    }
346
347    if !host.is_installed().await? {
348        return Ok(());
349    }
350
351    let current_version = match host.installed_version().await? {
352        Some(version) => version,
353        None => {
354            host.log_warning(&format!(
355                "Installed lxapp has no recorded version; skip force-update gating: {}",
356                host.target_appid()
357            ));
358            return Ok(());
359        }
360    };
361
362    let update = match with_foreground_update_timeout(
363        host.check_latest_update(Some(current_version.as_str())),
364        &format!(
365            "installed app force-update check for {}",
366            host.target_appid()
367        ),
368    )
369    .await
370    {
371        Ok(update) => update,
372        Err(error) => {
373            host.log_warning(&format!(
374                "force-update check failed (fail-open) for {}: {}",
375                host.target_appid(),
376                error
377            ));
378            return Ok(());
379        }
380    };
381
382    let Some(pkg) = update else {
383        return Ok(());
384    };
385
386    if let Err(error) = ensure_runtime_version_compatible(host, &pkg) {
387        if pkg.is_force_update {
388            return Err(error);
389        }
390        host.log_warning(&format!(
391            "optional update blocked by runtime version gate for {}: {}",
392            host.target_appid(),
393            error
394        ));
395        return Ok(());
396    }
397
398    if !pkg.is_force_update || pkg.version == current_version {
399        return Ok(());
400    }
401
402    if host.has_downloaded_update(&pkg.version).await? {
403        return Ok(());
404    }
405
406    host.wait_for_or_start_force_download(&pkg).await
407}