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