Skip to content
1,853 linesCodeBlameRaw

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

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

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