Skip to content
116 linesCodeBlameRaw
1//! Updates. The app asks g1t.sh/downloads/desktop/latest.json soon after
2//! it starts and every six hours after, downloads a newer version in the
3//! background, and then says so: a notification, and the tray's "Restart
4//! to update". Nothing is installed under someone mid-sentence; the
5//! restart, or quitting, installs it. The manifest is signed with g1t's
6//! release key, built into the app, and a download whose signature does
7//! not check out is thrown away (scripts/desktop-release.mjs).
8
9use std::time::Duration;
10
11use tauri::AppHandle;
12use tauri::Manager;
13use tauri_plugin_notification::NotificationExt;
14use tauri_plugin_updater::{Update, UpdaterExt};
15
16use crate::{config, tray};
17
18/// An update downloaded and checked, waiting to be installed.
19pub struct Ready {
20 pub version: String,
21 update: Update,
22 bytes: Vec<u8>,
23}
24
25const FIRST_CHECK: Duration = Duration::from_secs(15);
26const EVERY: Duration = Duration::from_secs(6 * 60 * 60);
27
28/// Checks soon after the start, then every six hours.
29pub fn schedule(app: AppHandle) {
30 tauri::async_runtime::spawn(async move {
31 tokio::time::sleep(FIRST_CHECK).await;
32 loop {
33 let _ = check(&app, false).await;
34 tokio::time::sleep(EVERY).await;
35 }
36 });
37}
38
39/// The version downloaded and waiting, if one is.
40pub fn ready(app: &AppHandle) -> Option<String> {
41 app.state::<config::State>().update.lock().unwrap().as_ref().map(|ready| ready.version.clone())
42}
43
44fn tell(app: &AppHandle, title: &str, body: &str) {
45 let _ = app.notification().builder().title(title).body(body).show();
46}
47
48/// Looks for an update and downloads it. `asked` is a person asking from
49/// the tray or the site, who is told the answer either way; the timer
50/// says nothing unless there is something to install.
51pub async fn check(app: &AppHandle, asked: bool) -> Result<String, String> {
52 if let Some(version) = ready(app) {
53 if asked {
54 tell(app, "g1t is ready to update", &format!("Version {version} is downloaded. Restart g1t to use it."));
55 }
56 return Ok(format!("ready:{version}"));
57 }
58 let updater = app.updater().map_err(|error| error.to_string())?;
59 let update = match updater.check().await {
60 Ok(Some(update)) => update,
61 Ok(None) => {
62 if asked {
63 tell(app, "g1t is up to date", &format!("You have version {}.", env!("CARGO_PKG_VERSION")));
64 }
65 return Ok("current".into());
66 }
67 Err(error) => {
68 if asked {
69 tell(app, "g1t could not check for updates", &error.to_string());
70 }
71 return Err(error.to_string());
72 }
73 };
74 let version = update.version.clone();
75 match update.download(|_, _| {}, || {}).await {
76 Ok(bytes) => {
77 *app.state::<config::State>().update.lock().unwrap() = Some(Ready { version: version.clone(), update, bytes });
78 tray::update_ready(&version);
79 tell(app, "g1t is ready to update", &format!("Version {version} is downloaded. Restart g1t from the tray to use it."));
80 Ok(format!("ready:{version}"))
81 }
82 Err(error) => {
83 if asked {
84 tell(app, "The update could not be downloaded", &error.to_string());
85 }
86 Err(error.to_string())
87 }
88 }
89}
90
91/// Installs what is downloaded, if anything. On Windows the installer
92/// ends this process itself; elsewhere the caller goes on.
93fn install(app: &AppHandle) -> bool {
94 let ready = app.state::<config::State>().update.lock().unwrap().take();
95 let Some(ready) = ready else { return false };
96 match ready.update.install(ready.bytes) {
97 Ok(()) => true,
98 Err(error) => {
99 tell(app, "The update could not be installed", &error.to_string());
100 false
101 }
102 }
103}
104
105/// The tray's restart: installs the update and starts the new version.
106pub fn restart(app: &AppHandle) {
107 if install(app) {
108 app.restart();
109 }
110}
111
112/// Quit, installing a downloaded update on the way out.
113pub fn quit(app: &AppHandle) {
114 install(app);
115 app.exit(0);
116}