Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
170 linesCodeBlameRaw
1//! Updates. The app asks g1t.sh/downloads/desktop/latest.json as it
2//! starts and every hour after, and downloads a newer version in the
3//! background. One found as it starts is installed straight away, and g1t
4//! opens again on it, before anyone has begun working in it: so a person
5//! whose copy misbehaves only has to open g1t again to be on the newest,
6//! fixed one (unless the runner is in the middle of a job, which is never
7//! cut short; then it waits like any other). One found later says so, with
8//! a notification and the tray's "Restart to update", since nothing is
9//! installed under someone mid-sentence; the restart, or quitting,
10//! installs it. The manifest is signed with g1t's
11//! release key, built into the app, and a download whose signature does
12//! not check out is thrown away (scripts/desktop-release.mjs).
13
14use std::time::Duration;
15
16use tauri::{AppHandle, Emitter, Manager};
17use tauri_plugin_notification::NotificationExt;
18use tauri_plugin_updater::{Update, UpdaterExt};
19
20use crate::{config, settings, tray};
21
22/// An update downloaded and checked, waiting to be installed.
23pub struct Ready {
24 pub version: String,
25 update: Update,
26 bytes: Vec<u8>,
27}
28
29/// The first look waits only for the window to be up.
30const FIRST_CHECK: Duration = Duration::from_secs(3);
31const EVERY: Duration = Duration::from_secs(60 * 60);
32/// An update downloaded this soon after g1t started is installed at once:
33/// nobody has begun working in it yet.
34const AT_START: Duration = Duration::from_secs(2 * 60);
35
36/// Checks as g1t starts, installing what it finds then, and every hour
37/// after, unless settings turned that off; a person can still check from
38/// the menu.
39pub fn schedule(app: AppHandle) {
40 tauri::async_runtime::spawn(async move {
41 let started = std::time::Instant::now();
42 tokio::time::sleep(FIRST_CHECK).await;
43 let mut first = true;
44 loop {
45 if config::settings(&app).auto_update {
46 let at_start = first && started.elapsed() < AT_START;
47 let found = look(&app, false, at_start).await;
48 if at_start && let Some(version) = found.ok().as_deref().and_then(|answer| answer.strip_prefix("ready:")).map(str::to_owned) {
49 if crate::runner::busy(&app) {
50 tell(&app, "g1t is ready to update", &format!("Version {version} is downloaded. Restart g1t from the tray to use it."));
51 } else {
52 tell(&app, "Updating g1t", &format!("g1t opens again on version {version} in a moment."));
53 restart(&app);
54 }
55 }
56 }
57 first = false;
58 tokio::time::sleep(EVERY).await;
59 }
60 });
61}
62
63/// When the updater last heard back, in milliseconds since 1970.
64pub fn checked_at(app: &AppHandle) -> Option<u64> {
65 *app.state::<config::State>().checked_at.lock().unwrap()
66}
67
68fn heard_back(app: &AppHandle) {
69 let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|since| since.as_millis() as u64).ok();
70 *app.state::<config::State>().checked_at.lock().unwrap() = now;
71}
72
73/// The version downloaded and waiting, if one is.
74pub fn ready(app: &AppHandle) -> Option<String> {
75 app.state::<config::State>().update.lock().unwrap().as_ref().map(|ready| ready.version.clone())
76}
77
78fn tell(app: &AppHandle, title: &str, body: &str) {
79 let _ = app.notification().builder().title(title).body(body).show();
80}
81
82/// Looks for an update and downloads it. `asked` is a person asking from
83/// the tray or the site, who is told the answer either way; the timer
84/// says nothing unless there is something to install.
85pub async fn check(app: &AppHandle, asked: bool) -> Result<String, String> {
86 look(app, asked, false).await
87}
88
89/// [`check`]; `at_start` says nothing of a download, which is installed at once.
90async fn look(app: &AppHandle, asked: bool, at_start: bool) -> Result<String, String> {
91 if let Some(version) = ready(app) {
92 if asked {
93 tell(app, "g1t is ready to update", &format!("Version {version} is downloaded. Restart g1t to use it."));
94 }
95 return Ok(format!("ready:{version}"));
96 }
97 let updater = app.updater().map_err(|error| error.to_string())?;
98 let update = match updater.check().await {
99 Ok(Some(update)) => {
100 heard_back(app);
101 update
102 }
103 Ok(None) => {
104 heard_back(app);
105 if asked {
106 tell(app, "g1t is up to date", &format!("You have version {}.", env!("CARGO_PKG_VERSION")));
107 }
108 return Ok("current".into());
109 }
110 Err(error) => {
111 if asked {
112 tell(app, "g1t could not check for updates", &error.to_string());
113 }
114 return Err(error.to_string());
115 }
116 };
117 let version = update.version.clone();
118 match update.download(|_, _| {}, || {}).await {
119 Ok(bytes) => {
120 *app.state::<config::State>().update.lock().unwrap() = Some(Ready { version: version.clone(), update, bytes });
121 tray::update_ready(&version);
122 let _ = app.emit_to(settings::SETTINGS, settings::READY_EVENT, version.clone());
123 // The settings the site draws, if they are open, say so too.
124 let _ = app.emit_to(tauri::EventTarget::webview(crate::frame::SITE), settings::READY_EVENT, version.clone());
125 if !at_start {
126 tell(app, "g1t is ready to update", &format!("Version {version} is downloaded. Restart g1t from the tray to use it."));
127 }
128 Ok(format!("ready:{version}"))
129 }
130 Err(error) => {
131 if asked {
132 tell(app, "The update could not be downloaded", &error.to_string());
133 }
134 Err(error.to_string())
135 }
136 }
137}
138
139/// Installs what is downloaded, if anything. On Windows the installer
140/// ends this process itself; elsewhere the caller goes on.
141fn install(app: &AppHandle) -> bool {
142 let ready = app.state::<config::State>().update.lock().unwrap().take();
143 let Some(ready) = ready else { return false };
144 match ready.update.install(ready.bytes) {
145 Ok(()) => true,
146 Err(error) => {
147 tell(app, "The update could not be installed", &error.to_string());
148 false
149 }
150 }
151}
152
153/// The tray's restart: installs the update and starts the new version.
154/// The runner stops first, since on Windows the installer ends g1t.
155pub fn restart(app: &AppHandle) {
156 if ready(app).is_none() {
157 return;
158 }
159 crate::runner::shutdown(app);
160 if install(app) {
161 app.restart();
162 }
163}
164
165/// Quit, stopping the runner and installing a downloaded update on the way out.
166pub fn quit(app: &AppHandle) {
167 crate::runner::shutdown(app);
168 install(app);
169 app.exit(0);
170}