Skip to main content

lingxia_update/
app.rs

1use crate::config::{UpdateUiMode, update_config};
2use crate::{BoxFuture, UpdatePackageInfo, UpdateTarget, Version};
3use std::path::{Path, PathBuf};
4use std::sync::OnceLock;
5use std::time::Duration;
6use tokio::sync::broadcast;
7use tokio::time::sleep;
8
9use super::error::UpdateError;
10
11pub const APP_UPDATE_START_DELAY: Duration = Duration::from_secs(15);
12
13#[derive(Debug, Clone)]
14pub enum AppUpdateEvent {
15    Available(UpdatePackageInfo),
16    DownloadStarted {
17        version: String,
18    },
19    DownloadProgress {
20        version: String,
21        downloaded_bytes: u64,
22        total_bytes: Option<u64>,
23        progress: Option<u8>,
24    },
25    Downloaded {
26        version: String,
27    },
28    InstallRequested {
29        version: String,
30    },
31    Failed {
32        stage: AppUpdateStage,
33        error: String,
34    },
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum AppUpdateStage {
39    Check,
40    Prompt,
41    Download,
42    Install,
43}
44
45pub type AppUpdateEventReceiver = broadcast::Receiver<AppUpdateEvent>;
46pub type AppUpdateEventSender = broadcast::Sender<AppUpdateEvent>;
47
48pub struct AppUpdateApply {
49    receiver: AppUpdateEventReceiver,
50    done: bool,
51}
52
53impl AppUpdateApply {
54    pub fn new(receiver: AppUpdateEventReceiver) -> Self {
55        Self {
56            receiver,
57            done: false,
58        }
59    }
60
61    pub fn channel() -> (Self, AppUpdateEventSender) {
62        let (sender, receiver) = broadcast::channel(32);
63        (Self::new(receiver), sender)
64    }
65
66    pub async fn next(&mut self) -> Option<AppUpdateEvent> {
67        if self.done {
68            return None;
69        }
70
71        let event = loop {
72            match self.receiver.recv().await {
73                Ok(event) => break Some(event),
74                Err(broadcast::error::RecvError::Lagged(_)) => continue,
75                Err(broadcast::error::RecvError::Closed) => break None,
76            }
77        };
78
79        let Some(event) = event else {
80            self.done = true;
81            return None;
82        };
83
84        if matches!(
85            event,
86            AppUpdateEvent::InstallRequested { .. } | AppUpdateEvent::Failed { .. }
87        ) {
88            self.done = true;
89        }
90
91        Some(event)
92    }
93}
94
95#[derive(Debug, Clone)]
96pub struct AppUpdateProgressReporter {
97    version: String,
98    sender: Option<AppUpdateEventSender>,
99}
100
101impl AppUpdateProgressReporter {
102    fn new(version: impl Into<String>) -> Self {
103        Self {
104            version: version.into(),
105            sender: None,
106        }
107    }
108
109    pub fn scoped(version: impl Into<String>, sender: AppUpdateEventSender) -> Self {
110        Self {
111            version: version.into(),
112            sender: Some(sender),
113        }
114    }
115
116    fn emit(&self, event: AppUpdateEvent) {
117        if let Some(sender) = &self.sender {
118            let _ = sender.send(event);
119        } else {
120            emit_app_update_event(event);
121        }
122    }
123
124    pub fn report(&self, downloaded_bytes: u64, total_bytes: Option<u64>) {
125        let progress = total_bytes.filter(|total| *total > 0).map(|total| {
126            ((downloaded_bytes as f64 / total as f64) * 100.0)
127                .round()
128                .clamp(0.0, 100.0) as u8
129        });
130        self.emit(AppUpdateEvent::DownloadProgress {
131            version: self.version.clone(),
132            downloaded_bytes,
133            total_bytes,
134            progress,
135        });
136    }
137}
138
139pub fn send_app_update_event(sender: &AppUpdateEventSender, event: AppUpdateEvent) {
140    let _ = sender.send(event);
141}
142
143fn emit_app_update_failed(stage: AppUpdateStage, error: &UpdateError) {
144    emit_app_update_event(AppUpdateEvent::Failed {
145        stage,
146        error: error.to_string(),
147    });
148}
149
150pub fn send_app_update_failed(
151    sender: &AppUpdateEventSender,
152    stage: AppUpdateStage,
153    error: &UpdateError,
154) {
155    send_app_update_event(
156        sender,
157        AppUpdateEvent::Failed {
158            stage,
159            error: error.to_string(),
160        },
161    );
162}
163
164pub trait AppUpdateHost: Clone + Send + Sync + 'static {
165    fn spawn_detached(&self, task: BoxFuture<'static, ()>);
166    fn current_app_version(&self) -> Result<String, UpdateError>;
167    fn check_app_update<'a>(
168        &'a self,
169        current_version: &'a str,
170    ) -> BoxFuture<'a, Result<Option<UpdatePackageInfo>, UpdateError>>;
171    fn show_builtin_update_prompt<'a>(
172        &'a self,
173        update: &'a UpdatePackageInfo,
174    ) -> BoxFuture<'a, Result<bool, UpdateError>>;
175    fn download_app_update<'a>(
176        &'a self,
177        update: &'a UpdatePackageInfo,
178        progress: AppUpdateProgressReporter,
179    ) -> BoxFuture<'a, Result<PathBuf, UpdateError>>;
180    fn install_app_update(&self, package_path: &Path) -> Result<(), UpdateError>;
181    fn log_app_update_warning(&self, detail: &str);
182    fn notify_app_update_available(&self, update: &UpdatePackageInfo) -> Result<(), UpdateError>;
183}
184
185fn app_update_events() -> &'static broadcast::Sender<AppUpdateEvent> {
186    static APP_UPDATE_EVENTS: OnceLock<broadcast::Sender<AppUpdateEvent>> = OnceLock::new();
187    APP_UPDATE_EVENTS.get_or_init(|| {
188        let (tx, _) = broadcast::channel(32);
189        tx
190    })
191}
192
193pub fn subscribe_app_update_events() -> AppUpdateEventReceiver {
194    app_update_events().subscribe()
195}
196
197fn emit_app_update_event(event: AppUpdateEvent) {
198    let _ = app_update_events().send(event);
199}
200
201pub async fn check_app_update<H: AppUpdateHost>(
202    host: &H,
203) -> Result<Option<UpdatePackageInfo>, UpdateError> {
204    let current_version = host.current_app_version()?;
205    host.check_app_update(&current_version).await
206}
207
208pub async fn download_app_update<H: AppUpdateHost>(
209    host: &H,
210    update: &UpdatePackageInfo,
211) -> Result<PathBuf, UpdateError> {
212    let current_version = host.current_app_version().map_err(|error| {
213        emit_app_update_failed(AppUpdateStage::Download, &error);
214        error
215    })?;
216    ensure_app_update_candidate_version(&current_version, &update.version).map_err(|error| {
217        emit_app_update_failed(AppUpdateStage::Download, &error);
218        error
219    })?;
220
221    emit_app_update_event(AppUpdateEvent::DownloadStarted {
222        version: update.version.clone(),
223    });
224    let path = host
225        .download_app_update(update, AppUpdateProgressReporter::new(&update.version))
226        .await
227        .map_err(|error| {
228            emit_app_update_failed(AppUpdateStage::Download, &error);
229            error
230        })?;
231    emit_app_update_event(AppUpdateEvent::Downloaded {
232        version: update.version.clone(),
233    });
234
235    Ok(path)
236}
237
238pub fn install_app_update<H: AppUpdateHost>(
239    host: &H,
240    update: &UpdatePackageInfo,
241    package_path: &Path,
242) -> Result<(), UpdateError> {
243    let current_version = host.current_app_version().map_err(|error| {
244        emit_app_update_failed(AppUpdateStage::Install, &error);
245        error
246    })?;
247    ensure_app_update_candidate_version(&current_version, &update.version).map_err(|error| {
248        emit_app_update_failed(AppUpdateStage::Install, &error);
249        error
250    })?;
251
252    host.install_app_update(package_path).map_err(|error| {
253        emit_app_update_failed(AppUpdateStage::Install, &error);
254        error
255    })?;
256    emit_app_update_event(AppUpdateEvent::InstallRequested {
257        version: update.version.clone(),
258    });
259
260    Ok(())
261}
262
263pub async fn download_and_install_app_update<H: AppUpdateHost>(
264    host: &H,
265    update: &UpdatePackageInfo,
266) -> Result<PathBuf, UpdateError> {
267    let path = download_app_update(host, update).await?;
268    install_app_update(host, update, &path)?;
269    Ok(path)
270}
271
272pub async fn check_and_install_app_update<H: AppUpdateHost>(host: &H) -> Result<(), UpdateError> {
273    let current_version = host.current_app_version().map_err(|error| {
274        emit_app_update_failed(AppUpdateStage::Check, &error);
275        error
276    })?;
277    let update = host
278        .check_app_update(&current_version)
279        .await
280        .map_err(|error| {
281            emit_app_update_failed(AppUpdateStage::Check, &error);
282            error
283        })?;
284    let Some(update) = update else {
285        return Ok(());
286    };
287
288    ensure_app_update_candidate_version(&current_version, &update.version).map_err(|error| {
289        emit_app_update_failed(AppUpdateStage::Check, &error);
290        error
291    })?;
292    emit_app_update_event(AppUpdateEvent::Available(update.clone()));
293
294    if update_config().ui_mode == UpdateUiMode::Custom {
295        host.notify_app_update_available(&update)?;
296        if update.is_force_update {
297            return Err(UpdateError::runtime(
298                "forced app update requires explicit host handling in custom UI mode",
299            ));
300        }
301        return Ok(());
302    }
303
304    let confirmed = host
305        .show_builtin_update_prompt(&update)
306        .await
307        .map_err(|error| {
308            emit_app_update_failed(AppUpdateStage::Prompt, &error);
309            error
310        })?;
311
312    if !confirmed && update.is_force_update {
313        let error = UpdateError::runtime("forced app update was not confirmed");
314        emit_app_update_failed(AppUpdateStage::Prompt, &error);
315        return Err(error);
316    }
317
318    if !confirmed {
319        return Ok(());
320    }
321
322    download_and_install_app_update(host, &update)
323        .await
324        .map(|_| ())
325}
326
327pub fn spawn_app_update_flow<H: AppUpdateHost>(
328    host: H,
329    start_delay: Duration,
330    bypass_auto_check: bool,
331) {
332    let runner = host.clone();
333    host.spawn_detached(Box::pin(async move {
334        if !start_delay.is_zero() {
335            sleep(start_delay).await;
336        }
337
338        if !bypass_auto_check && !update_config().auto_check_app {
339            return;
340        }
341
342        if let Err(error) = check_and_install_app_update(&runner).await {
343            runner.log_app_update_warning(&format!("App update flow failed: {}", error));
344        }
345    }));
346}
347
348pub fn ensure_app_update_candidate_version(
349    current_version: &str,
350    candidate_version: &str,
351) -> Result<(), UpdateError> {
352    let candidate_version = candidate_version.trim();
353    if candidate_version.is_empty() {
354        return Err(UpdateError::invalid_parameter(
355            "app update package version is empty",
356        ));
357    }
358
359    let candidate = Version::parse(candidate_version).map_err(|_| {
360        UpdateError::invalid_parameter(format!(
361            "app update package version is not semantic version: {}",
362            candidate_version
363        ))
364    })?;
365
366    let current = Version::parse(current_version).map_err(|_| {
367        UpdateError::runtime(format!(
368            "current app version is not semantic version: {}",
369            current_version
370        ))
371    })?;
372
373    if candidate < current {
374        return Err(UpdateError::unsupported(format!(
375            "reject app downgrade: current={} candidate={}",
376            current_version, candidate_version
377        )));
378    }
379
380    Ok(())
381}
382
383pub fn app_update_scope_key() -> String {
384    UpdateTarget::app(None::<String>).scope_key()
385}