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
13const 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 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}