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.
| 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. | 1 | //! The runner in the app: g1t's self-hosted runner, started for the |
| 2 | //! signed-in person when g1t opens and stopped when it quits, so their | |
| 3 | //! computer runs their workspace's `runs-on: self-hosted` jobs with | |
| 4 | //! nothing to install, register or start by hand | |
| 5 | //! (docs.g1t.sh/guides/desktop/#the-runner). | |
| 6 | //! | |
| 7 | //! - **When.** On by default; settings turn it off ("Run jobs on this | |
| 8 | //! computer while g1t is open"), and Stop in settings or the tray stops | |
| 9 | //! it until Start, or until g1t starts again. It starts a few seconds | |
| 10 | //! after the window, never before. | |
| 11 | //! - **How.** The backend settings choose (backend.rs): Sandboxed, the | |
| 12 | //! default, where nothing a job does reaches this computer; Docker; or | |
| 13 | //! directly on this computer. The two that are not sandboxes are only | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 14 | //! ever chosen by the person, and nothing falls back to them. Sandboxed |
| 15 | //! is `g1t-runner sandbox` (launcher.rs) on every OS: `up` a few seconds | |
| 16 | //! after the window, `status` every few seconds while it runs, `down` | |
| 17 | //! as g1t quits, and `down --drain` when another backend is chosen. | |
| 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. | 18 | //! - **Registering.** The first time, and when the workspace or backend |
| 19 | //! changes, the app needs a registration token for the workspace. Only | |
| 20 | //! the site can make one, for an owner who is signed in: the app asks | |
| 21 | //! the site's page (`g1t-runner-wanted`), the page asks the server | |
| 22 | //! (`POST /-/desktop/runner-token`) and hands the token to the app | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 23 | //! (`runner_register` in bridge.rs), which gives it to `g1t-runner` |
| 24 | //! once: in its environment, or to the sandbox's first `up`. It is never | |
| 25 | //! shown or written to the log. The registration is kept | |
| 26 | //! (registration.json and the runner's own folder; the sandbox keeps its | |
| 27 | //! own, and sandbox-registration.json says whose it is) and used on | |
| 28 | //! every later start. | |
| 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. | 29 | //! - **Up to date.** Soon after it starts and every six hours, while the |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 30 | //! runner is idle, the app looks for a newer release and puts it in |
| 31 | //! place, and has the sandbox take the newest image (`sandbox update`): | |
| 32 | //! never in the middle of a job. | |
| 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. | 33 | //! - **Seen.** What it is doing, and when it is not running why and how to |
| 34 | //! fix it (diagnosis.rs), is in the tray, in settings, and on the site's | |
| 35 | //! Runners page (`g1t-runner-status`), read from what its log says | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 36 | //! (log.rs), or for the sandbox from its status. The whole log is kept |
| 37 | //! in `runner/runner.log`. | |
| 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. | 38 | //! |
| 39 | //! All of it runs on a thread of its own, which owns the runner's process | |
| 40 | //! and is told what happened through one channel. | |
| 41 | ||
| 42 | mod backend; | |
| 43 | pub mod diagnosis; | |
| 44 | mod launcher; | |
| 45 | mod log; | |
| 46 | mod native; | |
| 47 | ||
| 48 | use std::collections::VecDeque; | |
| 49 | use std::fs::{File, OpenOptions}; | |
| 50 | use std::io::{BufRead, BufReader, Read, Write}; | |
| 51 | use std::path::PathBuf; | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 52 | use std::process::{Child, Stdio}; |
| 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. | 53 | use std::sync::Mutex; |
| 54 | use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender}; | |
| 55 | use std::time::{Duration, Instant}; | |
| 56 | ||
| 57 | use serde::{Deserialize, Serialize}; | |
| 58 | use tauri::{AppHandle, Emitter, EventTarget, Manager}; | |
| 59 | use url::Url; | |
| 60 | ||
| 61 | pub use backend::Backend; | |
| 62 | use backend::{Engine, Missing}; | |
| 63 | use diagnosis::{Code, Diagnosis, Facts}; | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 64 | use launcher::{Down, State, Status}; |
| 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. | 65 | |
| 66 | use crate::{config, frame, settings, tray}; | |
| 67 | ||
| 68 | /// What settings, the tray and the site listen for: the runner's status changed. | |
| 69 | pub const STATUS_EVENT: &str = "g1t-runner-status"; | |
| 70 | /// What the site's page listens for: the app needs a registration token for a workspace. | |
| 71 | pub const WANTED_EVENT: &str = "g1t-runner-wanted"; | |
| 72 | ||
| 73 | /// How long after the window the runner starts. | |
| 74 | const AFTER_START: Duration = Duration::from_secs(4); | |
| 75 | /// How often the thread looks at the runner when nothing has happened. | |
| 76 | const TICK: Duration = Duration::from_secs(5); | |
| 77 | /// How long after starting the first look for an update is, then how often. | |
| 78 | const FIRST_UPDATE: Duration = Duration::from_secs(90); | |
| 79 | const UPDATE_EVERY: Duration = Duration::from_secs(6 * 60 * 60); | |
| 80 | /// How often a backend that cannot start is looked at again. | |
| 81 | const LOOK_AGAIN: Duration = Duration::from_secs(30); | |
| 82 | /// How long the app waits for the site to hand over a token before asking again. | |
| 83 | const ASK_AGAIN: Duration = Duration::from_secs(60); | |
| 84 | /// The longest the app waits before starting a runner that stopped by itself again. | |
| 85 | const MOST_BACKOFF: Duration = Duration::from_secs(10 * 60); | |
| 86 | /// The runner says when a poll fails, and nothing when one works: this | |
| 87 | /// long without another failure, it is back (its longest wait is 60s). | |
| 88 | const BACK_ONLINE: Duration = Duration::from_secs(75); | |
| 89 | /// The runner's log is started over when it is bigger than this. | |
| 90 | const LOG_LIMIT: u64 = 5 * 1024 * 1024; | |
| 91 | /// The last lines of the log settings show. | |
| 92 | const TAIL: usize = 40; | |
| 93 | ||
| 94 | /// A workspace the site says the person may add runners to: those they own. | |
| 95 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 96 | pub struct Offered { | |
| 97 | pub slug: String, | |
| 98 | #[serde(default)] | |
| 99 | pub name: Option<String>, | |
| 100 | } | |
| 101 | ||
| 102 | /// What the runner is doing, as the thread last said. | |
| 103 | #[derive(Clone, Debug, Default, PartialEq)] | |
| 104 | struct Now { | |
| 105 | code: Code, | |
| 106 | detail: Option<String>, | |
| 107 | exit_code: Option<i32>, | |
| 108 | wait_seconds: Option<u64>, | |
| 109 | job: Option<String>, | |
| 110 | version: Option<String>, | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 111 | /// What the sandbox says to do, for a cause in its own words. |
| 112 | fix: Option<String>, | |
| 113 | /// How the sandbox is, while jobs run sandboxed. | |
| 114 | sandbox: Option<Status>, | |
| 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. | 115 | } |
| 116 | ||
| 117 | /// What the site last said about the person. | |
| 118 | #[derive(Clone, Debug, Default, PartialEq)] | |
| 119 | struct Site { | |
| 120 | /// Unknown until the site's first page loads. | |
| 121 | signed_in: Option<bool>, | |
| 122 | owned: Vec<Offered>, | |
| 123 | current: Option<String>, | |
| 124 | } | |
| 125 | ||
| 126 | /// What the rest of the app shares with the thread. | |
| 127 | #[derive(Default)] | |
| 128 | pub struct Shared { | |
| 129 | now: Mutex<Now>, | |
| 130 | /// The workspace a token is wanted for. | |
| 131 | wanted: Mutex<Option<String>>, | |
| 132 | site: Mutex<Site>, | |
| 133 | /// Stopped by the person until Start, or until g1t starts again. | |
| 134 | paused: Mutex<bool>, | |
| 135 | tail: Mutex<VecDeque<String>>, | |
| 136 | tx: Mutex<Option<Sender<Ask>>>, | |
| 137 | } | |
| 138 | ||
| 139 | /// What settings, the tray and the site are shown. | |
| 140 | #[derive(Clone, Debug, PartialEq, Serialize)] | |
| 141 | #[serde(rename_all = "camelCase")] | |
| 142 | pub struct View { | |
| 143 | pub diagnosis: Diagnosis, | |
| 144 | /// What the site says of this computer once g1t is closed on it. | |
| 145 | pub closed: Diagnosis, | |
| 146 | pub running: bool, | |
| 147 | pub version: Option<String>, | |
| 148 | pub backend: Backend, | |
| 149 | pub workspace: Option<String>, | |
| 150 | /// The workspaces it may run for, from the site. | |
| 151 | pub workspaces: Vec<Offered>, | |
| 152 | /// The setting: run jobs while g1t is open. | |
| 153 | pub enabled: bool, | |
| 154 | pub paused: bool, | |
| 155 | /// The workspace's Runners page. | |
| 156 | pub link: Option<String>, | |
| 157 | /// This computer's name, which the runner has on g1t. | |
| 158 | pub machine: String, | |
| 159 | pub platform: String, | |
| 160 | /// The last lines of the runner's log. | |
| 161 | pub log: Vec<String>, | |
| 162 | } | |
| 163 | ||
| 164 | /// What the thread is told. | |
| 165 | enum Ask { | |
| 166 | /// Something changed: settings, Start or Stop, Retry. | |
| 167 | Look, | |
| 168 | Retry, | |
| 169 | Token { workspace: String, token: String }, | |
| 170 | Refused { workspace: String, allowed: bool, why: String }, | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 171 | /// A line of the runner's output, or the launcher's; true from its standard output. |
| 172 | Line(u64, String, bool), | |
| 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. | 173 | Exited(u64), |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 174 | /// What `sandbox status` said. |
| 175 | Sandbox(u64, Result<Box<Status>, String>), | |
| 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. | 176 | Quit(Sender<()>), |
| 177 | } | |
| 178 | ||
| 179 | /// The registration the app made, besides what the runner keeps itself. | |
| 180 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] | |
| 181 | struct Registration { | |
| 182 | server: String, | |
| 183 | workspace: String, | |
| 184 | backend: Backend, | |
| 185 | } | |
| 186 | ||
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 187 | /// Whose the sandbox's registration is. The sandbox keeps its own, and |
| 188 | /// keeps it while another backend runs, so choosing Sandboxed again needs | |
| 189 | /// no new one; this says which workspace it is for. | |
| 190 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] | |
| 191 | struct SandboxRegistration { | |
| 192 | server: String, | |
| 193 | workspace: String, | |
| 194 | /// False from the `up` that was given a token until it registered. | |
| 195 | registered: bool, | |
| 196 | } | |
| 197 | ||
| 198 | impl SandboxRegistration { | |
| 199 | fn path(app: &AppHandle) -> PathBuf { | |
| 200 | folder(app).join("sandbox-registration.json") | |
| 201 | } | |
| 202 | fn load(app: &AppHandle) -> Option<SandboxRegistration> { | |
| 203 | std::fs::read(Self::path(app)).ok().and_then(|bytes| serde_json::from_slice(&bytes).ok()) | |
| 204 | } | |
| 205 | fn save(&self, app: &AppHandle) { | |
| 206 | let _ = std::fs::create_dir_all(folder(app)); | |
| 207 | let _ = std::fs::write(Self::path(app), serde_json::to_vec_pretty(self).unwrap_or_default()); | |
| 208 | } | |
| 209 | fn clear(app: &AppHandle) { | |
| 210 | let _ = std::fs::remove_file(Self::path(app)); | |
| 211 | } | |
| 212 | } | |
| 213 | ||
| 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. | 214 | fn folder(app: &AppHandle) -> PathBuf { |
| 215 | app.path().app_data_dir().unwrap_or_else(|_| std::env::temp_dir().join("sh.g1t.desktop")).join("runner") | |
| 216 | } | |
| 217 | ||
| 218 | impl Registration { | |
| 219 | fn path(app: &AppHandle) -> PathBuf { | |
| 220 | folder(app).join("registration.json") | |
| 221 | } | |
| 222 | fn load(app: &AppHandle) -> Option<Registration> { | |
| 223 | std::fs::read(Self::path(app)).ok().and_then(|bytes| serde_json::from_slice(&bytes).ok()) | |
| 224 | } | |
| 225 | fn save(&self, app: &AppHandle) { | |
| 226 | let _ = std::fs::create_dir_all(folder(app)); | |
| 227 | let _ = std::fs::write(Self::path(app), serde_json::to_vec_pretty(self).unwrap_or_default()); | |
| 228 | } | |
| 229 | fn clear(app: &AppHandle) { | |
| 230 | let _ = std::fs::remove_file(Self::path(app)); | |
| 231 | } | |
| 232 | } | |
| 233 | ||
| 234 | /// Where a native runner of a backend keeps its configuration. | |
| 235 | fn native_dir(app: &AppHandle, backend: Backend) -> PathBuf { | |
| 236 | folder(app).join(match backend { | |
| 237 | Backend::Docker => "docker", | |
| 238 | Backend::Host => "host", | |
| 239 | Backend::Sandboxed => launcher::FOLDER, | |
| 240 | }) | |
| 241 | } | |
| 242 | ||
| 243 | /// The workspace the runner is for: the one settings name, else the one it | |
| 244 | /// is registered with, else the one the person is in on the site, else the | |
| 245 | /// first they own. | |
| 246 | fn workspace(app: &AppHandle) -> Option<String> { | |
| 247 | let settings = config::settings(app); | |
| 248 | if let Some(chosen) = settings.runner_workspace.filter(|w| !w.is_empty()) { | |
| 249 | return Some(chosen); | |
| 250 | } | |
| 251 | let server = config::server(app).to_string(); | |
| 252 | if let Some(registration) = Registration::load(app).filter(|r| r.server == server) { | |
| 253 | return Some(registration.workspace); | |
| 254 | } | |
| 255 | let site = app.state::<Shared>().site.lock().unwrap().clone(); | |
| 256 | site.current.filter(|slug| site.owned.iter().any(|o| &o.slug == slug)).or_else(|| site.owned.first().map(|o| o.slug.clone())) | |
| 257 | } | |
| 258 | ||
| 259 | fn facts(app: &AppHandle, now: &Now) -> Facts { | |
| 260 | let server = config::server(app); | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 261 | let backend = config::settings(app).runner_backend; |
| 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. | 262 | Facts { |
| 263 | workspace: workspace(app), | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 264 | backend: backend.label(), |
| 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. | 265 | machine: backend::machine_name(), |
| 266 | job: now.job.clone(), | |
| 267 | detail: now.detail.clone(), | |
| 268 | exit_code: now.exit_code, | |
| 269 | wait_seconds: now.wait_seconds, | |
| 270 | server: server.host_str().unwrap_or("g1t").to_owned(), | |
| 271 | platform: std::env::consts::OS.into(), | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 272 | fix: now.fix.clone(), |
| 273 | sandbox: now.sandbox.clone().filter(|_| backend == Backend::Sandboxed), | |
| 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. | 274 | } |
| 275 | } | |
| 276 | ||
| 277 | pub fn view(app: &AppHandle) -> View { | |
| 278 | let shared = app.state::<Shared>(); | |
| 279 | let now = shared.now.lock().unwrap().clone(); | |
| 280 | let settings = config::settings(app); | |
| 281 | let facts = facts(app, &now); | |
| 282 | let server = config::server(app); | |
| 283 | View { | |
| 284 | diagnosis: diagnosis::diagnose(now.code, &facts), | |
| 285 | closed: diagnosis::closed(&facts), | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 286 | running: matches!(now.code, Code::Idle | Code::Busy | Code::Offline) || facts.sandbox.as_ref().is_some_and(Status::running), |
| 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. | 287 | version: now.version, |
| 288 | backend: settings.runner_backend, | |
| 289 | link: facts.workspace.as_ref().and_then(|w| server.join(&format!("{w}/-/runners")).ok()).map(|u| u.to_string()), | |
| 290 | workspace: facts.workspace, | |
| 291 | workspaces: shared.site.lock().unwrap().owned.clone(), | |
| 292 | enabled: settings.runner, | |
| 293 | paused: *shared.paused.lock().unwrap(), | |
| 294 | machine: facts.machine, | |
| 295 | platform: facts.platform, | |
| 296 | log: shared.tail.lock().unwrap().iter().cloned().collect(), | |
| 297 | } | |
| 298 | } | |
| 299 | ||
| 300 | /// The tray's line: `Runner: Waiting for jobs`, at most 60 characters. | |
| 301 | pub fn headline(view: &View) -> String { | |
| 302 | let line = format!("Runner: {}", view.diagnosis.title); | |
| 303 | if line.chars().count() > 60 { line.chars().take(59).collect::<String>() + "…" } else { line } | |
| 304 | } | |
| 305 | ||
| 306 | /// Tells everyone what the runner is doing now. | |
| 307 | fn publish(app: &AppHandle) { | |
| 308 | let view = view(app); | |
| 309 | tray::runner_changed(app, &view); | |
| 310 | let _ = app.emit_to(settings::SETTINGS, STATUS_EVENT, view.clone()); | |
| 311 | let _ = app.emit_to(EventTarget::webview(frame::SITE), STATUS_EVENT, view); | |
| 312 | } | |
| 313 | ||
| 314 | fn ask(app: &AppHandle, ask: Ask) { | |
| 315 | if let Some(tx) = app.state::<Shared>().tx.lock().unwrap().as_ref() { | |
| 316 | let _ = tx.send(ask); | |
| 317 | } | |
| 318 | } | |
| 319 | ||
| 320 | /// Starts the thread that keeps the runner, as the app starts. | |
| 321 | pub fn install(app: &AppHandle) { | |
| 322 | let (tx, rx) = mpsc::channel(); | |
| 323 | *app.state::<Shared>().tx.lock().unwrap() = Some(tx.clone()); | |
| 324 | let handle = app.clone(); | |
| 325 | let spawned = std::thread::Builder::new().name("g1t-runner".into()).spawn(move || Keeper::new(handle, tx).keep(rx)); | |
| 326 | if let Err(error) = spawned { | |
| 327 | eprintln!("g1t: the runner's thread could not start: {error}"); | |
| 328 | } | |
| 329 | } | |
| 330 | ||
| 331 | /// Settings changed: the thread looks again. | |
| 332 | pub fn settings_changed(app: &AppHandle) { | |
| 333 | ask(app, Ask::Look); | |
| 334 | publish(app); | |
| 335 | } | |
| 336 | ||
| 337 | /// Start or Stop, from settings or the tray. | |
| 338 | pub fn set_paused(app: &AppHandle, paused: bool) { | |
| 339 | *app.state::<Shared>().paused.lock().unwrap() = paused; | |
| 340 | // Start with the setting off turns the setting on: that is what was asked. | |
| 341 | if !paused && !config::settings(app).runner { | |
| 342 | let mut settings = config::settings(app); | |
| 343 | settings.runner = true; | |
| 344 | let _ = settings::apply(app, settings); | |
| 345 | } | |
| 346 | settings_changed(app); | |
| 347 | } | |
| 348 | ||
| 349 | /// The tray's Start or Stop row. | |
| 350 | pub fn toggle(app: &AppHandle) { | |
| 351 | let view = view(app); | |
| 352 | set_paused(app, view.enabled && !view.paused); | |
| 353 | } | |
| 354 | ||
| 355 | /// Retry and Restart: now, rather than when the wait is over. | |
| 356 | pub fn retry(app: &AppHandle) { | |
| 357 | *app.state::<Shared>().paused.lock().unwrap() = false; | |
| 358 | ask(app, Ask::Retry); | |
| 359 | } | |
| 360 | ||
| 361 | /// What the site says about the person, on every page: whether they are | |
| 362 | /// signed in, the workspaces they own, and the one they are in. | |
| 363 | pub fn offer(app: &AppHandle, signed_in: bool, owned: Vec<Offered>, current: Option<String>) { | |
| 364 | let next = Site { signed_in: Some(signed_in), owned, current: current.map(|c| c.to_ascii_lowercase()) }; | |
| 365 | let changed = { | |
| 366 | let shared = app.state::<Shared>(); | |
| 367 | let mut site = shared.site.lock().unwrap(); | |
| 368 | let changed = *site != next; | |
| 369 | *site = next; | |
| 370 | changed | |
| 371 | }; | |
| 372 | if changed { | |
| 373 | settings_changed(app); | |
| 374 | } | |
| 375 | } | |
| 376 | ||
| 377 | /// The site handed over a registration token, or said why it could not: | |
| 378 | /// `allowed` is false when the person may not add runners to the workspace. | |
| 379 | pub fn register(app: &AppHandle, workspace: String, token: Option<String>, allowed: bool, error: Option<String>) { | |
| 380 | match token { | |
| 381 | Some(token) if token.starts_with("g1trt_") => ask(app, Ask::Token { workspace, token }), | |
| 382 | _ => ask(app, Ask::Refused { workspace, allowed, why: error.unwrap_or_else(|| "g1t didn't make a registration token.".into()) }), | |
| 383 | } | |
| 384 | } | |
| 385 | ||
| 386 | /// A diagnosis's button, or Start and Stop: from the settings window, or | |
| 387 | /// from the site's card for this computer. | |
| 388 | pub fn act(app: &AppHandle, action: &str) -> Result<(), String> { | |
| 389 | use tauri_plugin_opener::OpenerExt; | |
| 390 | let browse = |url: &str| app.opener().open_url(url, None::<&str>).map_err(|e| e.to_string()); | |
| 391 | match action { | |
| 392 | "start" => { | |
| 393 | set_paused(app, false); | |
| 394 | retry(app); | |
| 395 | } | |
| 396 | "stop" => set_paused(app, true), | |
| 397 | "retry" | "restart" => retry(app), | |
| 398 | "sign_in" => crate::links::go(app, "/login"), | |
| 399 | "new_workspace" => crate::links::go(app, "/workspaces/new"), | |
| 400 | "view_runners" => match workspace(app) { | |
| 401 | Some(workspace) => crate::links::go(app, &format!("/{workspace}/-/runners")), | |
| 402 | None => crate::links::go(app, "/"), | |
| 403 | }, | |
| 404 | "choose_workspace" | "choose_backend" => settings::open(app), | |
| 405 | "view_log" => { | |
| 406 | let path = log_file(app); | |
| 407 | if !path.exists() { | |
| 408 | return Err("The runner hasn't written a log yet.".into()); | |
| 409 | } | |
| 410 | app.opener().open_path(path.to_string_lossy(), None::<&str>).map_err(|e| e.to_string())?; | |
| 411 | } | |
| 412 | "open_docker" => open_docker(app)?, | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 413 | "copy_wsl_install" => copy(launcher::WSL_INSTALL)?, |
| 414 | "copy_vfkit_install" => copy(launcher::VFKIT_INSTALL)?, | |
| 415 | "windows_features" => windows_features()?, | |
| 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. | 416 | "get_docker" => browse("https://docs.docker.com/get-started/get-docker/")?, |
| 417 | "install_sysbox" => browse("https://github.com/nestybox/sysbox#installation")?, | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 418 | "learn_more" => browse("https://docs.g1t.sh/guides/desktop/#how-jobs-run")?, |
| 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. | 419 | "report" => browse(config::server(app).join("support").map_err(|e| e.to_string())?.as_str())?, |
| 420 | other => return Err(format!("the runner has no action called {other}")), | |
| 421 | } | |
| 422 | Ok(()) | |
| 423 | } | |
| 424 | ||
| 425 | /// Starts Docker Desktop, where it is installed in its usual place. | |
| 426 | fn open_docker(app: &AppHandle) -> Result<(), String> { | |
| 427 | use tauri_plugin_opener::OpenerExt; | |
| 428 | if cfg!(target_os = "macos") { | |
| 429 | return app.opener().open_path("/Applications/Docker.app", None::<&str>).map_err(|e| e.to_string()); | |
| 430 | } | |
| 431 | if cfg!(windows) { | |
| 432 | let program = std::env::var_os("ProgramFiles").map(PathBuf::from).unwrap_or_else(|| PathBuf::from(r"C:\Program Files")); | |
| 433 | let desktop = program.join("Docker").join("Docker").join("Docker Desktop.exe"); | |
| 434 | if desktop.exists() { | |
| 435 | return app.opener().open_path(desktop.to_string_lossy(), None::<&str>).map_err(|e| e.to_string()); | |
| 436 | } | |
| 437 | return Err("Docker Desktop isn't installed in its usual place. Get Docker to install it.".into()); | |
| 438 | } | |
| 439 | Err("Start Docker with your system's service manager, such as `sudo systemctl start docker`.".into()) | |
| 440 | } | |
| 441 | ||
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 442 | /// Puts a command on the clipboard, for the person to paste where it runs. |
| 443 | fn copy(text: &str) -> Result<(), String> { | |
| 444 | let tools: &[(&str, &[&str])] = if cfg!(windows) { | |
| 445 | &[("clip", &[])] | |
| 446 | } else if cfg!(target_os = "macos") { | |
| 447 | &[("pbcopy", &[])] | |
| 448 | } else { | |
| 449 | &[("wl-copy", &[]), ("xclip", &["-selection", "clipboard"]), ("xsel", &["--clipboard", "--input"])] | |
| 450 | }; | |
| 451 | for (program, args) in tools { | |
| 452 | let Ok(mut child) = backend::quiet(program).args(*args).stdin(Stdio::piped()).stdout(Stdio::null()).stderr(Stdio::null()).spawn() else { continue }; | |
| 453 | if let Some(mut input) = child.stdin.take() { | |
| 454 | let _ = input.write_all(text.as_bytes()); | |
| 455 | } | |
| 456 | if child.wait().is_ok_and(|status| status.success()) { | |
| 457 | return Ok(()); | |
| 458 | } | |
| 459 | } | |
| 460 | Err(format!("It couldn't be copied. The command is: {text}")) | |
| 461 | } | |
| 462 | ||
| 463 | /// Windows' "Turn Windows features on or off", where Windows Sandbox and | |
| 464 | /// Virtual Machine Platform are. | |
| 465 | fn windows_features() -> Result<(), String> { | |
| 466 | if !cfg!(windows) { | |
| 467 | return Err("Windows features are only on Windows.".into()); | |
| 468 | } | |
| 469 | backend::quiet("optionalfeatures.exe").spawn().map(|_| ()).map_err(|e| format!("Windows features couldn't be opened: {e}")) | |
| 470 | } | |
| 471 | ||
| 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. | 472 | /// The workspace a token is wanted for now, if one is: the site asks as it loads. |
| 473 | pub fn wanted(app: &AppHandle) -> Option<String> { | |
| 474 | app.state::<Shared>().wanted.lock().unwrap().clone() | |
| 475 | } | |
| 476 | ||
| 477 | /// The runner's log, for the settings window to open. | |
| 478 | pub fn log_file(app: &AppHandle) -> PathBuf { | |
| 479 | folder(app).join("runner.log") | |
| 480 | } | |
| 481 | ||
| 482 | /// Stops the runner as g1t quits or restarts, and waits for it. | |
| 483 | pub fn shutdown(app: &AppHandle) { | |
| 484 | let tx = app.state::<Shared>().tx.lock().unwrap().take(); | |
| 485 | if let Some(tx) = tx { | |
| 486 | let (done, wait) = mpsc::channel(); | |
| 487 | if tx.send(Ask::Quit(done)).is_ok() { | |
| 488 | let _ = wait.recv_timeout(Duration::from_secs(30)); | |
| 489 | } | |
| 490 | } | |
| 491 | } | |
| 492 | ||
| 493 | /// How the runner is started this time. | |
| 494 | enum Process { | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 495 | /// `g1t-runner`, a child of the app, whose output is the runner's log. |
| 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. | 496 | Child(Child), |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 497 | /// The sandbox, which runs on its own once `up` is done; the launcher |
| 498 | /// command the app is waiting for, while there is one. | |
| 499 | Sandbox(Option<(Step, Child)>), | |
| 500 | } | |
| 501 | ||
| 502 | /// A launcher command that takes a while. | |
| 503 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| 504 | enum Step { | |
| 505 | Up, | |
| 506 | Update, | |
| 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. | 507 | } |
| 508 | ||
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 509 | impl Step { |
| 510 | fn name(self) -> &'static str { | |
| 511 | match self { | |
| 512 | Step::Up => "up", | |
| 513 | Step::Update => "update", | |
| 514 | } | |
| 515 | } | |
| 516 | } | |
| 517 | ||
| 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. | 518 | struct Run { |
| 519 | generation: u64, | |
| 520 | engine: Engine, | |
| 521 | backend: Backend, | |
| 522 | workspace: String, | |
| 523 | process: Process, | |
| 524 | /// Registered and polling. | |
| 525 | listening: bool, | |
| 526 | busy: bool, | |
| 527 | /// When a poll last failed to reach g1t. | |
| 528 | offline_at: Option<Instant>, | |
| 529 | /// Started with a registration token, which a container keeps in its configuration until replaced. | |
| 530 | with_token: bool, | |
| 531 | forgotten: bool, | |
| 532 | token_refused: bool, | |
| 533 | error: Option<String>, | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 534 | /// The status a launcher command is printing, line by line. |
| 535 | json: Option<Vec<String>>, | |
| 536 | /// Asking the sandbox how it is, and when it was last asked. | |
| 537 | polling: bool, | |
| 538 | polled_at: Option<Instant>, | |
| 539 | /// A poll asked before a launcher command ended, whose answer is old. | |
| 540 | stale: bool, | |
| 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. | 541 | } |
| 542 | ||
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 543 | impl Run { |
| 544 | fn new(generation: u64, engine: Engine, backend: Backend, workspace: String, process: Process, with_token: bool) -> Run { | |
| 545 | Run { | |
| 546 | generation, | |
| 547 | engine, | |
| 548 | backend, | |
| 549 | workspace, | |
| 550 | process, | |
| 551 | listening: false, | |
| 552 | busy: false, | |
| 553 | offline_at: None, | |
| 554 | with_token, | |
| 555 | forgotten: false, | |
| 556 | token_refused: false, | |
| 557 | error: None, | |
| 558 | json: None, | |
| 559 | polling: false, | |
| 560 | polled_at: None, | |
| 561 | stale: false, | |
| 562 | } | |
| 563 | } | |
| 564 | ||
| 565 | fn step(&self) -> Option<Step> { | |
| 566 | match &self.process { | |
| 567 | Process::Sandbox(Some((step, _))) => Some(*step), | |
| 568 | _ => None, | |
| 569 | } | |
| 570 | } | |
| 571 | } | |
| 572 | ||
| 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. | 573 | struct Keeper { |
| 574 | app: AppHandle, | |
| 575 | tx: Sender<Ask>, | |
| 576 | run: Option<Run>, | |
| 577 | generation: u64, | |
| 578 | token: Option<(String, String)>, | |
| 579 | not_before: Instant, | |
| 580 | failures: u32, | |
| 581 | next_update: Instant, | |
| 582 | log: Option<File>, | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 583 | /// The runner was updated: the sandbox takes its image before it starts again. |
| 584 | image_pending: bool, | |
| 585 | /// Why the last update failed, said until the next one works. | |
| 586 | update_failed: Option<String>, | |
| 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. | 587 | } |
| 588 | ||
| 589 | /// What a set of the state carries besides its code. | |
| 590 | #[derive(Default)] | |
| 591 | struct Extra { | |
| 592 | detail: Option<String>, | |
| 593 | exit_code: Option<i32>, | |
| 594 | wait_seconds: Option<u64>, | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 595 | fix: Option<String>, |
| 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. | 596 | } |
| 597 | ||
| 598 | fn detail(text: impl Into<String>) -> Extra { | |
| 599 | Extra { detail: Some(text.into()), ..Extra::default() } | |
| 600 | } | |
| 601 | ||
| 602 | impl Keeper { | |
| 603 | fn new(app: AppHandle, tx: Sender<Ask>) -> Keeper { | |
| 604 | let now = Instant::now(); | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 605 | Keeper { app, tx, run: None, generation: 0, token: None, not_before: now + AFTER_START, failures: 0, next_update: now + FIRST_UPDATE, log: None, image_pending: false, update_failed: None } |
| 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. | 606 | } |
| 607 | ||
| 608 | fn keep(mut self, rx: Receiver<Ask>) { | |
| 609 | self.reap(); | |
| 610 | self.set(Code::Starting, detail("Starting after g1t's window.")); | |
| 611 | loop { | |
| 612 | match rx.recv_timeout(TICK) { | |
| 613 | Ok(Ask::Quit(done)) => { | |
| 614 | self.halt(); | |
| 615 | let _ = done.send(()); | |
| 616 | return; | |
| 617 | } | |
| 618 | Ok(ask) => self.handle(ask), | |
| 619 | Err(RecvTimeoutError::Timeout) => {} | |
| 620 | Err(RecvTimeoutError::Disconnected) => { | |
| 621 | self.halt(); | |
| 622 | return; | |
| 623 | } | |
| 624 | } | |
| 625 | self.look(); | |
| 626 | } | |
| 627 | } | |
| 628 | ||
| 629 | fn set(&mut self, code: Code, extra: Extra) { | |
| 630 | let changed = { | |
| 631 | let shared = self.app.state::<Shared>(); | |
| 632 | let mut now = shared.now.lock().unwrap(); | |
| 633 | let job = if code == Code::Busy { now.job.clone() } else { None }; | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 634 | let next = Now { |
| 635 | code, | |
| 636 | detail: extra.detail, | |
| 637 | exit_code: extra.exit_code, | |
| 638 | wait_seconds: extra.wait_seconds, | |
| 639 | job, | |
| 640 | version: now.version.clone(), | |
| 641 | fix: extra.fix, | |
| 642 | sandbox: now.sandbox.clone(), | |
| 643 | }; | |
| 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. | 644 | let changed = *now != next; |
| 645 | *now = next; | |
| 646 | changed | |
| 647 | }; | |
| 648 | if changed { | |
| 649 | publish(&self.app); | |
| 650 | } | |
| 651 | } | |
| 652 | ||
| 653 | fn code(&self) -> Code { | |
| 654 | self.app.state::<Shared>().now.lock().unwrap().code | |
| 655 | } | |
| 656 | ||
| 657 | fn note(&mut self, job: Option<String>, version: Option<String>) { | |
| 658 | let shared = self.app.state::<Shared>(); | |
| 659 | let mut now = shared.now.lock().unwrap(); | |
| 660 | if job.is_some() { | |
| 661 | now.job = job; | |
| 662 | } | |
| 663 | if version.is_some() { | |
| 664 | now.version = version; | |
| 665 | } | |
| 666 | } | |
| 667 | ||
| 668 | /// A line of the runner's log, into the log file and the tail settings show. | |
| 669 | fn write_log(&mut self, line: &str) { | |
| 670 | { | |
| 671 | let shared = self.app.state::<Shared>(); | |
| 672 | let mut tail = shared.tail.lock().unwrap(); | |
| 673 | if tail.len() == TAIL { | |
| 674 | tail.pop_front(); | |
| 675 | } | |
| 676 | tail.push_back(line.to_owned()); | |
| 677 | } | |
| 678 | let path = log_file(&self.app); | |
| 679 | if self.log.is_none() { | |
| 680 | let _ = std::fs::create_dir_all(folder(&self.app)); | |
| 681 | if std::fs::metadata(&path).is_ok_and(|m| m.len() > LOG_LIMIT) { | |
| 682 | let _ = std::fs::rename(&path, path.with_extension("log.old")); | |
| 683 | } | |
| 684 | self.log = OpenOptions::new().create(true).append(true).open(&path).ok(); | |
| 685 | } | |
| 686 | if let Some(file) = self.log.as_mut() { | |
| 687 | let _ = writeln!(file, "{line}"); | |
| 688 | } | |
| 689 | } | |
| 690 | ||
| 691 | fn handle(&mut self, ask: Ask) { | |
| 692 | match ask { | |
| 693 | Ask::Look | Ask::Quit(_) => {} | |
| 694 | Ask::Retry => { | |
| 695 | self.not_before = Instant::now(); | |
| 696 | self.failures = 0; | |
| 697 | match self.code() { | |
| 698 | // The runner is fine: only the update is tried again. | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 699 | Code::UpdateFailed => { |
| 700 | self.next_update = Instant::now(); | |
| 701 | self.update_failed = None; | |
| 702 | } | |
| 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. | 703 | Code::Offline => self.halt(), |
| 704 | _ if self.run.as_ref().is_some_and(|run| !run.listening) => self.halt(), | |
| 705 | _ => {} | |
| 706 | } | |
| 707 | } | |
| 708 | Ask::Token { workspace, token } => { | |
| 709 | let shared = self.app.state::<Shared>(); | |
| 710 | let mut wanted = shared.wanted.lock().unwrap(); | |
| 711 | if wanted.as_deref() == Some(workspace.as_str()) { | |
| 712 | *wanted = None; | |
| 713 | self.token = Some((workspace, token)); | |
| 714 | self.not_before = Instant::now(); | |
| 715 | } | |
| 716 | } | |
| 717 | Ask::Refused { workspace, allowed, why } => { | |
| 718 | let matched = { | |
| 719 | let shared = self.app.state::<Shared>(); | |
| 720 | let mut wanted = shared.wanted.lock().unwrap(); | |
| 721 | let matched = wanted.as_deref() == Some(workspace.as_str()); | |
| 722 | if matched { | |
| 723 | *wanted = None; | |
| 724 | } | |
| 725 | matched | |
| 726 | }; | |
| 727 | if matched { | |
| 728 | self.not_before = Instant::now() + MOST_BACKOFF; | |
| 729 | self.set(if allowed { Code::RegisterFailed } else { Code::NotAllowed }, detail(why)); | |
| 730 | } | |
| 731 | } | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 732 | Ask::Line(generation, line, out) => self.line(generation, &line, out), |
| 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. | 733 | Ask::Exited(generation) => self.exited(generation), |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 734 | Ask::Sandbox(generation, status) => self.polled(generation, status), |
| 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. | 735 | } |
| 736 | } | |
| 737 | ||
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 738 | fn line(&mut self, generation: u64, line: &str, out: bool) { |
| 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. | 739 | let Some(run) = self.run.as_mut().filter(|run| run.generation == generation) else { return }; |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 740 | if run.engine == Engine::Launcher { |
| 741 | // The launcher's log, then its status, which is read when it ends. | |
| 742 | if out && (run.json.is_some() || launcher::starts_status(line)) { | |
| 743 | run.json.get_or_insert_with(Vec::new).push(line.to_owned()); | |
| 744 | } else { | |
| 745 | self.write_log(line); | |
| 746 | } | |
| 747 | return; | |
| 748 | } | |
| 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. | 749 | let mut became = None; |
| 750 | let mut job = None; | |
| 751 | let mut version = None; | |
| 752 | match log::read(line) { | |
| 753 | log::Event::Listening { version: v } => { | |
| 754 | run.listening = true; | |
| 755 | version = Some(v); | |
| 756 | became = Some(Code::Idle); | |
| 757 | } | |
| 758 | log::Event::Took { name, repo } => { | |
| 759 | run.busy = true; | |
| 760 | run.offline_at = None; | |
| 761 | job = Some(format!("{name} ({repo})")); | |
| 762 | became = Some(Code::Busy); | |
| 763 | } | |
| 764 | log::Event::Ended => { | |
| 765 | run.busy = false; | |
| 766 | became = Some(Code::Idle); | |
| 767 | } | |
| 768 | log::Event::Offline => { | |
| 769 | run.offline_at = Some(Instant::now()); | |
| 770 | if !run.busy { | |
| 771 | became = Some(Code::Offline); | |
| 772 | } | |
| 773 | } | |
| 774 | log::Event::Forgotten => run.forgotten = true, | |
| 775 | log::Event::TokenRefused => run.token_refused = true, | |
| 776 | log::Event::Failed(error) => run.error = Some(error), | |
| 777 | log::Event::Other => {} | |
| 778 | } | |
| 779 | let registered = version.is_some().then(|| Registration { | |
| 780 | server: config::server(&self.app).to_string(), | |
| 781 | workspace: run.workspace.clone(), | |
| 782 | backend: run.backend, | |
| 783 | }); | |
| 784 | self.write_log(line); | |
| 785 | if let Some(registration) = registered { | |
| 786 | registration.save(&self.app); | |
| 787 | self.failures = 0; | |
| 788 | } | |
| 789 | self.note(job, version); | |
| 790 | if let Some(code) = became { | |
| 791 | self.set(code, Extra::default()); | |
| 792 | } | |
| 793 | } | |
| 794 | ||
| 795 | fn exited(&mut self, generation: u64) { | |
| 796 | if self.run.as_ref().is_none_or(|run| run.generation != generation) { | |
| 797 | return; | |
| 798 | } | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 799 | if self.run.as_ref().is_some_and(|run| run.engine == Engine::Launcher) { |
| 800 | return self.launched(generation); | |
| 801 | } | |
| 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. | 802 | let mut run = self.run.take().expect("checked above"); |
| 803 | let exit_code = match &mut run.process { | |
| 804 | Process::Child(child) => child.wait().ok().and_then(|status| status.code()), | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 805 | Process::Sandbox(_) => None, |
| 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. | 806 | }; |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 807 | self.stop(&mut run, Down::Now); |
| 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. | 808 | if run.forgotten || run.token_refused { |
| 809 | // The registration is no more, or was never made: the next start asks for a token. | |
| 810 | self.forget(); | |
| 811 | self.not_before = Instant::now(); | |
| 812 | return; | |
| 813 | } | |
| 814 | self.failures += 1; | |
| 815 | let wait = Duration::from_secs(15 * 2u64.pow(self.failures.min(6))).min(MOST_BACKOFF); | |
| 816 | self.not_before = Instant::now() + wait; | |
| 817 | let offline = run.error.as_deref().is_some_and(|e| e.contains("could not reach g1t")); | |
| 818 | if offline { | |
| 819 | self.set(Code::Offline, Extra::default()); | |
| 820 | } else if !run.listening && Registration::load(&self.app).is_none() { | |
| 821 | self.set(Code::RegisterFailed, Extra { detail: run.error, ..Extra::default() }); | |
| 822 | } else { | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 823 | self.set(Code::Crashed, Extra { detail: run.error, exit_code, wait_seconds: Some(wait.as_secs()), ..Extra::default() }); |
| 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. | 824 | } |
| 825 | } | |
| 826 | ||
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 827 | /// Stops a run's process, the way it was started: the sandbox `how` says. |
| 828 | fn stop(&mut self, run: &mut Run, how: Down) { | |
| 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. | 829 | match (&mut run.process, run.engine) { |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 830 | (Process::Sandbox(step), _) => { |
| 831 | // A launcher command still going is ended first, then the sandbox stopped. | |
| 832 | if let Some((_, child)) = step.as_mut() { | |
| 833 | native::stop(child); | |
| 834 | } | |
| 835 | let binary = native::binary(&folder(&self.app)); | |
| 836 | if let Err(why) = launcher::down(&binary, &native_dir(&self.app, Backend::Sandboxed), how) { | |
| 837 | self.write_log(&format!("g1t: the sandbox didn't stop cleanly: {why}")); | |
| 838 | } | |
| 839 | self.app.state::<Shared>().now.lock().unwrap().sandbox = None; | |
| 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. | 840 | } |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 841 | (Process::Child(child), Engine::Launcher) => native::stop(child), |
| 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. | 842 | (Process::Child(child), Engine::Native { docker }) => { |
| 843 | native::stop(child); | |
| 844 | // A job's container outlives the runner killed under it: removed now, not at its next start. | |
| 845 | if docker { | |
| 846 | native::sweep(&native_dir(&self.app, run.backend)); | |
| 847 | } | |
| 848 | } | |
| 849 | } | |
| 850 | let _ = std::fs::remove_file(folder(&self.app).join("pid")); | |
| 851 | } | |
| 852 | ||
| 853 | /// Stops whatever runs now. | |
| 854 | fn halt(&mut self) { | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 855 | self.halt_as(Down::Now); |
| 856 | } | |
| 857 | ||
| 858 | /// Stops whatever runs: the sandbox now, after its job, or for good. | |
| 859 | fn halt_as(&mut self, how: Down) { | |
| 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. | 860 | self.generation += 1; |
| 861 | if let Some(mut run) = self.run.take() { | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 862 | self.stop(&mut run, how); |
| 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. | 863 | } |
| 864 | } | |
| 865 | ||
| 866 | /// A runner an earlier g1t started and did not stop: it ended without quitting. | |
| 867 | fn reap(&self) { | |
| 868 | if let Some(pid) = std::fs::read_to_string(folder(&self.app).join("pid")).ok().and_then(|text| text.trim().parse().ok()) { | |
| 869 | native::reap(pid); | |
| 870 | let _ = std::fs::remove_file(folder(&self.app).join("pid")); | |
| 871 | } | |
| 872 | } | |
| 873 | ||
| 874 | /// Unregisters the runner the app registered, whatever backend it was. | |
| 875 | fn forget(&mut self) { | |
| 876 | let app = self.app.clone(); | |
| 877 | let Some(registration) = Registration::load(&app) else { return }; | |
| 878 | let binary = native::binary(&folder(&app)); | |
| 879 | match registration.backend { | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 880 | // The sandbox keeps its registration for when it is chosen |
| 881 | // again (sandbox-registration.json says whose): it only stops, | |
| 882 | // after its job. | |
| 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. | 883 | Backend::Sandboxed => { |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 884 | let _ = launcher::down(&binary, &native_dir(&app, Backend::Sandboxed), Down::Drain); |
| 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. | 885 | } |
| 886 | backend => native::forget(&binary, &native_dir(&app, backend)), | |
| 887 | } | |
| 888 | Registration::clear(&app); | |
| 889 | } | |
| 890 | ||
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 891 | /// Removes the sandbox and its registration: it was for another |
| 892 | /// workspace, or g1t no longer knows its runners. | |
| 893 | fn forget_sandbox(&mut self) { | |
| 894 | let binary = native::binary(&folder(&self.app)); | |
| 895 | if let Err(why) = launcher::down(&binary, &native_dir(&self.app, Backend::Sandboxed), Down::Delete) { | |
| 896 | self.write_log(&format!("g1t: the sandbox couldn't be removed: {why}")); | |
| 897 | } | |
| 898 | SandboxRegistration::clear(&self.app); | |
| 899 | if Registration::load(&self.app).is_some_and(|r| r.backend == Backend::Sandboxed) { | |
| 900 | Registration::clear(&self.app); | |
| 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. | 901 | } |
| 902 | } | |
| 903 | ||
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 904 | /// Whether what a native runner keeps of a registration is still here. |
| 905 | fn kept(&self, backend: Backend) -> bool { | |
| 906 | native_dir(&self.app, backend).join("config.json").exists() | |
| 907 | } | |
| 908 | ||
| 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. | 909 | /// Asks the site for a registration token for `workspace`. |
| 910 | fn want(&mut self, workspace: &str) { | |
| 911 | *self.app.state::<Shared>().wanted.lock().unwrap() = Some(workspace.to_owned()); | |
| 912 | let _ = self.app.emit_to(EventTarget::webview(frame::SITE), WANTED_EVENT, workspace.to_owned()); | |
| 913 | self.not_before = Instant::now() + ASK_AGAIN; | |
| 914 | let signed_out = self.app.state::<Shared>().site.lock().unwrap().signed_in == Some(false); | |
| 915 | self.set(if signed_out { Code::SignedOut } else { Code::Registering }, Extra::default()); | |
| 916 | } | |
| 917 | ||
| 918 | /// The thread's look round: what should run, and whether it does. | |
| 919 | fn look(&mut self) { | |
| 920 | let settings = config::settings(&self.app); | |
| 921 | let paused = *self.app.state::<Shared>().paused.lock().unwrap(); | |
| 922 | if !settings.runner || paused { | |
| 923 | if self.run.is_some() { | |
| 924 | self.halt(); | |
| 925 | } | |
| 926 | *self.app.state::<Shared>().wanted.lock().unwrap() = None; | |
| 927 | self.set(if paused { Code::Stopped } else { Code::Off }, Extra::default()); | |
| 928 | return; | |
| 929 | } | |
| 930 | let workspace = workspace(&self.app); | |
| 931 | if let Some(run) = &self.run { | |
| 932 | let moved = run.backend != settings.runner_backend || Some(&run.workspace) != workspace.as_ref(); | |
| 933 | let back = run.offline_at.is_some_and(|at| at.elapsed() >= BACK_ONLINE); | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 934 | let (busy, listening, stepping) = (run.busy, run.listening, run.step().is_some()); |
| 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. | 935 | if back { |
| 936 | if let Some(run) = self.run.as_mut() { | |
| 937 | run.offline_at = None; | |
| 938 | } | |
| 939 | self.set(Code::Idle, Extra::default()); | |
| 940 | } | |
| 941 | // A change of backend or workspace waits for the job to end, as an update does. | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 942 | if moved && !busy && !stepping { |
| 943 | self.halt_as(Down::Drain); | |
| 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. | 944 | self.not_before = Instant::now(); |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 945 | } else if !busy && listening && !stepping && Instant::now() >= self.next_update { |
| 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. | 946 | self.update(); |
| 947 | } | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 948 | self.poll(); |
| 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. | 949 | return; |
| 950 | } | |
| 951 | if Instant::now() < self.not_before { | |
| 952 | return; | |
| 953 | } | |
| 954 | self.start(settings.runner_backend, workspace); | |
| 955 | } | |
| 956 | ||
| 957 | fn start(&mut self, backend: Backend, workspace: Option<String>) { | |
| 958 | let app = self.app.clone(); | |
| 959 | let engine = match backend::engine(backend) { | |
| 960 | Ok(engine) => engine, | |
| 961 | Err(missing) => { | |
| 962 | self.not_before = Instant::now() + LOOK_AGAIN; | |
| 963 | let code = match missing { | |
| 964 | Missing::Docker => Code::NoDocker, | |
| 965 | }; | |
| 966 | self.set(code, Extra::default()); | |
| 967 | return; | |
| 968 | } | |
| 969 | }; | |
| 970 | let Some(workspace) = workspace else { | |
| 971 | self.not_before = Instant::now() + LOOK_AGAIN; | |
| 972 | let code = match app.state::<Shared>().site.lock().unwrap().signed_in { | |
| 973 | Some(false) => Code::SignedOut, | |
| 974 | Some(true) => Code::NoWorkspace, | |
| 975 | None => Code::Starting, | |
| 976 | }; | |
| 977 | self.set(code, detail("Waiting for g1t to open.")); | |
| 978 | return; | |
| 979 | }; | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 980 | if engine == Engine::Launcher { |
| 981 | return self.start_sandbox(workspace); | |
| 982 | } | |
| 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. | 983 | let server = config::server(&app); |
| 984 | let registration = Registration::load(&app); | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 985 | let ours = registration.as_ref().is_some_and(|r| r.server == server.as_str() && r.workspace == workspace && r.backend == backend) && self.kept(backend); |
| 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. | 986 | let mut token = None; |
| 987 | if !ours { | |
| 988 | if registration.is_some() { | |
| 989 | self.set(Code::Starting, detail("Removing this computer's earlier registration.")); | |
| 990 | self.forget(); | |
| 991 | } | |
| 992 | match self.token.take() { | |
| 993 | Some((for_workspace, given)) if for_workspace == workspace => token = Some(given), | |
| 994 | _ => return self.want(&workspace), | |
| 995 | } | |
| 996 | } | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 997 | self.hold_workspace(&workspace); |
| 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. | 998 | self.set(Code::Starting, Extra::default()); |
| 999 | let envs = environment(&server, backend, token.as_deref()); | |
| 1000 | let generation = self.generation + 1; | |
| 1001 | self.generation = generation; | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 1002 | let started = self.ensure_binary(&server).and_then(|binary| { |
| 1003 | let child = native::spawn(&binary, &native_dir(&app, backend), &envs).map_err(|e| format!("g1t-runner could not start: {e}"))?; | |
| 1004 | let _ = std::fs::write(folder(&app).join("pid"), child.id().to_string()); | |
| 1005 | Ok(child) | |
| 1006 | }); | |
| 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. | 1007 | match started { |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 1008 | Ok(mut child) => { |
| 1009 | self.follow(generation, &mut child); | |
| 1010 | self.run = Some(Run::new(generation, engine, backend, workspace, Process::Child(child), token.is_some())); | |
| 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. | 1011 | self.set(Code::Starting, Extra::default()); |
| 1012 | } | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 1013 | Err(why) => self.not_started(workspace, token, why), |
| 1014 | } | |
| 1015 | } | |
| 1016 | ||
| 1017 | /// The runner couldn't be started: the token is kept for the next try, | |
| 1018 | /// while it lasts, and it is tried again in a while. | |
| 1019 | fn not_started(&mut self, workspace: String, token: Option<String>, why: String) { | |
| 1020 | if let Some(given) = token { | |
| 1021 | self.token = Some((workspace, given)); | |
| 1022 | } | |
| 1023 | self.failures += 1; | |
| 1024 | self.not_before = Instant::now() + LOOK_AGAIN; | |
| 1025 | let offline = why.contains("error sending request") || why.contains("dns") || why.contains("connect"); | |
| 1026 | self.write_log(&format!("g1t: the runner could not start: {why}")); | |
| 1027 | if offline { | |
| 1028 | self.set(Code::Offline, Extra::default()); | |
| 1029 | } else { | |
| 1030 | self.set(Code::Crashed, Extra { detail: Some(why), wait_seconds: Some(LOOK_AGAIN.as_secs()), ..Extra::default() }); | |
| 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. | 1031 | } |
| 1032 | } | |
| 1033 | ||
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 1034 | /// The setting names the workspace from now on, so browsing another one never moves the runner. |
| 1035 | fn hold_workspace(&self, workspace: &str) { | |
| 1036 | if config::settings(&self.app).runner_workspace.as_deref() != Some(workspace) { | |
| 1037 | let mut settings = config::settings(&self.app); | |
| 1038 | settings.runner_workspace = Some(workspace.to_owned()); | |
| 1039 | let _ = config::set_settings(&self.app, settings); | |
| 1040 | } | |
| 1041 | } | |
| 1042 | ||
| 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. | 1043 | /// Reads the child's output, a line at a time, onto the thread's channel. |
| 1044 | fn follow(&self, generation: u64, child: &mut Child) { | |
| 1045 | fn pipe(tx: Sender<Ask>, generation: u64, from: impl Read + Send + 'static, last: bool) { | |
| 1046 | std::thread::spawn(move || { | |
| 1047 | for line in BufReader::new(from).lines().map_while(Result::ok) { | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 1048 | if tx.send(Ask::Line(generation, line, last)).is_err() { |
| 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. | 1049 | return; |
| 1050 | } | |
| 1051 | } | |
| 1052 | if last { | |
| 1053 | let _ = tx.send(Ask::Exited(generation)); | |
| 1054 | } | |
| 1055 | }); | |
| 1056 | } | |
| 1057 | if let Some(err) = child.stderr.take() { | |
| 1058 | pipe(self.tx.clone(), generation, err, false); | |
| 1059 | } | |
| 1060 | if let Some(out) = child.stdout.take() { | |
| 1061 | pipe(self.tx.clone(), generation, out, true); | |
| 1062 | } | |
| 1063 | } | |
| 1064 | ||
| 1065 | /// `g1t-runner` installed, from the newest release when it is not. | |
| 1066 | fn ensure_binary(&mut self, server: &Url) -> Result<PathBuf, String> { | |
| 1067 | let folder = folder(&self.app); | |
| 1068 | let binary = native::binary(&folder); | |
| 1069 | if native::installed(&folder).is_some() { | |
| 1070 | return Ok(binary); | |
| 1071 | } | |
| 1072 | self.set(Code::Updating, detail("Downloading g1t-runner.")); | |
| 1073 | let manifest = native::latest(server)?; | |
| 1074 | let bytes = native::download(server, &manifest)?; | |
| 1075 | native::put(&folder, &bytes, &manifest.version)?; | |
| 1076 | self.note(None, Some(manifest.version)); | |
| 1077 | Ok(binary) | |
| 1078 | } | |
| 1079 | ||
| 1080 | /// A newer release or image, put in place while the runner is idle. | |
| 1081 | /// The new one is downloaded and checked before the runner stops, so | |
| 1082 | /// one that fails leaves it running as it was. | |
| 1083 | fn update(&mut self) { | |
| 1084 | self.next_update = Instant::now() + UPDATE_EVERY; | |
| 1085 | if !config::settings(&self.app).auto_update { | |
| 1086 | return; | |
| 1087 | } | |
| 1088 | let Some(engine) = self.run.as_ref().map(|run| run.engine) else { return }; | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 1089 | let server = config::server(&self.app); |
| 1090 | let folder = folder(&self.app); | |
| 1091 | let release = match self.newer_release(&server) { | |
| 1092 | Ok(release) => release, | |
| 1093 | Err(why) => { | |
| 1094 | let why = format!("g1t-runner couldn't be updated: {why}"); | |
| 1095 | self.write_log(&format!("g1t: {why}")); | |
| 1096 | self.update_failed = Some(why.clone()); | |
| 1097 | return self.set(Code::UpdateFailed, detail(why)); | |
| 1098 | } | |
| 1099 | }; | |
| 1100 | let Some((version, bytes)) = release else { | |
| 1101 | // The program is the newest; the sandbox's image may not be | |
| 1102 | // (`update` does nothing when it is). | |
| 1103 | if engine == Engine::Launcher { | |
| 1104 | self.sandbox_step(Step::Update); | |
| 1105 | } | |
| 1106 | return; | |
| 1107 | }; | |
| 1108 | self.set(Code::Updating, detail(format!("Updating g1t-runner to {version}."))); | |
| 1109 | // The sandbox's runner for Windows jobs is this program too: it | |
| 1110 | // stops, after its job, before the program is replaced. | |
| 1111 | self.halt_as(Down::Drain); | |
| 1112 | match native::put(&folder, &bytes, &version) { | |
| 1113 | Ok(()) => { | |
| 1114 | self.note(None, Some(version)); | |
| 1115 | self.update_failed = None; | |
| 1116 | // The sandbox's image comes with each release. | |
| 1117 | self.image_pending = engine == Engine::Launcher; | |
| 1118 | } | |
| 1119 | Err(why) => self.write_log(&format!("g1t: g1t-runner {version} couldn't be put in place: {why}")), | |
| 1120 | } | |
| 1121 | self.not_before = Instant::now(); | |
| 1122 | } | |
| 1123 | ||
| 1124 | /// A release newer than the `g1t-runner` here, downloaded and checked | |
| 1125 | /// against its SHA-256: its version and program. None when this one is | |
| 1126 | /// the newest, or the releases can't be asked now. | |
| 1127 | fn newer_release(&self, server: &Url) -> Result<Option<(String, Vec<u8>)>, String> { | |
| 1128 | let Ok(manifest) = native::latest(server) else { return Ok(None) }; | |
| 1129 | if !native::installed(&folder(&self.app)).is_none_or(|have| native::newer(&manifest.version, &have)) { | |
| 1130 | return Ok(None); | |
| 1131 | } | |
| 1132 | let bytes = native::download(server, &manifest).map_err(|why| format!("{} didn't download: {why}", manifest.version))?; | |
| 1133 | Ok(Some((manifest.version, bytes))) | |
| 1134 | } | |
| 1135 | ||
| 1136 | // --- The sandbox (launcher.rs) ------------------------------------------------ | |
| 1137 | ||
| 1138 | /// Starts the sandbox for `workspace`: registered with it the first | |
| 1139 | /// time, with a token from the site; from its own registration after. | |
| 1140 | fn start_sandbox(&mut self, workspace: String) { | |
| 1141 | let app = self.app.clone(); | |
| 1142 | let server = config::server(&app); | |
| 1143 | let binary = match self.ensure_binary(&server) { | |
| 1144 | Ok(binary) => binary, | |
| 1145 | Err(why) => return self.not_started(workspace, None, why), | |
| 1146 | }; | |
| 1147 | let dir = native_dir(&app, Backend::Sandboxed); | |
| 1148 | let mut status = match launcher::status(&binary, &dir) { | |
| 1149 | Ok(status) => status, | |
| 1150 | Err(why) => return self.no_status(why), | |
| 1151 | }; | |
| 1152 | self.seen(&status); | |
| 1153 | // Nothing here can be sandboxed: why, and how to fix it; looked at again in a while. | |
| 1154 | if !status.anything_available() { | |
| 1155 | self.not_before = Instant::now() + LOOK_AGAIN; | |
| 1156 | let (code, extra) = shown(&status); | |
| 1157 | return self.set(code, extra); | |
| 1158 | } | |
| 1159 | // Another backend's registration goes; the sandbox's own is kept for its workspace. | |
| 1160 | if Registration::load(&app).is_some_and(|r| r.backend != Backend::Sandboxed) { | |
| 1161 | self.set(Code::Starting, detail("Removing this computer's earlier registration.")); | |
| 1162 | self.forget(); | |
| 1163 | } | |
| 1164 | let kept = SandboxRegistration::load(&app); | |
| 1165 | let ours = kept.as_ref().is_some_and(|k| k.server == server.as_str() && k.workspace == workspace); | |
| 1166 | let made = status.capabilities.iter().any(|c| c.state != State::Absent) || status.state != State::Absent; | |
| 1167 | if !ours && made && kept.is_none() { | |
| 1168 | // Made by hand (`g1t-runner sandbox up`), and there is one per computer: it is the person's to remove. | |
| 1169 | self.not_before = Instant::now() + LOOK_AGAIN; | |
| 1170 | return self.set(Code::SandboxTaken, Extra::default()); | |
| 1171 | } | |
| 1172 | if !ours && made { | |
| 1173 | // The app's sandbox, registered with another workspace or server. | |
| 1174 | self.set(Code::Starting, detail("Removing this computer's sandbox for another workspace.")); | |
| 1175 | self.forget_sandbox(); | |
| 1176 | status = match launcher::status(&binary, &dir) { | |
| 1177 | Ok(status) => status, | |
| 1178 | Err(why) => return self.no_status(why), | |
| 1179 | }; | |
| 1180 | } | |
| 1181 | let mut token = None; | |
| 1182 | if !ours || !kept.as_ref().is_some_and(|k| k.registered) || status.unregistered() { | |
| 1183 | match self.token.take() { | |
| 1184 | Some((for_workspace, given)) if for_workspace == workspace => token = Some(given), | |
| 1185 | _ => return self.want(&workspace), | |
| 1186 | } | |
| 1187 | SandboxRegistration { server: server.to_string(), workspace: workspace.clone(), registered: false }.save(&app); | |
| 1188 | } | |
| 1189 | self.hold_workspace(&workspace); | |
| 1190 | let generation = self.generation + 1; | |
| 1191 | self.generation = generation; | |
| 1192 | self.run = Some(Run::new(generation, Engine::Launcher, Backend::Sandboxed, workspace.clone(), Process::Sandbox(None), token.is_some())); | |
| 1193 | // A new release's image first, then the sandbox starts again. | |
| 1194 | let step = if self.image_pending && status.linux().state != State::Absent { Step::Update } else { Step::Up }; | |
| 1195 | self.image_pending = false; | |
| 1196 | let name = backend::machine_name(); | |
| 1197 | let url = server.as_str().trim_end_matches('/').to_owned(); | |
| 1198 | let up = launcher::Up { url: &url, name: &name, labels: "desktop", token: token.as_deref() }; | |
| 1199 | let args = match step { | |
| 1200 | Step::Up => launcher::up_args(&dir, &up), | |
| 1201 | Step::Update => launcher::update_args(&dir), | |
| 1202 | }; | |
| A registration token never shows in the process list. The desktop app passed it to g1t-runner sandbox up as --token, which anyone on the computer can read while up runs; it goes in up's environment now, as G1T_RUNNER_TOKEN, which the launcher reads when --token is left out, as register-and-run already did. | 1203 | if let Err(why) = self.spawn_step(step, args, up.token) { |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 1204 | self.run = None; |
| 1205 | return self.not_started(workspace, token, why); | |
| 1206 | } | |
| 1207 | self.set(Code::Starting, detail(STARTING_SANDBOX)); | |
| 1208 | } | |
| 1209 | ||
| 1210 | /// `sandbox status` didn't answer: a `g1t-runner` from before its | |
| 1211 | /// launcher, which a newer release replaces, or one that failed. | |
| 1212 | fn no_status(&mut self, why: String) { | |
| 1213 | self.write_log(&format!("g1t: the sandbox's status: {why}")); | |
| 1214 | self.failures += 1; | |
| 1215 | if why.contains("older than its sandbox launcher") { | |
| 1216 | let server = config::server(&self.app); | |
| 1217 | if let Ok(Some((version, bytes))) = self.newer_release(&server) | |
| 1218 | && native::put(&folder(&self.app), &bytes, &version).is_ok() | |
| 1219 | { | |
| 1220 | self.note(None, Some(version)); | |
| 1221 | self.not_before = Instant::now(); | |
| 1222 | return; | |
| 1223 | } | |
| 1224 | self.not_before = Instant::now() + MOST_BACKOFF; | |
| 1225 | return self.set(Code::NoLauncher, detail(why)); | |
| 1226 | } | |
| 1227 | self.not_before = Instant::now() + LOOK_AGAIN; | |
| 1228 | self.set(Code::SandboxFailed, Extra { detail: Some(why), wait_seconds: Some(LOOK_AGAIN.as_secs()), ..Extra::default() }); | |
| 1229 | } | |
| 1230 | ||
| 1231 | /// Runs a launcher command for the sandbox's run, reading its output. | |
| A registration token never shows in the process list. The desktop app passed it to g1t-runner sandbox up as --token, which anyone on the computer can read while up runs; it goes in up's environment now, as G1T_RUNNER_TOKEN, which the launcher reads when --token is left out, as register-and-run already did. | 1232 | fn spawn_step(&mut self, step: Step, args: Vec<std::ffi::OsString>, token: Option<&str>) -> Result<(), String> { |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 1233 | let binary = native::binary(&folder(&self.app)); |
| A registration token never shows in the process list. The desktop app passed it to g1t-runner sandbox up as --token, which anyone on the computer can read while up runs; it goes in up's environment now, as G1T_RUNNER_TOKEN, which the launcher reads when --token is left out, as register-and-run already did. | 1234 | let mut command = launcher::command(&binary, args); |
| 1235 | launcher::with_token(&mut command, token); | |
| 1236 | let mut child = command.spawn().map_err(|e| format!("g1t-runner sandbox {} could not start: {e}", step.name()))?; | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 1237 | let _ = std::fs::write(folder(&self.app).join("pid"), child.id().to_string()); |
| 1238 | let Some(generation) = self.run.as_ref().map(|run| run.generation) else { | |
| 1239 | native::stop(&mut child); | |
| 1240 | return Ok(()); | |
| 1241 | }; | |
| 1242 | self.follow(generation, &mut child); | |
| 1243 | let run = self.run.as_mut().expect("checked above"); | |
| 1244 | run.json = None; | |
| 1245 | run.process = Process::Sandbox(Some((step, child))); | |
| 1246 | Ok(()) | |
| 1247 | } | |
| 1248 | ||
| 1249 | /// `update` on the running sandbox, while it has no job. | |
| 1250 | fn sandbox_step(&mut self, step: Step) { | |
| 1251 | if self.run.as_ref().is_none_or(|run| run.step().is_some()) { | |
| 1252 | return; | |
| 1253 | } | |
| 1254 | let dir = native_dir(&self.app, Backend::Sandboxed); | |
| A registration token never shows in the process list. The desktop app passed it to g1t-runner sandbox up as --token, which anyone on the computer can read while up runs; it goes in up's environment now, as G1T_RUNNER_TOKEN, which the launcher reads when --token is left out, as register-and-run already did. | 1255 | if let Err(why) = self.spawn_step(step, launcher::update_args(&dir), None) { |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 1256 | self.write_log(&format!("g1t: {why}")); |
| 1257 | } | |
| 1258 | } | |
| 1259 | ||
| 1260 | /// Asks the sandbox how it is, every few seconds, on a thread of its own. | |
| 1261 | fn poll(&mut self) { | |
| 1262 | let Some(run) = self.run.as_mut().filter(|run| run.engine == Engine::Launcher) else { return }; | |
| 1263 | if run.polling || run.polled_at.is_some_and(|at| at.elapsed() < launcher::STATUS_EVERY) { | |
| 1264 | return; | |
| 1265 | } | |
| 1266 | run.polling = true; | |
| 1267 | run.polled_at = Some(Instant::now()); | |
| 1268 | let (tx, generation) = (self.tx.clone(), run.generation); | |
| 1269 | let binary = native::binary(&folder(&self.app)); | |
| 1270 | let dir = native_dir(&self.app, Backend::Sandboxed); | |
| 1271 | let spawned = std::thread::Builder::new().name("g1t-sandbox-status".into()).spawn(move || { | |
| 1272 | let _ = tx.send(Ask::Sandbox(generation, launcher::status(&binary, &dir).map(Box::new))); | |
| 1273 | }); | |
| 1274 | if spawned.is_err() { | |
| 1275 | run.polling = false; | |
| 1276 | } | |
| 1277 | } | |
| 1278 | ||
| 1279 | /// What a poll said. | |
| 1280 | fn polled(&mut self, generation: u64, status: Result<Box<Status>, String>) { | |
| 1281 | let Some(run) = self.run.as_mut().filter(|run| run.generation == generation) else { return }; | |
| 1282 | run.polling = false; | |
| 1283 | if std::mem::take(&mut run.stale) { | |
| 1284 | return; | |
| 1285 | } | |
| 1286 | match status { | |
| 1287 | Ok(status) => self.sandbox_now(*status), | |
| 1288 | // Once is nothing: the next poll asks again. | |
| 1289 | Err(why) => self.write_log(&format!("g1t: the sandbox's status: {why}")), | |
| 1290 | } | |
| 1291 | } | |
| 1292 | ||
| 1293 | /// Keeps what a status says for the tray, settings and the site. | |
| 1294 | fn seen(&mut self, status: &Status) { | |
| 1295 | let shared = self.app.state::<Shared>(); | |
| 1296 | let mut now = shared.now.lock().unwrap(); | |
| 1297 | now.sandbox = Some(status.clone()); | |
| 1298 | if let Some(version) = status.version() { | |
| 1299 | now.version = Some(version); | |
| 1300 | } | |
| 1301 | now.job = status.job().map(launcher::Job::said); | |
| 1302 | } | |
| 1303 | ||
| 1304 | /// The sandbox as it is now, while it is the run. | |
| 1305 | fn sandbox_now(&mut self, status: Status) { | |
| 1306 | let had = self.app.state::<Shared>().now.lock().unwrap().job.clone(); | |
| 1307 | let Some(run) = self.run.as_mut() else { return }; | |
| 1308 | let step = run.step(); | |
| 1309 | let job = status.job().map(launcher::Job::said); | |
| 1310 | let was = had.filter(|_| run.busy); | |
| 1311 | run.busy = job.is_some(); | |
| 1312 | run.listening = status.running(); | |
| 1313 | let update_failed = self.update_failed.clone(); | |
| 1314 | if was != job { | |
| 1315 | if let Some(ended) = &was { | |
| 1316 | self.write_log(&format!("g1t: {ended} ended.")); | |
| 1317 | } | |
| 1318 | if let Some(took) = &job { | |
| 1319 | self.write_log(&format!("g1t: Took {took}.")); | |
| 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. | 1320 | } |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 1321 | } |
| 1322 | self.seen(&status); | |
| 1323 | match step { | |
| 1324 | Some(Step::Up) => return self.set(Code::Starting, detail(STARTING_SANDBOX)), | |
| 1325 | Some(Step::Update) if status.state == State::Updating => return self.set(Code::Updating, detail("Updating the sandbox to the newest image.")), | |
| 1326 | _ => {} | |
| 1327 | } | |
| 1328 | let (code, extra) = shown(&status); | |
| 1329 | if step.is_none() && !status.running() && !matches!(status.state, State::Starting | State::Updating) { | |
| 1330 | return self.sandbox_stopped(&status, code, extra); | |
| 1331 | } | |
| 1332 | match (code, update_failed) { | |
| 1333 | (Code::Idle, Some(why)) => self.set(Code::UpdateFailed, detail(why)), | |
| 1334 | (code, _) => self.set(code, extra), | |
| 1335 | } | |
| 1336 | } | |
| 1337 | ||
| 1338 | /// The sandbox stopped by itself, or never got going: started again, | |
| 1339 | /// waiting longer each time, or registered again when that is why. | |
| 1340 | fn sandbox_stopped(&mut self, status: &Status, code: Code, extra: Extra) { | |
| 1341 | let error = status.error.clone().or_else(|| extra.detail.clone()).unwrap_or_default(); | |
| 1342 | if self.registration_trouble(&error) { | |
| 1343 | return; | |
| 1344 | } | |
| 1345 | self.run = None; | |
| 1346 | self.generation += 1; | |
| 1347 | self.failures += 1; | |
| 1348 | let wait = Duration::from_secs(15 * 2u64.pow(self.failures.min(6))).min(MOST_BACKOFF); | |
| 1349 | self.not_before = Instant::now() + wait; | |
| 1350 | let (code, extra) = match code { | |
| 1351 | Code::Idle | Code::Busy | Code::Starting | Code::Updating => (Code::SandboxFailed, Extra { detail: status.error.clone().or_else(|| some_text("The sandbox stopped by itself.")), ..Extra::default() }), | |
| 1352 | code => (code, extra), | |
| 1353 | }; | |
| 1354 | self.set(code, Extra { wait_seconds: Some(wait.as_secs()), ..extra }); | |
| 1355 | } | |
| 1356 | ||
| 1357 | /// When the sandbox's trouble is its registration: true when the app | |
| 1358 | /// has seen to it, and the next start registers again. | |
| 1359 | fn registration_trouble(&mut self, error: &str) -> bool { | |
| 1360 | match launcher::why(error) { | |
| 1361 | launcher::Why::TokenRefused | launcher::Why::Unregistered => { | |
| 1362 | if let Some(mut kept) = SandboxRegistration::load(&self.app) { | |
| 1363 | kept.registered = false; | |
| 1364 | kept.save(&self.app); | |
| 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. | 1365 | } |
| 1366 | self.halt(); | |
| 1367 | self.not_before = Instant::now(); | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 1368 | true |
| 1369 | } | |
| 1370 | launcher::Why::Forgotten => { | |
| 1371 | self.halt(); | |
| 1372 | self.forget_sandbox(); | |
| 1373 | self.not_before = Instant::now(); | |
| 1374 | true | |
| 1375 | } | |
| 1376 | _ => false, | |
| 1377 | } | |
| 1378 | } | |
| 1379 | ||
| 1380 | /// A launcher command ended: what it printed last is the sandbox's status. | |
| 1381 | fn launched(&mut self, generation: u64) { | |
| 1382 | let Some(run) = self.run.as_mut().filter(|run| run.generation == generation) else { return }; | |
| 1383 | let Process::Sandbox(slot) = &mut run.process else { return }; | |
| 1384 | let Some((step, mut child)) = slot.take() else { return }; | |
| 1385 | let worked = child.wait().is_ok_and(|status| status.success()); | |
| 1386 | let _ = std::fs::remove_file(folder(&self.app).join("pid")); | |
| 1387 | let printed = run.json.take().unwrap_or_default().join("\n"); | |
| 1388 | run.stale = run.polling; | |
| 1389 | let with_token = run.with_token; | |
| 1390 | let status = launcher::parse(&printed); | |
| 1391 | let error = match (&status, worked) { | |
| 1392 | (Ok(status), true) => status.error.clone().filter(|_| !status.running()), | |
| 1393 | (Ok(status), false) => Some(status.error.clone().unwrap_or_else(|| format!("g1t-runner sandbox {} failed.", step.name()))), | |
| 1394 | (Err(why), _) => Some(why.clone()), | |
| 1395 | }; | |
| 1396 | let status = status.ok(); | |
| 1397 | let running = status.as_ref().is_some_and(Status::running); | |
| 1398 | if let Some(error) = &error { | |
| 1399 | self.write_log(&format!("g1t: sandbox {}: {error}", step.name())); | |
| 1400 | } | |
| 1401 | // Registered: from now on it starts with no token. | |
| 1402 | if step == Step::Up && with_token && (error.is_none() || running) { | |
| 1403 | let app = self.app.clone(); | |
| 1404 | if let Some(run) = self.run.as_mut() { | |
| 1405 | run.with_token = false; | |
| 1406 | let registration = Registration { server: config::server(&app).to_string(), workspace: run.workspace.clone(), backend: Backend::Sandboxed }; | |
| 1407 | SandboxRegistration { server: registration.server.clone(), workspace: registration.workspace.clone(), registered: true }.save(&app); | |
| 1408 | registration.save(&app); | |
| 1409 | } | |
| 1410 | } | |
| 1411 | if let Some(error) = error.clone() { | |
| 1412 | if self.registration_trouble(&error) { | |
| 1413 | return; | |
| 1414 | } | |
| 1415 | if step == Step::Update { | |
| 1416 | self.update_failed = Some(format!("The sandbox's image couldn't be updated: {error}")); | |
| 1417 | } else if !running { | |
| 1418 | // Nothing runs: tidied away, and tried again in a while. | |
| 1419 | self.halt(); | |
| 1420 | self.failures += 1; | |
| 1421 | let wait = Duration::from_secs(15 * 2u64.pow(self.failures.min(6))).min(MOST_BACKOFF); | |
| 1422 | self.not_before = Instant::now() + wait; | |
| 1423 | let (code, cause, fix) = status | |
| 1424 | .as_ref() | |
| 1425 | .and_then(diagnosis::sandbox_trouble) | |
| 1426 | .unwrap_or_else(|| (diagnosis::sandbox_code(launcher::why(&error)), error.clone(), None)); | |
| 1427 | return self.set(code, Extra { detail: Some(cause), fix, wait_seconds: Some(wait.as_secs()), ..Extra::default() }); | |
| 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. | 1428 | } |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 1429 | } else if step == Step::Up { |
| 1430 | self.failures = 0; | |
| 1431 | } else { | |
| 1432 | self.update_failed = None; | |
| 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. | 1433 | } |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 1434 | if step == Step::Update && !running { |
| 1435 | // Updated while stopped: it starts again with the new image. | |
| 1436 | self.run = None; | |
| 1437 | self.generation += 1; | |
| 1438 | self.not_before = Instant::now(); | |
| 1439 | return; | |
| 1440 | } | |
| 1441 | if let Some(status) = status { | |
| 1442 | self.sandbox_now(status); | |
| 1443 | } | |
| 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. | 1444 | } |
| 1445 | } | |
| 1446 | ||
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 1447 | /// What the tray says while the sandbox starts. |
| 1448 | const STARTING_SANDBOX: &str = "Starting the sandbox. The first time, it downloads and checks the sandbox's image, which takes a few minutes."; | |
| 1449 | ||
| 1450 | fn some_text(text: &str) -> Option<String> { | |
| 1451 | Some(text.to_owned()) | |
| 1452 | } | |
| 1453 | ||
| 1454 | /// What the app shows for how the sandbox is, with no launcher command | |
| 1455 | /// running: what is wrong with Linux jobs first, else what its runners do. | |
| 1456 | fn shown(status: &Status) -> (Code, Extra) { | |
| 1457 | if let Some((code, cause, fix)) = diagnosis::sandbox_trouble(status) { | |
| 1458 | return (code, Extra { detail: Some(cause), fix, ..Extra::default() }); | |
| 1459 | } | |
| 1460 | let linux = status.linux(); | |
| 1461 | match (linux.state, status.job()) { | |
| 1462 | (_, Some(_)) => (Code::Busy, Extra::default()), | |
| 1463 | (State::Running, None) => (Code::Idle, Extra::default()), | |
| 1464 | (State::Starting, None) => (Code::Starting, detail(STARTING_SANDBOX)), | |
| 1465 | (State::Updating, None) => (Code::Updating, detail("Updating the sandbox to the newest image.")), | |
| 1466 | // Linux jobs aren't running, but Windows jobs are. | |
| 1467 | _ if status.running() => (Code::Idle, Extra::default()), | |
| 1468 | _ => (Code::SandboxFailed, detail("The sandbox isn't running.")), | |
| 1469 | } | |
| 1470 | } | |
| 1471 | ||
| 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. | 1472 | /// What `register-and-run` reads: where g1t is, the runner's name and |
| 1473 | /// labels, and the token, the first time only. The app updates the runner, | |
| 1474 | /// so the runner does not update itself; registering again takes the | |
| 1475 | /// place of this computer's earlier runner of the same name. | |
| 1476 | fn environment(server: &Url, backend: Backend, token: Option<&str>) -> Vec<(&'static str, String)> { | |
| 1477 | let mut envs = vec![ | |
| 1478 | ("G1T_RUNNER_URL", server.as_str().trim_end_matches('/').to_owned()), | |
| 1479 | ("G1T_RUNNER_NAME", backend::machine_name()), | |
| 1480 | ("G1T_RUNNER_LABELS", "desktop".to_owned()), | |
| 1481 | ("G1T_RUNNER_REPLACE", "1".to_owned()), | |
| 1482 | ("G1T_RUNNER_NO_AUTO_UPDATE", "1".to_owned()), | |
| 1483 | ]; | |
| 1484 | if backend == Backend::Host { | |
| 1485 | envs.push(("G1T_RUNNER_NO_DOCKER", "1".to_owned())); | |
| 1486 | } | |
| 1487 | if let Some(token) = token { | |
| 1488 | envs.push(("G1T_RUNNER_TOKEN", token.to_owned())); | |
| 1489 | } | |
| 1490 | envs | |
| 1491 | } | |
| 1492 | ||
| 1493 | #[cfg(test)] | |
| 1494 | mod tests { | |
| 1495 | use super::*; | |
| 1496 | ||
| 1497 | fn view(code: Code, job: Option<&str>) -> View { | |
| 1498 | let facts = Facts { workspace: Some("acme".into()), backend: "sandboxed", machine: "box".into(), job: job.map(str::to_owned), ..Facts::default() }; | |
| 1499 | View { | |
| 1500 | diagnosis: diagnosis::diagnose(code, &facts), | |
| 1501 | closed: diagnosis::closed(&facts), | |
| 1502 | running: code == Code::Idle, | |
| 1503 | version: None, | |
| 1504 | backend: Backend::Sandboxed, | |
| 1505 | workspace: Some("acme".into()), | |
| 1506 | workspaces: vec![], | |
| 1507 | enabled: true, | |
| 1508 | paused: false, | |
| 1509 | link: None, | |
| 1510 | machine: "box".into(), | |
| 1511 | platform: "windows".into(), | |
| 1512 | log: vec![], | |
| 1513 | } | |
| 1514 | } | |
| 1515 | ||
| 1516 | #[test] | |
| 1517 | fn the_tray_says_what_the_runner_is_doing() { | |
| 1518 | assert_eq!(headline(&view(Code::Idle, None)), "Runner: Waiting for jobs"); | |
| 1519 | assert_eq!(headline(&view(Code::Busy, Some("Build (acme/web)"))), "Runner: Running Build (acme/web)"); | |
| 1520 | assert_eq!(headline(&view(Code::NoDocker, None)), "Runner: Not running: Docker isn't running"); | |
| 1521 | assert_eq!(headline(&view(Code::Busy, Some(&"x".repeat(200)))).chars().count(), 60); | |
| 1522 | } | |
| 1523 | ||
| 1524 | #[test] | |
| 1525 | fn the_runner_is_told_where_and_who_and_the_token_only_once() { | |
| 1526 | let server = Url::parse("https://g1t.sh/").unwrap(); | |
| 1527 | let envs = environment(&server, Backend::Host, Some("g1trt_abc")); | |
| 1528 | let get = |name: &str| envs.iter().find(|(n, _)| *n == name).map(|(_, v)| v.as_str()); | |
| 1529 | assert_eq!(get("G1T_RUNNER_URL"), Some("https://g1t.sh")); | |
| 1530 | assert_eq!(get("G1T_RUNNER_LABELS"), Some("desktop")); | |
| 1531 | assert_eq!(get("G1T_RUNNER_NO_DOCKER"), Some("1")); | |
| 1532 | assert_eq!(get("G1T_RUNNER_NO_AUTO_UPDATE"), Some("1")); | |
| 1533 | assert_eq!(get("G1T_RUNNER_TOKEN"), Some("g1trt_abc")); | |
| 1534 | let again = environment(&server, Backend::Docker, None); | |
| 1535 | assert!(!again.iter().any(|(name, _)| *name == "G1T_RUNNER_TOKEN" || *name == "G1T_RUNNER_NO_DOCKER")); | |
| 1536 | } | |
| 1537 | ||
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 1538 | fn status(json: &str) -> Status { |
| 1539 | launcher::parse(json).unwrap() | |
| 1540 | } | |
| 1541 | ||
| 1542 | #[test] | |
| 1543 | fn the_sandbox_shows_what_its_runners_do_and_linux_trouble_first() { | |
| 1544 | let running = status(r#"{"schema":1,"state":"running","capabilities":[{"id":"linux","available":true,"state":"running"},{"id":"windows","available":false,"state":"absent","cause":"Windows Sandbox is not turned on, or this edition of Windows has none (Home does not)."}]}"#); | |
| 1545 | assert_eq!(shown(&running).0, Code::Idle); | |
| 1546 | let busy = status(r#"{"schema":1,"state":"running","job":{"kind":"workflow","id":"j","name":"build","repo":"acme/web"}}"#); | |
| 1547 | assert_eq!(shown(&busy).0, Code::Busy); | |
| 1548 | let starting = status(r#"{"schema":1,"state":"starting","capabilities":[{"id":"linux","available":true,"state":"starting"}]}"#); | |
| 1549 | assert_eq!(shown(&starting).0, Code::Starting); | |
| 1550 | let updating = status(r#"{"schema":1,"state":"updating","capabilities":[{"id":"linux","available":true,"state":"updating"}]}"#); | |
| 1551 | assert_eq!(shown(&updating).0, Code::Updating); | |
| 1552 | // Windows jobs run while WSL is missing: what is wrong with Linux jobs is what it says. | |
| 1553 | let no_wsl = status(r#"{"schema":1,"state":"error","error":"WSL is not installed, and the sandbox for Linux jobs is a WSL distribution of its own.","capabilities":[ | |
| 1554 | {"id":"linux","available":false,"state":"error","cause":"WSL is not installed, and the sandbox for Linux jobs is a WSL distribution of its own.","fix":"Open a terminal as administrator and run `wsl --install --no-distribution`."}, | |
| 1555 | {"id":"windows","available":true,"state":"running"}]}"#); | |
| 1556 | let (code, extra) = shown(&no_wsl); | |
| 1557 | assert_eq!(code, Code::NoWsl); | |
| 1558 | assert!(extra.detail.unwrap().starts_with("WSL is not installed")); | |
| 1559 | let failed = status(r#"{"schema":1,"state":"error","error":"the sandbox did not start within 180 seconds","capabilities":[{"id":"linux","available":true,"state":"error"}]}"#); | |
| 1560 | let (code, extra) = shown(&failed); | |
| 1561 | assert_eq!((code, extra.detail.as_deref()), (Code::SandboxFailed, Some("the sandbox did not start within 180 seconds"))); | |
| 1562 | let stopped = status(r#"{"schema":1,"state":"stopped","capabilities":[{"id":"linux","available":true,"state":"stopped"},{"id":"windows","available":true,"state":"stopped"}]}"#); | |
| 1563 | assert_eq!(shown(&stopped).0, Code::SandboxFailed); | |
| 1564 | } | |
| 1565 | ||
| 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. | 1566 | #[test] |
| 1567 | fn the_site_names_workspaces_in_its_own_words() { | |
| 1568 | let offered: Vec<Offered> = serde_json::from_str(r#"[{"slug":"acme","name":"Acme"},{"slug":"solo"}]"#).unwrap(); | |
| 1569 | assert_eq!(offered[1], Offered { slug: "solo".into(), name: None }); | |
| 1570 | } | |
| 1571 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.