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