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