Skip to main content

lingxia_update/
lxapp.rs

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