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.
| The desktop app is 0.3.3: Settings opens again on Windows, and an update found as g1t starts installs itself. Since 0.3.0, Settings opened from the title bar's button or the site's account menu showed a white window and nothing else: both are synchronous commands, which run on the main thread, and on Windows a webview window made there deadlocks (Tauri's known issue), so the window appeared and its page never loaded. The settings window is now made on a thread of its own whoever asks, and Tauri hands the making to the event loop. And so that opening g1t again is all it takes to leave a copy that misbehaves, the app looks for a newer version as it starts, three seconds after the window rather than fifteen, and one it downloads within its first two minutes is installed straight away, g1t opening again on it, before anyone has begun working in it; a runner in the middle of a job is never cut short for it, and then the update waits for Restart to update as before. After that it looks every hour rather than every six, and what it finds still waits for a restart or quitting. The desktop guide's Updates section says so. | 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 | |
| preparing for cloud work | 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 | ||
| 14 | use std::time::Duration; | |
| 15 | ||
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 16 | use tauri::{AppHandle, Emitter, Manager}; |
| preparing for cloud work | 17 | use tauri_plugin_notification::NotificationExt; |
| 18 | use tauri_plugin_updater::{Update, UpdaterExt}; | |
| 19 | ||
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 20 | use crate::{config, settings, tray}; |
| preparing for cloud work | 21 | |
| 22 | /// An update downloaded and checked, waiting to be installed. | |
| 23 | pub struct Ready { | |
| 24 | pub version: String, | |
| 25 | update: Update, | |
| 26 | bytes: Vec<u8>, | |
| 27 | } | |
| 28 | ||
| The desktop app is 0.3.3: Settings opens again on Windows, and an update found as g1t starts installs itself. Since 0.3.0, Settings opened from the title bar's button or the site's account menu showed a white window and nothing else: both are synchronous commands, which run on the main thread, and on Windows a webview window made there deadlocks (Tauri's known issue), so the window appeared and its page never loaded. The settings window is now made on a thread of its own whoever asks, and Tauri hands the making to the event loop. And so that opening g1t again is all it takes to leave a copy that misbehaves, the app looks for a newer version as it starts, three seconds after the window rather than fifteen, and one it downloads within its first two minutes is installed straight away, g1t opening again on it, before anyone has begun working in it; a runner in the middle of a job is never cut short for it, and then the update waits for Restart to update as before. After that it looks every hour rather than every six, and what it finds still waits for a restart or quitting. The desktop guide's Updates section says so. | 29 | /// The first look waits only for the window to be up. |
| 30 | const FIRST_CHECK: Duration = Duration::from_secs(3); | |
| 31 | const 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. | |
| 34 | const AT_START: Duration = Duration::from_secs(2 * 60); | |
| preparing for cloud work | 35 | |
| The desktop app is 0.3.3: Settings opens again on Windows, and an update found as g1t starts installs itself. Since 0.3.0, Settings opened from the title bar's button or the site's account menu showed a white window and nothing else: both are synchronous commands, which run on the main thread, and on Windows a webview window made there deadlocks (Tauri's known issue), so the window appeared and its page never loaded. The settings window is now made on a thread of its own whoever asks, and Tauri hands the making to the event loop. And so that opening g1t again is all it takes to leave a copy that misbehaves, the app looks for a newer version as it starts, three seconds after the window rather than fifteen, and one it downloads within its first two minutes is installed straight away, g1t opening again on it, before anyone has begun working in it; a runner in the middle of a job is never cut short for it, and then the update waits for Restart to update as before. After that it looks every hour rather than every six, and what it finds still waits for a restart or quitting. The desktop guide's Updates section says so. | 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. | |
| preparing for cloud work | 39 | pub fn schedule(app: AppHandle) { |
| 40 | tauri::async_runtime::spawn(async move { | |
| The desktop app is 0.3.3: Settings opens again on Windows, and an update found as g1t starts installs itself. Since 0.3.0, Settings opened from the title bar's button or the site's account menu showed a white window and nothing else: both are synchronous commands, which run on the main thread, and on Windows a webview window made there deadlocks (Tauri's known issue), so the window appeared and its page never loaded. The settings window is now made on a thread of its own whoever asks, and Tauri hands the making to the event loop. And so that opening g1t again is all it takes to leave a copy that misbehaves, the app looks for a newer version as it starts, three seconds after the window rather than fifteen, and one it downloads within its first two minutes is installed straight away, g1t opening again on it, before anyone has begun working in it; a runner in the middle of a job is never cut short for it, and then the update waits for Restart to update as before. After that it looks every hour rather than every six, and what it finds still waits for a restart or quitting. The desktop guide's Updates section says so. | 41 | let started = std::time::Instant::now(); |
| preparing for cloud work | 42 | tokio::time::sleep(FIRST_CHECK).await; |
| The desktop app is 0.3.3: Settings opens again on Windows, and an update found as g1t starts installs itself. Since 0.3.0, Settings opened from the title bar's button or the site's account menu showed a white window and nothing else: both are synchronous commands, which run on the main thread, and on Windows a webview window made there deadlocks (Tauri's known issue), so the window appeared and its page never loaded. The settings window is now made on a thread of its own whoever asks, and Tauri hands the making to the event loop. And so that opening g1t again is all it takes to leave a copy that misbehaves, the app looks for a newer version as it starts, three seconds after the window rather than fifteen, and one it downloads within its first two minutes is installed straight away, g1t opening again on it, before anyone has begun working in it; a runner in the middle of a job is never cut short for it, and then the update waits for Restart to update as before. After that it looks every hour rather than every six, and what it finds still waits for a restart or quitting. The desktop guide's Updates section says so. | 43 | let mut first = true; |
| preparing for cloud work | 44 | loop { |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 45 | if config::settings(&app).auto_update { |
| The desktop app is 0.3.3: Settings opens again on Windows, and an update found as g1t starts installs itself. Since 0.3.0, Settings opened from the title bar's button or the site's account menu showed a white window and nothing else: both are synchronous commands, which run on the main thread, and on Windows a webview window made there deadlocks (Tauri's known issue), so the window appeared and its page never loaded. The settings window is now made on a thread of its own whoever asks, and Tauri hands the making to the event loop. And so that opening g1t again is all it takes to leave a copy that misbehaves, the app looks for a newer version as it starts, three seconds after the window rather than fifteen, and one it downloads within its first two minutes is installed straight away, g1t opening again on it, before anyone has begun working in it; a runner in the middle of a job is never cut short for it, and then the update waits for Restart to update as before. After that it looks every hour rather than every six, and what it finds still waits for a restart or quitting. The desktop guide's Updates section says so. | 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 | } | |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 56 | } |
| The desktop app is 0.3.3: Settings opens again on Windows, and an update found as g1t starts installs itself. Since 0.3.0, Settings opened from the title bar's button or the site's account menu showed a white window and nothing else: both are synchronous commands, which run on the main thread, and on Windows a webview window made there deadlocks (Tauri's known issue), so the window appeared and its page never loaded. The settings window is now made on a thread of its own whoever asks, and Tauri hands the making to the event loop. And so that opening g1t again is all it takes to leave a copy that misbehaves, the app looks for a newer version as it starts, three seconds after the window rather than fifteen, and one it downloads within its first two minutes is installed straight away, g1t opening again on it, before anyone has begun working in it; a runner in the middle of a job is never cut short for it, and then the update waits for Restart to update as before. After that it looks every hour rather than every six, and what it finds still waits for a restart or quitting. The desktop guide's Updates section says so. | 57 | first = false; |
| preparing for cloud work | 58 | tokio::time::sleep(EVERY).await; |
| 59 | } | |
| 60 | }); | |
| 61 | } | |
| 62 | ||
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 63 | /// When the updater last heard back, in milliseconds since 1970. |
| 64 | pub fn checked_at(app: &AppHandle) -> Option<u64> { | |
| 65 | *app.state::<config::State>().checked_at.lock().unwrap() | |
| 66 | } | |
| 67 | ||
| 68 | fn 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 | ||
| preparing for cloud work | 73 | /// The version downloaded and waiting, if one is. |
| 74 | pub fn ready(app: &AppHandle) -> Option<String> { | |
| 75 | app.state::<config::State>().update.lock().unwrap().as_ref().map(|ready| ready.version.clone()) | |
| 76 | } | |
| 77 | ||
| 78 | fn 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. | |
| 85 | pub async fn check(app: &AppHandle, asked: bool) -> Result<String, String> { | |
| The desktop app is 0.3.3: Settings opens again on Windows, and an update found as g1t starts installs itself. Since 0.3.0, Settings opened from the title bar's button or the site's account menu showed a white window and nothing else: both are synchronous commands, which run on the main thread, and on Windows a webview window made there deadlocks (Tauri's known issue), so the window appeared and its page never loaded. The settings window is now made on a thread of its own whoever asks, and Tauri hands the making to the event loop. And so that opening g1t again is all it takes to leave a copy that misbehaves, the app looks for a newer version as it starts, three seconds after the window rather than fifteen, and one it downloads within its first two minutes is installed straight away, g1t opening again on it, before anyone has begun working in it; a runner in the middle of a job is never cut short for it, and then the update waits for Restart to update as before. After that it looks every hour rather than every six, and what it finds still waits for a restart or quitting. The desktop guide's Updates section says so. | 86 | look(app, asked, false).await |
| 87 | } | |
| 88 | ||
| 89 | /// [`check`]; `at_start` says nothing of a download, which is installed at once. | |
| 90 | async fn look(app: &AppHandle, asked: bool, at_start: bool) -> Result<String, String> { | |
| preparing for cloud work | 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 { | |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 99 | Ok(Some(update)) => { |
| 100 | heard_back(app); | |
| 101 | update | |
| 102 | } | |
| preparing for cloud work | 103 | Ok(None) => { |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 104 | heard_back(app); |
| preparing for cloud work | 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); | |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 122 | let _ = app.emit_to(settings::SETTINGS, settings::READY_EVENT, version.clone()); |
| The desktop app is 0.3.3: Settings opens again on Windows, and an update found as g1t starts installs itself. Since 0.3.0, Settings opened from the title bar's button or the site's account menu showed a white window and nothing else: both are synchronous commands, which run on the main thread, and on Windows a webview window made there deadlocks (Tauri's known issue), so the window appeared and its page never loaded. The settings window is now made on a thread of its own whoever asks, and Tauri hands the making to the event loop. And so that opening g1t again is all it takes to leave a copy that misbehaves, the app looks for a newer version as it starts, three seconds after the window rather than fifteen, and one it downloads within its first two minutes is installed straight away, g1t opening again on it, before anyone has begun working in it; a runner in the middle of a job is never cut short for it, and then the update waits for Restart to update as before. After that it looks every hour rather than every six, and what it finds still waits for a restart or quitting. The desktop guide's Updates section says so. | 123 | if !at_start { |
| 124 | tell(app, "g1t is ready to update", &format!("Version {version} is downloaded. Restart g1t from the tray to use it.")); | |
| 125 | } | |
| preparing for cloud work | 126 | Ok(format!("ready:{version}")) |
| 127 | } | |
| 128 | Err(error) => { | |
| 129 | if asked { | |
| 130 | tell(app, "The update could not be downloaded", &error.to_string()); | |
| 131 | } | |
| 132 | Err(error.to_string()) | |
| 133 | } | |
| 134 | } | |
| 135 | } | |
| 136 | ||
| 137 | /// Installs what is downloaded, if anything. On Windows the installer | |
| 138 | /// ends this process itself; elsewhere the caller goes on. | |
| 139 | fn install(app: &AppHandle) -> bool { | |
| 140 | let ready = app.state::<config::State>().update.lock().unwrap().take(); | |
| 141 | let Some(ready) = ready else { return false }; | |
| 142 | match ready.update.install(ready.bytes) { | |
| 143 | Ok(()) => true, | |
| 144 | Err(error) => { | |
| 145 | tell(app, "The update could not be installed", &error.to_string()); | |
| 146 | false | |
| 147 | } | |
| 148 | } | |
| 149 | } | |
| 150 | ||
| 151 | /// 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. | 152 | /// The runner stops first, since on Windows the installer ends g1t. |
| preparing for cloud work | 153 | pub 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. | 154 | if ready(app).is_none() { |
| 155 | return; | |
| 156 | } | |
| 157 | crate::runner::shutdown(app); | |
| preparing for cloud work | 158 | if install(app) { |
| 159 | app.restart(); | |
| 160 | } | |
| 161 | } | |
| 162 | ||
| 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. | 163 | /// Quit, stopping the runner and installing a downloaded update on the way out. |
| preparing for cloud work | 164 | pub 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. | 165 | crate::runner::shutdown(app); |
| preparing for cloud work | 166 | install(app); |
| 167 | app.exit(0); | |
| 168 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.