Skip to content
139 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

preparing for cloud work1//! 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
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r911use tauri::{AppHandle, Emitter, Manager};
preparing for cloud work12use tauri_plugin_notification::NotificationExt;
13use tauri_plugin_updater::{Update, UpdaterExt};
14
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r915use crate::{config, settings, tray};
preparing for cloud work16
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
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r927/// Checks soon after the start, then every six hours, unless settings
28/// turned that off; a person can still check from the menu.
preparing for cloud work29pub fn schedule(app: AppHandle) {
30 tauri::async_runtime::spawn(async move {
31 tokio::time::sleep(FIRST_CHECK).await;
32 loop {
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r933 if config::settings(&app).auto_update {
34 let _ = check(&app, false).await;
35 }
preparing for cloud work36 tokio::time::sleep(EVERY).await;
37 }
38 });
39}
40
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r941/// 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
preparing for cloud work51/// 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 {
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r972 Ok(Some(update)) => {
73 heard_back(app);
74 update
75 }
preparing for cloud work76 Ok(None) => {
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r977 heard_back(app);
preparing for cloud work78 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);
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r995 let _ = app.emit_to(settings::SETTINGS, settings::READY_EVENT, version.clone());
preparing for cloud work96 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.
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.123/// The runner stops first, since on Windows the installer ends g1t.
preparing for cloud work124pub fn restart(app: &AppHandle) {
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.125 if ready(app).is_none() {
126 return;
127 }
128 crate::runner::shutdown(app);
preparing for cloud work129 if install(app) {
130 app.restart();
131 }
132}
133
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.134/// Quit, stopping the runner and installing a downloaded update on the way out.
preparing for cloud work135pub fn quit(app: &AppHandle) {
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.136 crate::runner::shutdown(app);
preparing for cloud work137 install(app);
138 app.exit(0);
139}

This file's history is long; its oldest lines are credited to the oldest commit read.