Skip to content
139 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, Emitter, Manager};
12use tauri_plugin_notification::NotificationExt;
13use tauri_plugin_updater::{Update, UpdaterExt};
14
15use crate::{config, settings, tray};
16
17/// An update downloaded and checked, waiting to be installed.
18pub struct Ready {
19 pub version: String,
20 update: Update,
21 bytes: Vec<u8>,
22}
23
24const FIRST_CHECK: Duration = Duration::from_secs(15);
25const EVERY: Duration = Duration::from_secs(6 * 60 * 60);
26
27/// Checks soon after the start, then every six hours, unless settings
28/// turned that off; a person can still check from the menu.
29pub fn schedule(app: AppHandle) {
30 tauri::async_runtime::spawn(async move {
31 tokio::time::sleep(FIRST_CHECK).await;
32 loop {
33 if config::settings(&app).auto_update {
34 let _ = check(&app, false).await;
35 }
36 tokio::time::sleep(EVERY).await;
37 }
38 });
39}
40
41/// When the updater last heard back, in milliseconds since 1970.
42pub fn checked_at(app: &AppHandle) -> Option<u64> {
43 *app.state::<config::State>().checked_at.lock().unwrap()
44}
45
46fn heard_back(app: &AppHandle) {
47 let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|since| since.as_millis() as u64).ok();
48 *app.state::<config::State>().checked_at.lock().unwrap() = now;
49}
50
51/// The version downloaded and waiting, if one is.
52pub fn ready(app: &AppHandle) -> Option<String> {
53 app.state::<config::State>().update.lock().unwrap().as_ref().map(|ready| ready.version.clone())
54}
55
56fn tell(app: &AppHandle, title: &str, body: &str) {
57 let _ = app.notification().builder().title(title).body(body).show();
58}
59
60/// Looks for an update and downloads it. `asked` is a person asking from
61/// the tray or the site, who is told the answer either way; the timer
62/// says nothing unless there is something to install.
63pub async fn check(app: &AppHandle, asked: bool) -> Result<String, String> {
64 if let Some(version) = ready(app) {
65 if asked {
66 tell(app, "g1t is ready to update", &format!("Version {version} is downloaded. Restart g1t to use it."));
67 }
68 return Ok(format!("ready:{version}"));
69 }
70 let updater = app.updater().map_err(|error| error.to_string())?;
71 let update = match updater.check().await {
72 Ok(Some(update)) => {
73 heard_back(app);
74 update
75 }
76 Ok(None) => {
77 heard_back(app);
78 if asked {
79 tell(app, "g1t is up to date", &format!("You have version {}.", env!("CARGO_PKG_VERSION")));
80 }
81 return Ok("current".into());
82 }
83 Err(error) => {
84 if asked {
85 tell(app, "g1t could not check for updates", &error.to_string());
86 }
87 return Err(error.to_string());
88 }
89 };
90 let version = update.version.clone();
91 match update.download(|_, _| {}, || {}).await {
92 Ok(bytes) => {
93 *app.state::<config::State>().update.lock().unwrap() = Some(Ready { version: version.clone(), update, bytes });
94 tray::update_ready(&version);
95 let _ = app.emit_to(settings::SETTINGS, settings::READY_EVENT, version.clone());
96 tell(app, "g1t is ready to update", &format!("Version {version} is downloaded. Restart g1t from the tray to use it."));
97 Ok(format!("ready:{version}"))
98 }
99 Err(error) => {
100 if asked {
101 tell(app, "The update could not be downloaded", &error.to_string());
102 }
103 Err(error.to_string())
104 }
105 }
106}
107
108/// Installs what is downloaded, if anything. On Windows the installer
109/// ends this process itself; elsewhere the caller goes on.
110fn install(app: &AppHandle) -> bool {
111 let ready = app.state::<config::State>().update.lock().unwrap().take();
112 let Some(ready) = ready else { return false };
113 match ready.update.install(ready.bytes) {
114 Ok(()) => true,
115 Err(error) => {
116 tell(app, "The update could not be installed", &error.to_string());
117 false
118 }
119 }
120}
121
122/// The tray's restart: installs the update and starts the new version.
123/// The runner stops first, since on Windows the installer ends g1t.
124pub fn restart(app: &AppHandle) {
125 if ready(app).is_none() {
126 return;
127 }
128 crate::runner::shutdown(app);
129 if install(app) {
130 app.restart();
131 }
132}
133
134/// Quit, stopping the runner and installing a downloaded update on the way out.
135pub fn quit(app: &AppHandle) {
136 crate::runner::shutdown(app);
137 install(app);
138 app.exit(0);
139}