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    fn install_app_update(&self, package_path: &Path) -> Result<(), UpdateError>;
157    fn log_app_update_warning(&self, detail: &str);
158}
159
160fn app_update_events() -> &'static broadcast::Sender<AppUpdateEvent> {
161    static APP_UPDATE_EVENTS: OnceLock<broadcast::Sender<AppUpdateEvent>> = OnceLock::new();
162    APP_UPDATE_EVENTS.get_or_init(|| {
163        let (tx, _) = broadcast::channel(32);
164        tx
165    })
166}
167
168pub fn subscribe_app_update_events() -> AppUpdateEventReceiver {
169    app_update_events().subscribe()
170}
171
172fn emit_app_update_event(event: AppUpdateEvent) {
173    let _ = app_update_events().send(event);
174}
175
176pub async fn check_app_update<H: AppUpdateHost>(
177    host: &H,
178) -> Result<Option<UpdatePackageInfo>, UpdateError> {
179    let current_version = host.current_app_version()?;
180    host.check_app_update(&current_version).await
181}
182
183pub fn ensure_app_update_candidate_version(
184    current_version: &str,
185    candidate_version: &str,
186) -> Result<(), UpdateError> {
187    let candidate_version = candidate_version.trim();
188    if candidate_version.is_empty() {
189        return Err(UpdateError::invalid_parameter(
190            "app update package version is empty",
191        ));
192    }
193
194    let candidate = Version::parse(candidate_version).map_err(|_| {
195        UpdateError::invalid_parameter(format!(
196            "app update package version is not semantic version: {}",
197            candidate_version
198        ))
199    })?;
200
201    let current = Version::parse(current_version).map_err(|_| {
202        UpdateError::runtime(format!(
203            "current app version is not semantic version: {}",
204            current_version
205        ))
206    })?;
207
208    if candidate < current {
209        return Err(UpdateError::unsupported(format!(
210            "reject app downgrade: current={} candidate={}",
211            current_version, candidate_version
212        )));
213    }
214
215    Ok(())
216}
217
218pub fn app_update_scope_key() -> String {
219    UpdateTarget::app(None::<String>).scope_key()
220}