Skip to main content

lingxia_update/
app.rs

1use crate::{BoxFuture, UpdatePackageInfo, UpdateTarget, Version};
2use std::path::{Path, PathBuf};
3use std::sync::OnceLock;
4use tokio::sync::broadcast;
5
6use super::error::UpdateError;
7
8#[derive(Debug, Clone)]
9pub enum AppUpdateEvent {
10    Available(UpdatePackageInfo),
11    DownloadStarted {
12        version: String,
13    },
14    DownloadProgress {
15        version: String,
16        downloaded_bytes: u64,
17        total_bytes: Option<u64>,
18        progress: Option<u8>,
19    },
20    Downloaded {
21        version: String,
22    },
23    InstallRequested {
24        version: String,
25    },
26    Failed {
27        stage: AppUpdateStage,
28        error: String,
29    },
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum AppUpdateStage {
34    Check,
35    Download,
36    Install,
37}
38
39pub type AppUpdateEventReceiver = broadcast::Receiver<AppUpdateEvent>;
40pub type AppUpdateEventSender = broadcast::Sender<AppUpdateEvent>;
41
42pub struct AppUpdateApply {
43    receiver: AppUpdateEventReceiver,
44    done: bool,
45}
46
47impl AppUpdateApply {
48    pub fn new(receiver: AppUpdateEventReceiver) -> Self {
49        Self {
50            receiver,
51            done: false,
52        }
53    }
54
55    pub fn channel() -> (Self, AppUpdateEventSender) {
56        let (sender, receiver) = broadcast::channel(32);
57        (Self::new(receiver), sender)
58    }
59
60    pub async fn next(&mut self) -> Option<AppUpdateEvent> {
61        if self.done {
62            return None;
63        }
64
65        let event = loop {
66            match self.receiver.recv().await {
67                Ok(event) => break Some(event),
68                Err(broadcast::error::RecvError::Lagged(_)) => continue,
69                Err(broadcast::error::RecvError::Closed) => break None,
70            }
71        };
72
73        let Some(event) = event else {
74            self.done = true;
75            return None;
76        };
77
78        if matches!(
79            event,
80            AppUpdateEvent::InstallRequested { .. } | AppUpdateEvent::Failed { .. }
81        ) {
82            self.done = true;
83        }
84
85        Some(event)
86    }
87}
88
89#[derive(Debug, Clone)]
90pub struct AppUpdateProgressReporter {
91    version: String,
92    sender: Option<AppUpdateEventSender>,
93}
94
95impl AppUpdateProgressReporter {
96    pub fn scoped(version: impl Into<String>, sender: AppUpdateEventSender) -> Self {
97        Self {
98            version: version.into(),
99            sender: Some(sender),
100        }
101    }
102
103    fn emit(&self, event: AppUpdateEvent) {
104        if let Some(sender) = &self.sender {
105            let _ = sender.send(event);
106        } else {
107            emit_app_update_event(event);
108        }
109    }
110
111    pub fn report(&self, downloaded_bytes: u64, total_bytes: Option<u64>) {
112        let progress = total_bytes.filter(|total| *total > 0).map(|total| {
113            ((downloaded_bytes as f64 / total as f64) * 100.0)
114                .round()
115                .clamp(0.0, 100.0) as u8
116        });
117        self.emit(AppUpdateEvent::DownloadProgress {
118            version: self.version.clone(),
119            downloaded_bytes,
120            total_bytes,
121            progress,
122        });
123    }
124}
125
126pub fn send_app_update_event(sender: &AppUpdateEventSender, event: AppUpdateEvent) {
127    let _ = sender.send(event);
128}
129
130pub fn send_app_update_failed(
131    sender: &AppUpdateEventSender,
132    stage: AppUpdateStage,
133    error: &UpdateError,
134) {
135    send_app_update_event(
136        sender,
137        AppUpdateEvent::Failed {
138            stage,
139            error: error.to_string(),
140        },
141    );
142}
143
144pub trait AppUpdateHost: Clone + Send + Sync + 'static {
145    fn spawn_detached(&self, task: BoxFuture<'static, ()>);
146    fn current_app_version(&self) -> Result<String, UpdateError>;
147    fn check_app_update<'a>(
148        &'a self,
149        current_version: &'a str,
150    ) -> BoxFuture<'a, Result<Option<UpdatePackageInfo>, UpdateError>>;
151    fn download_app_update<'a>(
152        &'a self,
153        update: &'a UpdatePackageInfo,
154        progress: AppUpdateProgressReporter,
155    ) -> BoxFuture<'a, Result<PathBuf, UpdateError>>;
156    /// Hand off the downloaded package to the platform installer. `info_json`
157    /// carries the prompt metadata `{version, releaseNotes, isForceUpdate}`:
158    /// the platform shows release notes in the "ready to update" prompt and,
159    /// when `isForceUpdate` is true, presents a blocking "must update" prompt
160    /// instead of the dismissible reminder.
161    fn install_app_update(&self, package_path: &Path, info_json: &str) -> Result<(), UpdateError>;
162    fn log_app_update_warning(&self, detail: &str);
163}
164
165fn app_update_events() -> &'static broadcast::Sender<AppUpdateEvent> {
166    static APP_UPDATE_EVENTS: OnceLock<broadcast::Sender<AppUpdateEvent>> = OnceLock::new();
167    APP_UPDATE_EVENTS.get_or_init(|| {
168        let (tx, _) = broadcast::channel(32);
169        tx
170    })
171}
172
173pub fn subscribe_app_update_events() -> AppUpdateEventReceiver {
174    app_update_events().subscribe()
175}
176
177fn emit_app_update_event(event: AppUpdateEvent) {
178    let _ = app_update_events().send(event);
179}
180
181pub async fn check_app_update<H: AppUpdateHost>(
182    host: &H,
183) -> Result<Option<UpdatePackageInfo>, UpdateError> {
184    let current_version = host.current_app_version()?;
185    let candidate = host.check_app_update(&current_version).await?;
186    // Only surface a strictly-newer candidate. A provider that re-offers the
187    // installed version (or the same version after a successful update) would
188    // otherwise make the app re-download and re-prompt on every check — an
189    // endless "update available" loop. Unparseable versions fall through to the
190    // apply-time downgrade guard.
191    if let Some(package) = &candidate
192        && !app_update_candidate_is_newer(&package.version, &current_version)
193    {
194        return Ok(None);
195    }
196    Ok(candidate)
197}
198
199fn app_update_candidate_is_newer(candidate: &str, current: &str) -> bool {
200    match (
201        Version::parse(candidate.trim()),
202        Version::parse(current.trim()),
203    ) {
204        (Ok(candidate), Ok(current)) => candidate > current,
205        // Can't compare — let the apply-time guard decide rather than hiding it.
206        _ => true,
207    }
208}
209
210pub fn ensure_app_update_candidate_version(
211    current_version: &str,
212    candidate_version: &str,
213) -> Result<(), UpdateError> {
214    let candidate_version = candidate_version.trim();
215    if candidate_version.is_empty() {
216        return Err(UpdateError::invalid_parameter(
217            "app update package version is empty",
218        ));
219    }
220
221    let candidate = Version::parse(candidate_version).map_err(|_| {
222        UpdateError::invalid_parameter(format!(
223            "app update package version is not semantic version: {}",
224            candidate_version
225        ))
226    })?;
227
228    let current = Version::parse(current_version).map_err(|_| {
229        UpdateError::runtime(format!(
230            "current app version is not semantic version: {}",
231            current_version
232        ))
233    })?;
234
235    if candidate < current {
236        return Err(UpdateError::unsupported(format!(
237            "reject app downgrade: current={} candidate={}",
238            current_version, candidate_version
239        )));
240    }
241
242    Ok(())
243}
244
245pub fn app_update_scope_key() -> String {
246    UpdateTarget::app(None::<String>).scope_key()
247}