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

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