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