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(¤t_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}