Skip to content
2,282 linesCodeBlameRaw

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

Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.1//! The runner in the app: g1t's self-hosted runner, started for the
2//! signed-in person when g1t opens and stopped when it quits, so their
3//! computer runs their workspace's `runs-on: self-hosted` jobs with
4//! nothing to install, register or start by hand
5//! (docs.g1t.sh/guides/desktop/#the-runner).
6//!
7//! - **When.** On by default; settings turn it off ("Run jobs on this
8//! computer while g1t is open"), and Stop in settings or the tray stops
9//! it until Start, or until g1t starts again. It starts a few seconds
10//! after the window, never before.
11//! - **How.** The backend settings choose (backend.rs): Sandboxed, the
12//! default, where nothing a job does reaches this computer; Docker; or
13//! directly on this computer. The two that are not sandboxes are only
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.14//! ever chosen by the person, and nothing falls back to them. Sandboxed
15//! is `g1t-runner sandbox` (launcher.rs) on every OS: `up` a few seconds
16//! after the window, `status` every few seconds while it runs, `down`
17//! as g1t quits, and `down --drain` when another backend is chosen.
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.18//! Each kind of job it runs (Linux jobs, Windows jobs) starts and stops
19//! on its own too: settings keep which are on (`runnerCapabilities`),
20//! every `up` names them (`--only`), and stopping one is `down --only`
21//! it, which leaves the other running. One that should run and doesn't
22//! while the other runs is tried again by itself, every few minutes, and
23//! every five while the newest release has no sandbox images.
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.24//! - **Registering.** The first time, and when the workspace or backend
25//! changes, the app needs a registration token for the workspace. Only
26//! the site can make one, for an owner who is signed in: the app asks
27//! the site's page (`g1t-runner-wanted`), the page asks the server
28//! (`POST /-/desktop/runner-token`) and hands the token to the app
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.29//! (`runner_register` in bridge.rs), which gives it to `g1t-runner`
30//! once: in its environment, or to the sandbox's first `up`. It is never
31//! shown or written to the log. The registration is kept
32//! (registration.json and the runner's own folder; the sandbox keeps its
33//! own, and sandbox-registration.json says whose it is) and used on
34//! every later start.
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.35//! - **Up to date.** Soon after it starts and every hour, and when the
36//! person asks ("Check for runner updates"), the app looks for a newer
37//! release on a thread of its own: its manifest only when g1t's release
38//! key signed it (native.rs), its program only when its SHA-256 is the
39//! manifest's. What it finds waits, downloaded, until each kind of job
40//! is idle, and goes in kind by kind, never in the middle of a job: the
41//! sandbox's Linux jobs take its image (`sandbox update`, which drains
42//! them itself) once they have no job; its Windows jobs, whose runner is
43//! the program itself, are drained (`down --only windows --drain`), the
44//! program is replaced, and they start again; a runner in Docker or on
45//! this computer is stopped between jobs and started on the new one.
46//! "Download updates by themselves" off, it looks only when asked.
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.47//! - **Seen.** What it is doing, and when it is not running why and how to
The desktop app is 0.3.4, and the runner has a window of its own. Beside the title bar's Settings button a chip says what the runner is doing, live, with a dot in its colour (Runner running, Running a job, Running 2 jobs, Runner starting, Runner updating, Runner stopped, Runner offline, Runner needs you), as Docker Desktop shows its engine; in a narrow window only the dot shows. It opens the Runner window, as the tray's runner row now does: at its top what the whole runner is doing, with what it is running, its version and workspace, and its own Stop or Start; then each kind of job's card as before; then its setup (running jobs here at all, how jobs run, the workspace, the computer with Check for runner updates and View on g1t); and its log, open and following along as it is written. Settings keeps the runner as one line, what it is doing with Open runner, and each window saves only its own settings over what the app has now, so neither undoes the other's. The chip's words are diagnosis.rs's, the Runner window is made off the main thread as Settings now is, and it has a capability of its own with only the commands it uses; the title bar may now ask the runner's status. Two things fixed on the way: a settings row hidden until it applies, Restart to change the title bar, showed all the time, since its row's display beat the hidden attribute; and in a narrow window the title bar's buttons ran over the search, which now shrinks instead. The desktop guide describes the chip, the Runner window and where each runner setting now is.48//! fix it (diagnosis.rs), is in the title bar's chip, the Runner window
49//! (launcher/runner.html), the tray, a line in settings, and on the
50//! site's Runners page (`g1t-runner-status`), read from what its log says
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.51//! (log.rs), or for the sandbox from its status. The whole log is kept
52//! 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.53//!
54//! All of it runs on a thread of its own, which owns the runner's process
55//! and is told what happened through one channel.
56
57mod backend;
58pub mod diagnosis;
59mod launcher;
60mod log;
61mod native;
62
63use std::collections::VecDeque;
64use std::fs::{File, OpenOptions};
65use std::io::{BufRead, BufReader, Read, Write};
66use std::path::PathBuf;
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.67use 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.68use std::sync::Mutex;
69use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender};
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.70use std::time::{Duration, Instant, SystemTime};
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.71
72use serde::{Deserialize, Serialize};
73use tauri::{AppHandle, Emitter, EventTarget, Manager};
74use url::Url;
75
76pub use backend::Backend;
77use backend::{Engine, Missing};
78use diagnosis::{Code, Diagnosis, Facts};
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.79use launcher::{Down, State, Status};
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.80use native::Fault;
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.81
82use crate::{config, frame, settings, tray};
83
84/// What settings, the tray and the site listen for: the runner's status changed.
85pub const STATUS_EVENT: &str = "g1t-runner-status";
86/// What the site's page listens for: the app needs a registration token for a workspace.
87pub const WANTED_EVENT: &str = "g1t-runner-wanted";
88
89/// How long after the window the runner starts.
90const AFTER_START: Duration = Duration::from_secs(4);
91/// How often the thread looks at the runner when nothing has happened.
92const TICK: Duration = Duration::from_secs(5);
93/// How long after starting the first look for an update is, then how often.
94const FIRST_UPDATE: Duration = Duration::from_secs(90);
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.95const UPDATE_EVERY: Duration = Duration::from_secs(60 * 60);
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.96/// How often a backend that cannot start is looked at again.
97const LOOK_AGAIN: Duration = Duration::from_secs(30);
98/// How long the app waits for the site to hand over a token before asking again.
99const ASK_AGAIN: Duration = Duration::from_secs(60);
100/// The longest the app waits before starting a runner that stopped by itself again.
101const MOST_BACKOFF: Duration = Duration::from_secs(10 * 60);
102/// The runner says when a poll fails, and nothing when one works: this
103/// long without another failure, it is back (its longest wait is 60s).
104const BACK_ONLINE: Duration = Duration::from_secs(75);
105/// The runner's log is started over when it is bigger than this.
106const LOG_LIMIT: u64 = 5 * 1024 * 1024;
107/// The last lines of the log settings show.
108const TAIL: usize = 40;
109
110/// A workspace the site says the person may add runners to: those they own.
111#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
112pub struct Offered {
113 pub slug: String,
114 #[serde(default)]
115 pub name: Option<String>,
116}
117
118/// What the runner is doing, as the thread last said.
119#[derive(Clone, Debug, Default, PartialEq)]
120struct Now {
121 code: Code,
122 detail: Option<String>,
123 exit_code: Option<i32>,
124 wait_seconds: Option<u64>,
125 job: Option<String>,
126 version: Option<String>,
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.127 /// What the sandbox says to do, for a cause in its own words.
128 fix: Option<String>,
129 /// How the sandbox is, while jobs run sandboxed.
130 sandbox: Option<Status>,
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.131 /// A sandbox run is up, or on its way.
132 live: bool,
133 /// The kinds of job a launcher command is starting now.
134 starting: Vec<String>,
135 /// How far the sandbox's image has downloaded, while it does.
136 download: Option<launcher::Download>,
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.137}
138
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.139/// How the runner's updates stand, as the thread last said.
140#[derive(Clone, Debug, Default, PartialEq)]
141struct Updates {
142 /// When the releases last answered.
143 checked_at: Option<SystemTime>,
144 checking: bool,
145 /// A newer version, downloaded and checked, waiting.
146 ready: Option<String>,
147 /// Its program is still to be put in place.
148 program: bool,
149 /// The sandbox's image is still to take it.
150 image: bool,
151 /// The kinds of job taking it now.
152 installing: Vec<String>,
153}
154
155/// What a look at the releases found.
156struct Found {
157 version: String,
158 /// Newer than the program here.
159 newer: bool,
160 /// Its program, downloaded and checked: None when it isn't newer, or is
161 /// already waiting.
162 program: Option<Vec<u8>>,
163 /// The release has the sandbox's images.
164 images: bool,
165}
166
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.167/// What the site last said about the person.
168#[derive(Clone, Debug, Default, PartialEq)]
169struct Site {
170 /// Unknown until the site's first page loads.
171 signed_in: Option<bool>,
172 owned: Vec<Offered>,
173 current: Option<String>,
174}
175
176/// What the rest of the app shares with the thread.
177#[derive(Default)]
178pub struct Shared {
179 now: Mutex<Now>,
180 /// The workspace a token is wanted for.
181 wanted: Mutex<Option<String>>,
182 site: Mutex<Site>,
183 /// Stopped by the person until Start, or until g1t starts again.
184 paused: Mutex<bool>,
185 tail: Mutex<VecDeque<String>>,
186 tx: Mutex<Option<Sender<Ask>>>,
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.187 updates: Mutex<Updates>,
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.188}
189
190/// What settings, the tray and the site are shown.
191#[derive(Clone, Debug, PartialEq, Serialize)]
192#[serde(rename_all = "camelCase")]
193pub struct View {
194 pub diagnosis: Diagnosis,
195 /// What the site says of this computer once g1t is closed on it.
196 pub closed: Diagnosis,
197 pub running: bool,
198 pub version: Option<String>,
199 pub backend: Backend,
200 pub workspace: Option<String>,
201 /// The workspaces it may run for, from the site.
202 pub workspaces: Vec<Offered>,
203 /// The setting: run jobs while g1t is open.
204 pub enabled: bool,
205 pub paused: bool,
206 /// The workspace's Runners page.
207 pub link: Option<String>,
208 /// This computer's name, which the runner has on g1t.
209 pub machine: String,
210 pub platform: String,
211 /// The last lines of the runner's log.
212 pub log: Vec<String>,
The desktop app is 0.3.4, and the runner has a window of its own. Beside the title bar's Settings button a chip says what the runner is doing, live, with a dot in its colour (Runner running, Running a job, Running 2 jobs, Runner starting, Runner updating, Runner stopped, Runner offline, Runner needs you), as Docker Desktop shows its engine; in a narrow window only the dot shows. It opens the Runner window, as the tray's runner row now does: at its top what the whole runner is doing, with what it is running, its version and workspace, and its own Stop or Start; then each kind of job's card as before; then its setup (running jobs here at all, how jobs run, the workspace, the computer with Check for runner updates and View on g1t); and its log, open and following along as it is written. Settings keeps the runner as one line, what it is doing with Open runner, and each window saves only its own settings over what the app has now, so neither undoes the other's. The chip's words are diagnosis.rs's, the Runner window is made off the main thread as Settings now is, and it has a capability of its own with only the commands it uses; the title bar may now ask the runner's status. Two things fixed on the way: a settings row hidden until it applies, Restart to change the title bar, showed all the time, since its row's display beat the hidden attribute; and in a narrow window the title bar's buttons ran over the search, which now shrinks instead. The desktop guide describes the chip, the Runner window and where each runner setting now is.213 /// In a few words, for the title bar's chip.
214 pub chip: diagnosis::Chip,
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.215}
216
217/// What the thread is told.
218enum Ask {
219 /// Something changed: settings, Start or Stop, Retry.
220 Look,
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.221 /// A kind of job was started: start what should run now, stopping nothing.
222 Soon,
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.223 Retry,
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.224 /// "Remove it and start again": the sandbox this computer has, removed
225 /// with its registration, and the app's own made in its place.
226 RemoveSandbox,
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.227 /// "Check for runner updates": look now, and install what is found
228 /// even with "Download updates by themselves" off.
229 Check,
230 /// What a look at the releases found.
231 Checked(Result<Found, Fault>),
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.232 Token { workspace: String, token: String },
233 Refused { workspace: String, allowed: bool, why: String },
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.234 /// A line of the runner's output, or the launcher's; true from its standard output.
235 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.236 Exited(u64),
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.237 /// What `sandbox status` said.
238 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.239 Quit(Sender<()>),
240}
241
242/// The registration the app made, besides what the runner keeps itself.
243#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
244struct Registration {
245 server: String,
246 workspace: String,
247 backend: Backend,
248}
249
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.250/// Whose the sandbox's registration is. The sandbox keeps its own, and
251/// keeps it while another backend runs, so choosing Sandboxed again needs
252/// no new one; this says which workspace it is for.
253#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
254struct SandboxRegistration {
255 server: String,
256 workspace: String,
257 /// False from the `up` that was given a token until it registered.
258 registered: bool,
259}
260
261impl SandboxRegistration {
262 fn path(app: &AppHandle) -> PathBuf {
263 folder(app).join("sandbox-registration.json")
264 }
265 fn load(app: &AppHandle) -> Option<SandboxRegistration> {
266 std::fs::read(Self::path(app)).ok().and_then(|bytes| serde_json::from_slice(&bytes).ok())
267 }
268 fn save(&self, app: &AppHandle) {
269 let _ = std::fs::create_dir_all(folder(app));
270 let _ = std::fs::write(Self::path(app), serde_json::to_vec_pretty(self).unwrap_or_default());
271 }
272 fn clear(app: &AppHandle) {
273 let _ = std::fs::remove_file(Self::path(app));
274 }
275}
276
The desktop app's runner lives in its local data folder, which stays on the computer: %LOCALAPPDATA%\sh.g1t.desktop\runner on Windows, where it was in roaming data (%APPDATA%), which networks with roaming profiles copy to a profile server along with everything else there, the sandbox's VM disk and the runner's credential included. A computer whose runner was already set up in roaming data keeps it where it is, since its WSL distribution is registered at that path and moving its disk would break it; on macOS and Linux the two folders were always the same one. The desktop guide gives the new place.277/// Where the runner keeps everything: its program, registrations, log and
278/// the sandbox's VM. In the app's local data, which stays on this computer:
279/// roaming data (Windows' %APPDATA%) is copied to a profile server on
280/// networks with roaming profiles, and a VM's disk and a runner's
281/// credential have no business there. A computer whose runner already
282/// lives in roaming data keeps it there: its WSL distribution is
283/// registered at that path, and moving its disk would break it. On macOS
284/// and Linux the two are the same folder.
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.285fn folder(app: &AppHandle) -> PathBuf {
The desktop app's runner lives in its local data folder, which stays on the computer: %LOCALAPPDATA%\sh.g1t.desktop\runner on Windows, where it was in roaming data (%APPDATA%), which networks with roaming profiles copy to a profile server along with everything else there, the sandbox's VM disk and the runner's credential included. A computer whose runner was already set up in roaming data keeps it where it is, since its WSL distribution is registered at that path and moving its disk would break it; on macOS and Linux the two folders were always the same one. The desktop guide gives the new place.286 let fallback = || std::env::temp_dir().join("sh.g1t.desktop");
287 let local = app.path().app_local_data_dir().unwrap_or_else(|_| fallback()).join("runner");
288 let roaming = app.path().app_data_dir().unwrap_or_else(|_| fallback()).join("runner");
289 chosen_folder(local, roaming)
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.290}
291
The desktop app's runner lives in its local data folder, which stays on the computer: %LOCALAPPDATA%\sh.g1t.desktop\runner on Windows, where it was in roaming data (%APPDATA%), which networks with roaming profiles copy to a profile server along with everything else there, the sandbox's VM disk and the runner's credential included. A computer whose runner was already set up in roaming data keeps it where it is, since its WSL distribution is registered at that path and moving its disk would break it; on macOS and Linux the two folders were always the same one. The desktop guide gives the new place.292/// The local folder, unless the runner was set up in the roaming one before.
293fn chosen_folder(local: PathBuf, roaming: PathBuf) -> PathBuf {
294 if !local.exists() && roaming.exists() { roaming } else { local }
295}
296
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.297impl Registration {
298 fn path(app: &AppHandle) -> PathBuf {
299 folder(app).join("registration.json")
300 }
301 fn load(app: &AppHandle) -> Option<Registration> {
302 std::fs::read(Self::path(app)).ok().and_then(|bytes| serde_json::from_slice(&bytes).ok())
303 }
304 fn save(&self, app: &AppHandle) {
305 let _ = std::fs::create_dir_all(folder(app));
306 let _ = std::fs::write(Self::path(app), serde_json::to_vec_pretty(self).unwrap_or_default());
307 }
308 fn clear(app: &AppHandle) {
309 let _ = std::fs::remove_file(Self::path(app));
310 }
311}
312
313/// Where a native runner of a backend keeps its configuration.
314fn native_dir(app: &AppHandle, backend: Backend) -> PathBuf {
315 folder(app).join(match backend {
316 Backend::Docker => "docker",
317 Backend::Host => "host",
318 Backend::Sandboxed => launcher::FOLDER,
319 })
320}
321
322/// The workspace the runner is for: the one settings name, else the one it
323/// is registered with, else the one the person is in on the site, else the
324/// first they own.
325fn workspace(app: &AppHandle) -> Option<String> {
326 let settings = config::settings(app);
327 if let Some(chosen) = settings.runner_workspace.filter(|w| !w.is_empty()) {
328 return Some(chosen);
329 }
330 let server = config::server(app).to_string();
331 if let Some(registration) = Registration::load(app).filter(|r| r.server == server) {
332 return Some(registration.workspace);
333 }
334 let site = app.state::<Shared>().site.lock().unwrap().clone();
335 site.current.filter(|slug| site.owned.iter().any(|o| &o.slug == slug)).or_else(|| site.owned.first().map(|o| o.slug.clone()))
336}
337
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.338/// The kinds of job a sandbox can have.
339const KINDS: [&str; 3] = ["linux", "windows", "macos"];
340
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.341fn facts(app: &AppHandle, now: &Now) -> Facts {
342 let server = config::server(app);
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.343 let settings = config::settings(app);
344 let backend = settings.runner_backend;
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.345 Facts {
346 workspace: workspace(app),
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.347 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.348 machine: backend::machine_name(),
349 job: now.job.clone(),
350 detail: now.detail.clone(),
351 exit_code: now.exit_code,
352 wait_seconds: now.wait_seconds,
353 server: server.host_str().unwrap_or("g1t").to_owned(),
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.354 site: server.as_str().trim_end_matches('/').to_owned(),
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.355 platform: std::env::consts::OS.into(),
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.356 fix: now.fix.clone(),
357 sandbox: now.sandbox.clone().filter(|_| backend == Backend::Sandboxed),
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.358 off: KINDS.iter().filter(|kind| !settings.runner_capabilities.on(kind)).map(|kind| (*kind).to_owned()).collect(),
359 live: now.live,
360 starting: now.starting.clone(),
361 download: now.download.clone(),
362 held: !settings.runner || *app.state::<Shared>().paused.lock().unwrap(),
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.363 update: update_facts(app, now, 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.364 }
365}
366
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.367/// How the runner's updates stand, for the words: what is still to take a
368/// waiting version. The sandbox's Windows jobs (any kind but Linux) take
369/// its program while they run; a runner with no kinds of job takes it
370/// itself.
371fn update_facts(app: &AppHandle, now: &Now, backend: Backend) -> diagnosis::Update {
372 let updates = app.state::<Shared>().updates.lock().unwrap().clone();
373 let sandbox = now.sandbox.as_ref().filter(|_| backend == Backend::Sandboxed);
374 let mut waiting = Vec::new();
375 if updates.image {
376 waiting.push("linux".to_owned());
377 }
378 if updates.program {
379 match sandbox {
380 Some(status) => waiting.extend(native_kinds(status).into_iter().map(|c| c.id.clone())),
381 None => waiting.push("runner".to_owned()),
382 }
383 }
384 diagnosis::Update {
385 installed: native::installed(&folder(app)),
386 checked_at: updates.checked_at.map(rfc3339),
387 checked_ago: updates.checked_at.and_then(|at| at.elapsed().ok()).map(|ago| ago.as_secs()),
388 checking: updates.checking,
389 ready: updates.ready,
390 waiting,
391 installing: updates.installing,
392 }
393}
394
395/// The kinds of job running on the program itself, which replacing it
396/// stops: the sandbox's Windows jobs (not Linux jobs, which run in its VM).
397fn native_kinds(status: &Status) -> Vec<&launcher::Capability> {
398 status.capabilities.iter().filter(|c| c.id != "linux" && !c.off() && matches!(c.state, State::Running | State::Starting)).collect()
399}
400
401/// A time as RFC 3339, in UTC to the second: `2026-10-12T12:04:05Z`.
402fn rfc3339(at: SystemTime) -> String {
403 let seconds = at.duration_since(SystemTime::UNIX_EPOCH).map_or(0, |since| since.as_secs());
404 let (days, rest) = (seconds / 86_400, seconds % 86_400);
405 // Days since 1970 to a date in the proleptic Gregorian calendar (Howard Hinnant's civil_from_days).
406 let z = days as i64 + 719_468;
407 let era = z.div_euclid(146_097);
408 let day_of_era = z.rem_euclid(146_097);
409 let year_of_era = (day_of_era - day_of_era / 1460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
410 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
411 let month_index = (5 * day_of_year + 2) / 153;
412 let day = day_of_year - (153 * month_index + 2) / 5 + 1;
413 let month = if month_index < 10 { month_index + 3 } else { month_index - 9 };
414 let year = year_of_era + era * 400 + i64::from(month <= 2);
415 format!("{year:04}-{month:02}-{day:02}T{:02}:{:02}:{:02}Z", rest / 3600, rest % 3600 / 60, rest % 60)
416}
417
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.418pub fn view(app: &AppHandle) -> View {
419 let shared = app.state::<Shared>();
420 let now = shared.now.lock().unwrap().clone();
421 let settings = config::settings(app);
422 let facts = facts(app, &now);
423 let server = config::server(app);
The desktop app is 0.3.4, and the runner has a window of its own. Beside the title bar's Settings button a chip says what the runner is doing, live, with a dot in its colour (Runner running, Running a job, Running 2 jobs, Runner starting, Runner updating, Runner stopped, Runner offline, Runner needs you), as Docker Desktop shows its engine; in a narrow window only the dot shows. It opens the Runner window, as the tray's runner row now does: at its top what the whole runner is doing, with what it is running, its version and workspace, and its own Stop or Start; then each kind of job's card as before; then its setup (running jobs here at all, how jobs run, the workspace, the computer with Check for runner updates and View on g1t); and its log, open and following along as it is written. Settings keeps the runner as one line, what it is doing with Open runner, and each window saves only its own settings over what the app has now, so neither undoes the other's. The chip's words are diagnosis.rs's, the Runner window is made off the main thread as Settings now is, and it has a capability of its own with only the commands it uses; the title bar may now ask the runner's status. Two things fixed on the way: a settings row hidden until it applies, Restart to change the title bar, showed all the time, since its row's display beat the hidden attribute; and in a narrow window the title bar's buttons ran over the search, which now shrinks instead. The desktop guide describes the chip, the Runner window and where each runner setting now is.424 let diagnosed = diagnosis::diagnose(now.code, &facts);
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.425 View {
The desktop app is 0.3.4, and the runner has a window of its own. Beside the title bar's Settings button a chip says what the runner is doing, live, with a dot in its colour (Runner running, Running a job, Running 2 jobs, Runner starting, Runner updating, Runner stopped, Runner offline, Runner needs you), as Docker Desktop shows its engine; in a narrow window only the dot shows. It opens the Runner window, as the tray's runner row now does: at its top what the whole runner is doing, with what it is running, its version and workspace, and its own Stop or Start; then each kind of job's card as before; then its setup (running jobs here at all, how jobs run, the workspace, the computer with Check for runner updates and View on g1t); and its log, open and following along as it is written. Settings keeps the runner as one line, what it is doing with Open runner, and each window saves only its own settings over what the app has now, so neither undoes the other's. The chip's words are diagnosis.rs's, the Runner window is made off the main thread as Settings now is, and it has a capability of its own with only the commands it uses; the title bar may now ask the runner's status. Two things fixed on the way: a settings row hidden until it applies, Restart to change the title bar, showed all the time, since its row's display beat the hidden attribute; and in a narrow window the title bar's buttons ran over the search, which now shrinks instead. The desktop guide describes the chip, the Runner window and where each runner setting now is.426 chip: diagnosis::chip(&diagnosed),
427 diagnosis: diagnosed,
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.428 closed: diagnosis::closed(&facts),
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.429 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.430 version: now.version,
431 backend: settings.runner_backend,
432 link: facts.workspace.as_ref().and_then(|w| server.join(&format!("{w}/-/runners")).ok()).map(|u| u.to_string()),
433 workspace: facts.workspace,
434 workspaces: shared.site.lock().unwrap().owned.clone(),
435 enabled: settings.runner,
436 paused: *shared.paused.lock().unwrap(),
437 machine: facts.machine,
438 platform: facts.platform,
439 log: shared.tail.lock().unwrap().iter().cloned().collect(),
440 }
441}
442
The desktop app is 0.3.3: Settings opens again on Windows, and an update found as g1t starts installs itself. Since 0.3.0, Settings opened from the title bar's button or the site's account menu showed a white window and nothing else: both are synchronous commands, which run on the main thread, and on Windows a webview window made there deadlocks (Tauri's known issue), so the window appeared and its page never loaded. The settings window is now made on a thread of its own whoever asks, and Tauri hands the making to the event loop. And so that opening g1t again is all it takes to leave a copy that misbehaves, the app looks for a newer version as it starts, three seconds after the window rather than fifteen, and one it downloads within its first two minutes is installed straight away, g1t opening again on it, before anyone has begun working in it; a runner in the middle of a job is never cut short for it, and then the update waits for Restart to update as before. After that it looks every hour rather than every six, and what it finds still waits for a restart or quitting. The desktop guide's Updates section says so.443/// Whether the runner is running a job: what restarts g1t waits for it,
444/// since restarting stops the runner and would cut the job short.
445pub fn busy(app: &AppHandle) -> bool {
446 let now = app.state::<Shared>().now.lock().unwrap().clone();
447 now.code == Code::Busy || now.sandbox.as_ref().is_some_and(|status| status.job().is_some())
448}
449
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.450/// The tray's line: `Runner: Waiting for jobs`, at most 60 characters.
451pub fn headline(view: &View) -> String {
452 let line = format!("Runner: {}", view.diagnosis.title);
453 if line.chars().count() > 60 { line.chars().take(59).collect::<String>() + "…" } else { line }
454}
455
456/// Tells everyone what the runner is doing now.
457fn publish(app: &AppHandle) {
458 let view = view(app);
459 tray::runner_changed(app, &view);
460 let _ = app.emit_to(settings::SETTINGS, STATUS_EVENT, view.clone());
The desktop app is 0.3.4, and the runner has a window of its own. Beside the title bar's Settings button a chip says what the runner is doing, live, with a dot in its colour (Runner running, Running a job, Running 2 jobs, Runner starting, Runner updating, Runner stopped, Runner offline, Runner needs you), as Docker Desktop shows its engine; in a narrow window only the dot shows. It opens the Runner window, as the tray's runner row now does: at its top what the whole runner is doing, with what it is running, its version and workspace, and its own Stop or Start; then each kind of job's card as before; then its setup (running jobs here at all, how jobs run, the workspace, the computer with Check for runner updates and View on g1t); and its log, open and following along as it is written. Settings keeps the runner as one line, what it is doing with Open runner, and each window saves only its own settings over what the app has now, so neither undoes the other's. The chip's words are diagnosis.rs's, the Runner window is made off the main thread as Settings now is, and it has a capability of its own with only the commands it uses; the title bar may now ask the runner's status. Two things fixed on the way: a settings row hidden until it applies, Restart to change the title bar, showed all the time, since its row's display beat the hidden attribute; and in a narrow window the title bar's buttons ran over the search, which now shrinks instead. The desktop guide describes the chip, the Runner window and where each runner setting now is.461 let _ = app.emit_to(settings::RUNNER, STATUS_EVENT, view.clone());
462 let _ = app.emit_to(EventTarget::webview(frame::BAR), STATUS_EVENT, view.chip.clone());
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.463 let _ = app.emit_to(EventTarget::webview(frame::SITE), STATUS_EVENT, view);
464}
465
466fn ask(app: &AppHandle, ask: Ask) {
467 if let Some(tx) = app.state::<Shared>().tx.lock().unwrap().as_ref() {
468 let _ = tx.send(ask);
469 }
470}
471
472/// Starts the thread that keeps the runner, as the app starts.
473pub fn install(app: &AppHandle) {
474 let (tx, rx) = mpsc::channel();
475 *app.state::<Shared>().tx.lock().unwrap() = Some(tx.clone());
476 let handle = app.clone();
477 let spawned = std::thread::Builder::new().name("g1t-runner".into()).spawn(move || Keeper::new(handle, tx).keep(rx));
478 if let Err(error) = spawned {
479 eprintln!("g1t: the runner's thread could not start: {error}");
480 }
481}
482
483/// Settings changed: the thread looks again.
484pub fn settings_changed(app: &AppHandle) {
485 ask(app, Ask::Look);
486 publish(app);
487}
488
489/// Start or Stop, from settings or the tray.
490pub fn set_paused(app: &AppHandle, paused: bool) {
491 *app.state::<Shared>().paused.lock().unwrap() = paused;
492 // Start with the setting off turns the setting on: that is what was asked.
493 if !paused && !config::settings(app).runner {
494 let mut settings = config::settings(app);
495 settings.runner = true;
496 let _ = settings::apply(app, settings);
497 }
498 settings_changed(app);
499}
500
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.501/// Start or Stop on one kind of job's card: kept in settings, and the
502/// thread starts or stops that kind alone. Starting one with the runner
503/// off or stopped starts the runner too: that is what was asked.
504pub fn set_kind(app: &AppHandle, kind: &str, on: bool) -> Result<(), String> {
505 let mut settings = config::settings(app);
506 if !settings.runner_capabilities.set(kind, on) {
507 return Err(format!("g1t runs no {kind} jobs"));
508 }
509 if on {
510 settings.runner = true;
511 *app.state::<Shared>().paused.lock().unwrap() = false;
512 }
513 settings::apply(app, settings)?;
514 if on {
515 ask(app, Ask::Soon);
516 }
517 settings_changed(app);
518 Ok(())
519}
520
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.521/// The tray's Start or Stop row.
522pub fn toggle(app: &AppHandle) {
523 let view = view(app);
524 set_paused(app, view.enabled && !view.paused);
525}
526
527/// Retry and Restart: now, rather than when the wait is over.
528pub fn retry(app: &AppHandle) {
529 *app.state::<Shared>().paused.lock().unwrap() = false;
530 ask(app, Ask::Retry);
531}
532
533/// What the site says about the person, on every page: whether they are
534/// signed in, the workspaces they own, and the one they are in.
535pub fn offer(app: &AppHandle, signed_in: bool, owned: Vec<Offered>, current: Option<String>) {
536 let next = Site { signed_in: Some(signed_in), owned, current: current.map(|c| c.to_ascii_lowercase()) };
537 let changed = {
538 let shared = app.state::<Shared>();
539 let mut site = shared.site.lock().unwrap();
540 let changed = *site != next;
541 *site = next;
542 changed
543 };
544 if changed {
545 settings_changed(app);
546 }
547}
548
549/// The site handed over a registration token, or said why it could not:
550/// `allowed` is false when the person may not add runners to the workspace.
551pub fn register(app: &AppHandle, workspace: String, token: Option<String>, allowed: bool, error: Option<String>) {
552 match token {
553 Some(token) if token.starts_with("g1trt_") => ask(app, Ask::Token { workspace, token }),
554 _ => ask(app, Ask::Refused { workspace, allowed, why: error.unwrap_or_else(|| "g1t didn't make a registration token.".into()) }),
555 }
556}
557
558/// A diagnosis's button, or Start and Stop: from the settings window, or
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.559/// from the site's card for this computer. With a kind of job (`linux`,
560/// `windows`), Start and Stop are that kind's alone.
561pub fn act(app: &AppHandle, action: &str, kind: Option<&str>) -> Result<(), String> {
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.562 use tauri_plugin_opener::OpenerExt;
563 let browse = |url: &str| app.opener().open_url(url, None::<&str>).map_err(|e| e.to_string());
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.564 if let ("start" | "stop", Some(kind)) = (action, kind) {
565 return set_kind(app, kind, action == "start");
566 }
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.567 match action {
568 "start" => {
569 set_paused(app, false);
570 retry(app);
571 }
572 "stop" => set_paused(app, true),
573 "retry" | "restart" => retry(app),
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.574 "remove_sandbox" => {
575 *app.state::<Shared>().paused.lock().unwrap() = false;
576 ask(app, Ask::RemoveSandbox);
577 }
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.578 "check_updates" => ask(app, Ask::Check),
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.579 "sign_in" => crate::links::go(app, "/login"),
580 "new_workspace" => crate::links::go(app, "/workspaces/new"),
581 "view_runners" => match workspace(app) {
582 Some(workspace) => crate::links::go(app, &format!("/{workspace}/-/runners")),
583 None => crate::links::go(app, "/"),
584 },
Merge branch 'main' into rm-agents585 // They are chosen in the settings' Runner section.
586 "choose_workspace" | "choose_backend" => settings::open_runner_settings(app),
587 "open_runner" => settings::runner_menu(app, false, None),
588 // The Runner window, bundled with the app: where jobs are taken out
589 // of the sandbox, which no page from the network may do (bridge.rs).
590 "runner_window" => settings::open_runner_window(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.591 "view_log" => {
592 let path = log_file(app);
593 if !path.exists() {
594 return Err("The runner hasn't written a log yet.".into());
595 }
596 app.opener().open_path(path.to_string_lossy(), None::<&str>).map_err(|e| e.to_string())?;
597 }
598 "open_docker" => open_docker(app)?,
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.599 "copy_wsl_install" => copy(launcher::WSL_INSTALL)?,
600 "copy_vfkit_install" => copy(launcher::VFKIT_INSTALL)?,
601 "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.602 "get_docker" => browse("https://docs.docker.com/get-started/get-docker/")?,
603 "install_sysbox" => browse("https://github.com/nestybox/sysbox#installation")?,
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.604 "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.605 "report" => browse(config::server(app).join("support").map_err(|e| e.to_string())?.as_str())?,
606 other => return Err(format!("the runner has no action called {other}")),
607 }
608 Ok(())
609}
610
611/// Starts Docker Desktop, where it is installed in its usual place.
612fn open_docker(app: &AppHandle) -> Result<(), String> {
613 use tauri_plugin_opener::OpenerExt;
614 if cfg!(target_os = "macos") {
615 return app.opener().open_path("/Applications/Docker.app", None::<&str>).map_err(|e| e.to_string());
616 }
617 if cfg!(windows) {
618 let program = std::env::var_os("ProgramFiles").map(PathBuf::from).unwrap_or_else(|| PathBuf::from(r"C:\Program Files"));
619 let desktop = program.join("Docker").join("Docker").join("Docker Desktop.exe");
620 if desktop.exists() {
621 return app.opener().open_path(desktop.to_string_lossy(), None::<&str>).map_err(|e| e.to_string());
622 }
623 return Err("Docker Desktop isn't installed in its usual place. Get Docker to install it.".into());
624 }
625 Err("Start Docker with your system's service manager, such as `sudo systemctl start docker`.".into())
626}
627
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.628/// Puts a command on the clipboard, for the person to paste where it runs.
629fn copy(text: &str) -> Result<(), String> {
630 let tools: &[(&str, &[&str])] = if cfg!(windows) {
631 &[("clip", &[])]
632 } else if cfg!(target_os = "macos") {
633 &[("pbcopy", &[])]
634 } else {
635 &[("wl-copy", &[]), ("xclip", &["-selection", "clipboard"]), ("xsel", &["--clipboard", "--input"])]
636 };
637 for (program, args) in tools {
638 let Ok(mut child) = backend::quiet(program).args(*args).stdin(Stdio::piped()).stdout(Stdio::null()).stderr(Stdio::null()).spawn() else { continue };
639 if let Some(mut input) = child.stdin.take() {
640 let _ = input.write_all(text.as_bytes());
641 }
642 if child.wait().is_ok_and(|status| status.success()) {
643 return Ok(());
644 }
645 }
646 Err(format!("It couldn't be copied. The command is: {text}"))
647}
648
649/// Windows' "Turn Windows features on or off", where Windows Sandbox and
650/// Virtual Machine Platform are.
651fn windows_features() -> Result<(), String> {
652 if !cfg!(windows) {
653 return Err("Windows features are only on Windows.".into());
654 }
655 backend::quiet("optionalfeatures.exe").spawn().map(|_| ()).map_err(|e| format!("Windows features couldn't be opened: {e}"))
656}
657
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.658/// The workspace a token is wanted for now, if one is: the site asks as it loads.
659pub fn wanted(app: &AppHandle) -> Option<String> {
660 app.state::<Shared>().wanted.lock().unwrap().clone()
661}
662
663/// The runner's log, for the settings window to open.
664pub fn log_file(app: &AppHandle) -> PathBuf {
665 folder(app).join("runner.log")
666}
667
668/// Stops the runner as g1t quits or restarts, and waits for it.
669pub fn shutdown(app: &AppHandle) {
670 let tx = app.state::<Shared>().tx.lock().unwrap().take();
671 if let Some(tx) = tx {
672 let (done, wait) = mpsc::channel();
673 if tx.send(Ask::Quit(done)).is_ok() {
674 let _ = wait.recv_timeout(Duration::from_secs(30));
675 }
676 }
677}
678
679/// How the runner is started this time.
680enum Process {
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.681 /// `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.682 Child(Child),
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.683 /// The sandbox, which runs on its own once `up` is done; the launcher
684 /// command the app is waiting for, while there is one.
685 Sandbox(Option<(Step, Child)>),
686}
687
688/// A launcher command that takes a while.
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.689#[derive(Clone, Debug, PartialEq, Eq)]
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.690enum Step {
691 Up,
692 Update,
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.693 /// `up --only <kind>`: one kind of job, started while the rest run.
694 Start(String),
695 /// `down --only <kind>`: one kind of job, stopped, the rest running on.
696 Stop(String),
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.697 /// `down --only <kind> --drain`: one kind of job, stopped after its
698 /// job, so the program its runner is can be replaced.
699 Drain(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.700}
701
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.702impl Step {
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.703 fn name(&self) -> &'static str {
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.704 match self {
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.705 Step::Up | Step::Start(_) => "up",
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.706 Step::Update => "update",
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.707 Step::Stop(_) | Step::Drain(_) => "down",
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.708 }
709 }
710}
711
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.712struct Run {
713 generation: u64,
714 engine: Engine,
715 backend: Backend,
716 workspace: String,
717 process: Process,
718 /// Registered and polling.
719 listening: bool,
720 busy: bool,
721 /// When a poll last failed to reach g1t.
722 offline_at: Option<Instant>,
723 /// Started with a registration token, which a container keeps in its configuration until replaced.
724 with_token: bool,
725 forgotten: bool,
726 token_refused: bool,
727 error: Option<String>,
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.728 /// The status a launcher command is printing, line by line.
729 json: Option<Vec<String>>,
730 /// Asking the sandbox how it is, and when it was last asked.
731 polling: bool,
732 polled_at: Option<Instant>,
733 /// A poll asked before a launcher command ended, whose answer is old.
734 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.735}
736
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.737impl Run {
738 fn new(generation: u64, engine: Engine, backend: Backend, workspace: String, process: Process, with_token: bool) -> Run {
739 Run {
740 generation,
741 engine,
742 backend,
743 workspace,
744 process,
745 listening: false,
746 busy: false,
747 offline_at: None,
748 with_token,
749 forgotten: false,
750 token_refused: false,
751 error: None,
752 json: None,
753 polling: false,
754 polled_at: None,
755 stale: false,
756 }
757 }
758
759 fn step(&self) -> Option<Step> {
760 match &self.process {
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.761 Process::Sandbox(Some((step, _))) => Some(step.clone()),
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.762 _ => None,
763 }
764 }
765}
766
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.767struct Keeper {
768 app: AppHandle,
769 tx: Sender<Ask>,
770 run: Option<Run>,
771 generation: u64,
772 token: Option<(String, String)>,
773 not_before: Instant,
774 failures: u32,
775 next_update: Instant,
776 log: Option<File>,
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.777 /// Why the last update failed (`UpdateFailed`, or `UpdateRefused` for
778 /// a release g1t didn't sign), said while the runner is idle, until
779 /// the next one works.
780 update_failed: Option<(Code, String)>,
781 /// A newer program, downloaded and checked, waiting for the kinds of
782 /// job that run on it to be idle: its version and its bytes.
783 program: Option<(String, Vec<u8>)>,
784 /// The person asked for this update: it installs with "Download
785 /// updates by themselves" off.
786 asked: bool,
787 /// When g1t last looked, as settings last said it.
788 said_checked: String,
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.789 /// A kind of job that should run and doesn't, while another runs:
790 /// when it is tried again, and how many times it has been.
791 retry: Vec<(String, Instant, u32)>,
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.792}
793
794/// What a set of the state carries besides its code.
795#[derive(Default)]
796struct Extra {
797 detail: Option<String>,
798 exit_code: Option<i32>,
799 wait_seconds: Option<u64>,
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.800 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.801}
802
803fn detail(text: impl Into<String>) -> Extra {
804 Extra { detail: Some(text.into()), ..Extra::default() }
805}
806
807impl Keeper {
808 fn new(app: AppHandle, tx: Sender<Ask>) -> Keeper {
809 let now = Instant::now();
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.810 Keeper {
811 app,
812 tx,
813 run: None,
814 generation: 0,
815 token: None,
816 not_before: now + AFTER_START,
817 failures: 0,
818 next_update: now + FIRST_UPDATE,
819 log: None,
820 update_failed: None,
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.821 program: None,
822 asked: false,
823 said_checked: String::new(),
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.824 retry: Vec::new(),
825 }
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.826 }
827
828 fn keep(mut self, rx: Receiver<Ask>) {
829 self.reap();
830 self.set(Code::Starting, detail("Starting after g1t's window."));
831 loop {
832 match rx.recv_timeout(TICK) {
833 Ok(Ask::Quit(done)) => {
834 self.halt();
835 let _ = done.send(());
836 return;
837 }
838 Ok(ask) => self.handle(ask),
839 Err(RecvTimeoutError::Timeout) => {}
840 Err(RecvTimeoutError::Disconnected) => {
841 self.halt();
842 return;
843 }
844 }
845 self.look();
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.846 self.said_when();
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.847 }
848 }
849
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.850 /// Settings say when g1t last looked for a newer runner, in minutes
851 /// and hours: said again as that changes.
852 fn said_when(&mut self) {
853 let (checked_at, checking) = self.with_updates(|updates| (updates.checked_at, updates.checking));
854 let checked_ago = checked_at.and_then(|at| at.elapsed().ok()).map(|ago| ago.as_secs());
855 let checked = diagnosis::checked(&diagnosis::Update { checked_ago, checking, ..diagnosis::Update::default() });
856 if checked != self.said_checked {
857 self.said_checked = checked;
858 publish(&self.app);
859 }
860 }
861
862 fn with_updates<R>(&self, change: impl FnOnce(&mut Updates) -> R) -> R {
863 change(&mut self.app.state::<Shared>().updates.lock().unwrap())
864 }
865
866 /// What an idle runner shows: why its last update failed, until one works.
867 fn idle(&self, code: Code) -> (Code, Extra) {
868 match (&self.update_failed, code) {
869 (Some((failed, why)), Code::Idle) => (*failed, detail(why.clone())),
870 _ => (code, Extra::default()),
871 }
872 }
873
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.874 fn set(&mut self, code: Code, extra: Extra) {
875 let changed = {
876 let shared = self.app.state::<Shared>();
877 let mut now = shared.now.lock().unwrap();
878 let job = if code == Code::Busy { now.job.clone() } else { None };
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.879 let next = Now {
880 code,
881 detail: extra.detail,
882 exit_code: extra.exit_code,
883 wait_seconds: extra.wait_seconds,
884 job,
885 fix: extra.fix,
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.886 live: self.run.as_ref().is_some_and(|run| run.engine == Engine::Launcher),
887 ..now.clone()
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.888 };
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.889 let changed = *now != next;
890 *now = next;
891 changed
892 };
893 if changed {
894 publish(&self.app);
895 }
896 }
897
898 fn code(&self) -> Code {
899 self.app.state::<Shared>().now.lock().unwrap().code
900 }
901
902 fn note(&mut self, job: Option<String>, version: Option<String>) {
903 let shared = self.app.state::<Shared>();
904 let mut now = shared.now.lock().unwrap();
905 if job.is_some() {
906 now.job = job;
907 }
908 if version.is_some() {
909 now.version = version;
910 }
911 }
912
913 /// A line of the runner's log, into the log file and the tail settings show.
914 fn write_log(&mut self, line: &str) {
915 {
916 let shared = self.app.state::<Shared>();
917 let mut tail = shared.tail.lock().unwrap();
918 if tail.len() == TAIL {
919 tail.pop_front();
920 }
921 tail.push_back(line.to_owned());
922 }
923 let path = log_file(&self.app);
924 if self.log.is_none() {
925 let _ = std::fs::create_dir_all(folder(&self.app));
926 if std::fs::metadata(&path).is_ok_and(|m| m.len() > LOG_LIMIT) {
927 let _ = std::fs::rename(&path, path.with_extension("log.old"));
928 }
929 self.log = OpenOptions::new().create(true).append(true).open(&path).ok();
930 }
931 if let Some(file) = self.log.as_mut() {
932 let _ = writeln!(file, "{line}");
933 }
934 }
935
936 fn handle(&mut self, ask: Ask) {
937 match ask {
938 Ask::Look | Ask::Quit(_) => {}
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.939 Ask::Soon => {
940 self.not_before = Instant::now();
941 for (_, at, _) in &mut self.retry {
942 *at = Instant::now();
943 }
944 }
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.945 Ask::RemoveSandbox => {
946 self.set(Code::Starting, detail("Removing this computer's sandbox."));
947 self.halt();
948 self.forget_sandbox();
949 self.failures = 0;
950 self.not_before = Instant::now();
951 self.set(Code::Starting, detail("Starting a sandbox of g1t's own."));
952 }
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.953 Ask::Retry => {
954 self.not_before = Instant::now();
955 self.failures = 0;
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.956 // Each kind of job that waits to be tried again is tried now.
957 for (_, at, _) in &mut self.retry {
958 *at = Instant::now();
959 }
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.960 match self.code() {
961 // The runner is fine: only the update is tried again.
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.962 Code::UpdateFailed | Code::UpdateRefused if self.run.is_some() => self.check(true),
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.963 Code::Offline => self.halt(),
964 _ if self.run.as_ref().is_some_and(|run| !run.listening) => self.halt(),
965 _ => {}
966 }
967 }
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.968 Ask::Check => self.check(true),
969 Ask::Checked(found) => self.checked(found),
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.970 Ask::Token { workspace, token } => {
971 let shared = self.app.state::<Shared>();
972 let mut wanted = shared.wanted.lock().unwrap();
973 if wanted.as_deref() == Some(workspace.as_str()) {
974 *wanted = None;
975 self.token = Some((workspace, token));
976 self.not_before = Instant::now();
977 }
978 }
979 Ask::Refused { workspace, allowed, why } => {
980 let matched = {
981 let shared = self.app.state::<Shared>();
982 let mut wanted = shared.wanted.lock().unwrap();
983 let matched = wanted.as_deref() == Some(workspace.as_str());
984 if matched {
985 *wanted = None;
986 }
987 matched
988 };
989 if matched {
990 self.not_before = Instant::now() + MOST_BACKOFF;
991 self.set(if allowed { Code::RegisterFailed } else { Code::NotAllowed }, detail(why));
992 }
993 }
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.994 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.995 Ask::Exited(generation) => self.exited(generation),
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.996 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.997 }
998 }
999
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1000 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.1001 let Some(run) = self.run.as_mut().filter(|run| run.generation == generation) else { return };
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1002 if run.engine == Engine::Launcher {
1003 // The launcher's log, then its status, which is read when it ends.
1004 if out && (run.json.is_some() || launcher::starts_status(line)) {
1005 run.json.get_or_insert_with(Vec::new).push(line.to_owned());
1006 } else {
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.1007 if let Some(download) = launcher::download(line) {
1008 let changed = {
1009 let shared = self.app.state::<Shared>();
1010 let mut now = shared.now.lock().unwrap();
1011 let changed = now.download.as_ref() != Some(&download);
1012 now.download = Some(download);
1013 changed
1014 };
1015 if changed {
1016 publish(&self.app);
1017 }
1018 }
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1019 self.write_log(line);
1020 }
1021 return;
1022 }
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.1023 let mut became = None;
1024 let mut job = None;
1025 let mut version = None;
1026 match log::read(line) {
1027 log::Event::Listening { version: v } => {
1028 run.listening = true;
1029 version = Some(v);
1030 became = Some(Code::Idle);
1031 }
1032 log::Event::Took { name, repo } => {
1033 run.busy = true;
1034 run.offline_at = None;
1035 job = Some(format!("{name} ({repo})"));
1036 became = Some(Code::Busy);
1037 }
1038 log::Event::Ended => {
1039 run.busy = false;
1040 became = Some(Code::Idle);
1041 }
1042 log::Event::Offline => {
1043 run.offline_at = Some(Instant::now());
1044 if !run.busy {
1045 became = Some(Code::Offline);
1046 }
1047 }
1048 log::Event::Forgotten => run.forgotten = true,
1049 log::Event::TokenRefused => run.token_refused = true,
1050 log::Event::Failed(error) => run.error = Some(error),
1051 log::Event::Other => {}
1052 }
1053 let registered = version.is_some().then(|| Registration {
1054 server: config::server(&self.app).to_string(),
1055 workspace: run.workspace.clone(),
1056 backend: run.backend,
1057 });
1058 self.write_log(line);
1059 if let Some(registration) = registered {
1060 registration.save(&self.app);
1061 self.failures = 0;
1062 }
1063 self.note(job, version);
1064 if let Some(code) = became {
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.1065 let (code, extra) = self.idle(code);
1066 self.set(code, extra);
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.1067 }
1068 }
1069
1070 fn exited(&mut self, generation: u64) {
1071 if self.run.as_ref().is_none_or(|run| run.generation != generation) {
1072 return;
1073 }
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1074 if self.run.as_ref().is_some_and(|run| run.engine == Engine::Launcher) {
1075 return self.launched(generation);
1076 }
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.1077 let mut run = self.run.take().expect("checked above");
1078 let exit_code = match &mut run.process {
1079 Process::Child(child) => child.wait().ok().and_then(|status| status.code()),
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1080 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.1081 };
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1082 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.1083 if run.forgotten || run.token_refused {
1084 // The registration is no more, or was never made: the next start asks for a token.
1085 self.forget();
1086 self.not_before = Instant::now();
1087 return;
1088 }
1089 self.failures += 1;
1090 let wait = Duration::from_secs(15 * 2u64.pow(self.failures.min(6))).min(MOST_BACKOFF);
1091 self.not_before = Instant::now() + wait;
1092 let offline = run.error.as_deref().is_some_and(|e| e.contains("could not reach g1t"));
1093 if offline {
1094 self.set(Code::Offline, Extra::default());
1095 } else if !run.listening && Registration::load(&self.app).is_none() {
1096 self.set(Code::RegisterFailed, Extra { detail: run.error, ..Extra::default() });
1097 } else {
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1098 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.1099 }
1100 }
1101
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1102 /// Stops a run's process, the way it was started: the sandbox `how` says.
1103 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.1104 match (&mut run.process, run.engine) {
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1105 (Process::Sandbox(step), _) => {
1106 // A launcher command still going is ended first, then the sandbox stopped.
1107 if let Some((_, child)) = step.as_mut() {
1108 native::stop(child);
1109 }
1110 let binary = native::binary(&folder(&self.app));
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.1111 // How it is once stopped, so each kind of job's card says so.
1112 let stopped = match launcher::down(&binary, &native_dir(&self.app, Backend::Sandboxed), how) {
1113 Ok(status) => Some(status),
1114 Err(why) => {
1115 self.write_log(&format!("g1t: the sandbox didn't stop cleanly: {why}"));
1116 None
1117 }
1118 };
1119 let shared = self.app.state::<Shared>();
1120 let mut now = shared.now.lock().unwrap();
1121 now.sandbox = stopped.filter(|_| how != Down::Delete);
1122 now.starting.clear();
1123 now.download = None;
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.1124 }
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1125 (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.1126 (Process::Child(child), Engine::Native { docker }) => {
1127 native::stop(child);
1128 // A job's container outlives the runner killed under it: removed now, not at its next start.
1129 if docker {
1130 native::sweep(&native_dir(&self.app, run.backend));
1131 }
1132 }
1133 }
1134 let _ = std::fs::remove_file(folder(&self.app).join("pid"));
1135 }
1136
1137 /// Stops whatever runs now.
1138 fn halt(&mut self) {
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1139 self.halt_as(Down::Now);
1140 }
1141
1142 /// Stops whatever runs: the sandbox now, after its job, or for good.
1143 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.1144 self.generation += 1;
1145 if let Some(mut run) = self.run.take() {
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1146 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.1147 }
1148 }
1149
1150 /// A runner an earlier g1t started and did not stop: it ended without quitting.
1151 fn reap(&self) {
1152 if let Some(pid) = std::fs::read_to_string(folder(&self.app).join("pid")).ok().and_then(|text| text.trim().parse().ok()) {
1153 native::reap(pid);
1154 let _ = std::fs::remove_file(folder(&self.app).join("pid"));
1155 }
1156 }
1157
1158 /// Unregisters the runner the app registered, whatever backend it was.
1159 fn forget(&mut self) {
1160 let app = self.app.clone();
1161 let Some(registration) = Registration::load(&app) else { return };
1162 let binary = native::binary(&folder(&app));
1163 match registration.backend {
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1164 // The sandbox keeps its registration for when it is chosen
1165 // again (sandbox-registration.json says whose): it only stops,
1166 // 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.1167 Backend::Sandboxed => {
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1168 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.1169 }
1170 backend => native::forget(&binary, &native_dir(&app, backend)),
1171 }
1172 Registration::clear(&app);
1173 }
1174
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1175 /// Removes the sandbox and its registration: it was for another
1176 /// workspace, or g1t no longer knows its runners.
1177 fn forget_sandbox(&mut self) {
1178 let binary = native::binary(&folder(&self.app));
1179 if let Err(why) = launcher::down(&binary, &native_dir(&self.app, Backend::Sandboxed), Down::Delete) {
1180 self.write_log(&format!("g1t: the sandbox couldn't be removed: {why}"));
1181 }
1182 SandboxRegistration::clear(&self.app);
1183 if Registration::load(&self.app).is_some_and(|r| r.backend == Backend::Sandboxed) {
1184 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.1185 }
1186 }
1187
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1188 /// Whether what a native runner keeps of a registration is still here.
1189 fn kept(&self, backend: Backend) -> bool {
1190 native_dir(&self.app, backend).join("config.json").exists()
1191 }
1192
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.1193 /// Asks the site for a registration token for `workspace`.
1194 fn want(&mut self, workspace: &str) {
1195 *self.app.state::<Shared>().wanted.lock().unwrap() = Some(workspace.to_owned());
1196 let _ = self.app.emit_to(EventTarget::webview(frame::SITE), WANTED_EVENT, workspace.to_owned());
1197 self.not_before = Instant::now() + ASK_AGAIN;
1198 let signed_out = self.app.state::<Shared>().site.lock().unwrap().signed_in == Some(false);
1199 self.set(if signed_out { Code::SignedOut } else { Code::Registering }, Extra::default());
1200 }
1201
1202 /// The thread's look round: what should run, and whether it does.
1203 fn look(&mut self) {
1204 let settings = config::settings(&self.app);
1205 let paused = *self.app.state::<Shared>().paused.lock().unwrap();
1206 if !settings.runner || paused {
1207 if self.run.is_some() {
1208 self.halt();
1209 }
1210 *self.app.state::<Shared>().wanted.lock().unwrap() = None;
1211 self.set(if paused { Code::Stopped } else { Code::Off }, Extra::default());
1212 return;
1213 }
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.1214 // Looking needs a runner here to compare with; the first comes with the first start.
1215 if Instant::now() >= self.next_update && native::installed(&folder(&self.app)).is_some() {
1216 self.check(false);
1217 }
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.1218 let workspace = workspace(&self.app);
1219 if let Some(run) = &self.run {
1220 let moved = run.backend != settings.runner_backend || Some(&run.workspace) != workspace.as_ref();
1221 let back = run.offline_at.is_some_and(|at| at.elapsed() >= BACK_ONLINE);
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.1222 let (busy, stepping) = (run.busy, 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.1223 if back {
1224 if let Some(run) = self.run.as_mut() {
1225 run.offline_at = None;
1226 }
1227 self.set(Code::Idle, Extra::default());
1228 }
1229 // A change of backend or workspace waits for the job to end, as an update does.
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1230 if moved && !busy && !stepping {
1231 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.1232 self.not_before = Instant::now();
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.1233 } else if !moved && !stepping && !self.install() {
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.1234 self.kinds(workspace.as_deref());
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.1235 }
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1236 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.1237 return;
1238 }
1239 if Instant::now() < self.not_before {
1240 return;
1241 }
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.1242 // Nothing runs: a waiting program goes in before anything starts.
1243 if self.program.is_some()
1244 && self.allowed()
1245 && let Some((version, bytes)) = self.program.take()
1246 {
1247 self.put(version, &bytes);
1248 }
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.1249 self.start(settings.runner_backend, workspace);
1250 }
1251
1252 fn start(&mut self, backend: Backend, workspace: Option<String>) {
1253 let app = self.app.clone();
1254 let engine = match backend::engine(backend) {
1255 Ok(engine) => engine,
1256 Err(missing) => {
1257 self.not_before = Instant::now() + LOOK_AGAIN;
1258 let code = match missing {
1259 Missing::Docker => Code::NoDocker,
1260 };
1261 self.set(code, Extra::default());
1262 return;
1263 }
1264 };
1265 let Some(workspace) = workspace else {
1266 self.not_before = Instant::now() + LOOK_AGAIN;
1267 let code = match app.state::<Shared>().site.lock().unwrap().signed_in {
1268 Some(false) => Code::SignedOut,
1269 Some(true) => Code::NoWorkspace,
1270 None => Code::Starting,
1271 };
1272 self.set(code, detail("Waiting for g1t to open."));
1273 return;
1274 };
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1275 if engine == Engine::Launcher {
1276 return self.start_sandbox(workspace);
1277 }
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.1278 let server = config::server(&app);
1279 let registration = Registration::load(&app);
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1280 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.1281 let mut token = None;
1282 if !ours {
1283 if registration.is_some() {
1284 self.set(Code::Starting, detail("Removing this computer's earlier registration."));
1285 self.forget();
1286 }
1287 match self.token.take() {
1288 Some((for_workspace, given)) if for_workspace == workspace => token = Some(given),
1289 _ => return self.want(&workspace),
1290 }
1291 }
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1292 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.1293 self.set(Code::Starting, Extra::default());
1294 let envs = environment(&server, backend, token.as_deref());
1295 let generation = self.generation + 1;
1296 self.generation = generation;
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.1297 let binary = match self.ensure_binary(&server) {
1298 Ok(binary) => binary,
1299 Err(fault) => return self.not_installed(workspace, token, fault),
1300 };
1301 let started = native::spawn(&binary, &native_dir(&app, backend), &envs).map_err(|e| format!("g1t-runner could not start: {e}")).inspect(|child| {
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1302 let _ = std::fs::write(folder(&app).join("pid"), child.id().to_string());
1303 });
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.1304 match started {
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1305 Ok(mut child) => {
1306 self.follow(generation, &mut child);
1307 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.1308 self.set(Code::Starting, Extra::default());
1309 }
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1310 Err(why) => self.not_started(workspace, token, why),
1311 }
1312 }
1313
1314 /// The runner couldn't be started: the token is kept for the next try,
1315 /// while it lasts, and it is tried again in a while.
1316 fn not_started(&mut self, workspace: String, token: Option<String>, why: String) {
1317 if let Some(given) = token {
1318 self.token = Some((workspace, given));
1319 }
1320 self.failures += 1;
1321 self.not_before = Instant::now() + LOOK_AGAIN;
1322 let offline = why.contains("error sending request") || why.contains("dns") || why.contains("connect");
1323 self.write_log(&format!("g1t: the runner could not start: {why}"));
1324 if offline {
1325 self.set(Code::Offline, Extra::default());
1326 } else {
1327 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.1328 }
1329 }
1330
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1331 /// The setting names the workspace from now on, so browsing another one never moves the runner.
1332 fn hold_workspace(&self, workspace: &str) {
1333 if config::settings(&self.app).runner_workspace.as_deref() != Some(workspace) {
1334 let mut settings = config::settings(&self.app);
1335 settings.runner_workspace = Some(workspace.to_owned());
1336 let _ = config::set_settings(&self.app, settings);
1337 }
1338 }
1339
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.1340 /// Reads the child's output, a line at a time, onto the thread's channel.
1341 fn follow(&self, generation: u64, child: &mut Child) {
1342 fn pipe(tx: Sender<Ask>, generation: u64, from: impl Read + Send + 'static, last: bool) {
1343 std::thread::spawn(move || {
1344 for line in BufReader::new(from).lines().map_while(Result::ok) {
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1345 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.1346 return;
1347 }
1348 }
1349 if last {
1350 let _ = tx.send(Ask::Exited(generation));
1351 }
1352 });
1353 }
1354 if let Some(err) = child.stderr.take() {
1355 pipe(self.tx.clone(), generation, err, false);
1356 }
1357 if let Some(out) = child.stdout.take() {
1358 pipe(self.tx.clone(), generation, out, true);
1359 }
1360 }
1361
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.1362 /// `g1t-runner` installed, from the newest release when it is not:
1363 /// one g1t signed, as every update is.
1364 fn ensure_binary(&mut self, server: &Url) -> Result<PathBuf, Fault> {
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.1365 let folder = folder(&self.app);
1366 let binary = native::binary(&folder);
1367 if native::installed(&folder).is_some() {
1368 return Ok(binary);
1369 }
1370 self.set(Code::Updating, detail("Downloading g1t-runner."));
1371 let manifest = native::latest(server)?;
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.1372 let bytes = native::download(server, &manifest).map_err(Fault::Failed)?;
1373 native::put(&folder, &bytes, &manifest.version).map_err(Fault::Failed)?;
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.1374 self.note(None, Some(manifest.version));
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.1375 self.with_updates(|updates| updates.checked_at = Some(SystemTime::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.1376 Ok(binary)
1377 }
1378
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.1379 /// There is no `g1t-runner` here, and none could be had: a release g1t
1380 /// didn't sign is refused, and asked for again in a while.
1381 fn not_installed(&mut self, workspace: String, token: Option<String>, fault: Fault) {
1382 match fault {
1383 Fault::Failed(why) => self.not_started(workspace, token, why),
1384 Fault::Unsigned(why) => {
1385 if let Some(given) = token {
1386 self.token = Some((workspace, given));
1387 }
1388 self.write_log(&format!("g1t: g1t-runner wasn't installed: {why}"));
1389 self.not_before = Instant::now() + MOST_BACKOFF;
1390 self.set(Code::UpdateRefused, detail(why));
1391 }
1392 }
1393 }
1394
1395 // --- Updates ------------------------------------------------------------------
1396
1397 /// Looks for a newer release on a thread of its own, so the runner is
1398 /// looked after meanwhile: its signed manifest, and its program when it
1399 /// is newer than the one here. `asked` is the person asking, which
1400 /// works with "Download updates by themselves" off.
1401 fn check(&mut self, asked: 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.1402 self.next_update = Instant::now() + UPDATE_EVERY;
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.1403 if !asked && !config::settings(&self.app).auto_update {
1404 return;
1405 }
1406 if self.with_updates(|updates| std::mem::replace(&mut updates.checking, true)) {
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.1407 return;
1408 }
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.1409 self.asked |= asked;
1410 publish(&self.app);
1411 let (server, folder) = (config::server(&self.app), folder(&self.app));
1412 let waiting = self.program.as_ref().map(|(version, _)| version.clone());
1413 let tx = self.tx.clone();
1414 let spawned = std::thread::Builder::new().name("g1t-runner-releases".into()).spawn(move || {
1415 let _ = tx.send(Ask::Checked(look_for_release(&server, &folder, waiting.as_deref())));
1416 });
1417 if spawned.is_err() {
1418 self.with_updates(|updates| updates.checking = false);
1419 }
1420 }
1421
1422 /// What the look found: a newer program waits for its kinds of job to
1423 /// be idle, and so does the sandbox's image when it is older than the
1424 /// release's. A release g1t didn't sign, or one that didn't download,
1425 /// leaves the runner as it is, and is said while it is idle.
1426 fn checked(&mut self, found: Result<Found, Fault>) {
1427 self.with_updates(|updates| updates.checking = false);
1428 let found = match found {
1429 Ok(found) => found,
1430 Err(fault) => {
1431 let failed = match fault {
1432 Fault::Unsigned(why) => (Code::UpdateRefused, why),
1433 Fault::Failed(why) => (Code::UpdateFailed, format!("g1t-runner couldn't be updated: {why}")),
1434 };
1435 self.write_log(&format!("g1t: {}", failed.1));
1436 self.update_failed = Some(failed);
1437 self.asked = false;
1438 if matches!(self.code(), Code::Idle | Code::UpdateFailed | Code::UpdateRefused) {
1439 let (code, extra) = self.idle(Code::Idle);
1440 self.set(code, extra);
1441 }
1442 return publish(&self.app);
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1443 }
1444 };
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.1445 if let Some(bytes) = found.program {
1446 self.program = Some((found.version.clone(), bytes));
1447 }
1448 if !found.newer {
1449 self.program = None;
1450 }
1451 // The sandbox's image is the release's version; one older takes the newest.
1452 let sandbox = self.app.state::<Shared>().now.lock().unwrap().sandbox.clone();
1453 let launcher = self.run.as_ref().is_some_and(|run| run.engine == Engine::Launcher);
1454 let image = launcher
1455 && found.images
1456 && sandbox.is_some_and(|status| status.linux().state != State::Absent && status.image_version.as_deref().is_some_and(|have| native::newer(&found.version, have)));
1457 let program = self.program.is_some();
1458 self.with_updates(|updates| {
1459 updates.checked_at = Some(SystemTime::now());
1460 updates.program = program;
1461 updates.image = image;
1462 updates.ready = (program || image).then(|| found.version.clone());
1463 });
1464 if program || image {
1465 self.write_log(&format!("g1t: g1t-runner {} is ready, and installs as each kind of job is idle.", found.version));
1466 } else {
1467 self.asked = false;
1468 }
1469 // It worked: what the last failure said goes.
1470 if self.update_failed.take().is_some() && matches!(self.code(), Code::UpdateFailed | Code::UpdateRefused) {
1471 self.set(Code::Idle, Extra::default());
1472 }
1473 publish(&self.app);
1474 }
1475
1476 /// Whether a waiting update may go in: "Download updates by
1477 /// themselves" is on, or the person asked for it. Turned off since it
1478 /// downloaded, it no longer waits.
1479 fn allowed(&mut self) -> bool {
1480 if self.asked || config::settings(&self.app).auto_update {
1481 return true;
1482 }
1483 self.program = None;
1484 self.with_updates(|updates| {
1485 updates.program = false;
1486 updates.image = false;
1487 updates.ready = None;
1488 });
1489 false
1490 }
1491
1492 /// Puts a waiting update in, kind by kind, as each kind of job is idle:
1493 /// true when it started something, which is waited for. Never in the
1494 /// middle of a job.
1495 fn install(&mut self) -> bool {
1496 let image = self.with_updates(|updates| updates.image);
1497 if (self.program.is_none() && !image) || !self.allowed() {
1498 return false;
1499 }
1500 let Some((engine, busy, listening)) = self.run.as_ref().map(|run| (run.engine, run.busy, run.listening)) else { return false };
1501 if engine != Engine::Launcher {
1502 // A runner in Docker or on this computer is the program: stopped between jobs, started on the new one.
1503 if busy || !listening {
1504 return false;
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1505 }
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.1506 let Some((version, bytes)) = self.program.take() else { return false };
1507 self.set(Code::Updating, detail(format!("Updating g1t-runner to {version}.")));
1508 self.halt();
1509 self.put(version, &bytes);
1510 self.not_before = Instant::now();
1511 return true;
1512 }
1513 let Some(status) = self.app.state::<Shared>().now.lock().unwrap().sandbox.clone() else { return false };
1514 if self.program.is_some() {
1515 let running = native_kinds(&status);
1516 if running.is_empty() {
1517 // Nothing runs on the program: it is replaced now, and
1518 // kinds() starts what was drained on the new one first,
1519 // before the image, which takes minutes, is updated.
1520 let (version, bytes) = self.program.take().expect("checked above");
1521 self.put(version, &bytes);
1522 return false;
1523 } else if !status.per_kind() {
1524 // A launcher from before kinds of job stopped on their own: the whole sandbox stops between jobs.
1525 if busy {
1526 return false;
1527 }
1528 let (version, bytes) = self.program.take().expect("checked above");
1529 self.set(Code::Updating, detail(format!("Updating g1t-runner to {version}.")));
1530 self.halt_as(Down::Drain);
1531 self.put(version, &bytes);
1532 self.with_updates(|updates| updates.image = true);
1533 self.not_before = Instant::now();
1534 return true;
1535 } else if let Some(idle) = running.iter().find(|kind| kind.job.is_none()) {
1536 // Stopped after any job it takes meanwhile; replaced once it has.
1537 let kind = idle.id.clone();
1538 self.with_updates(|updates| updates.installing = vec![kind.clone()]);
1539 self.kind_step(Step::Drain(kind), None);
1540 return true;
1541 }
1542 }
1543 // Linux jobs take the image once they have no job (`update` drains them itself too).
1544 let image = self.with_updates(|updates| updates.image);
1545 if image && status.linux().job.is_none() {
1546 self.with_updates(|updates| updates.installing.push("linux".into()));
1547 self.sandbox_step(Step::Update);
1548 return true;
1549 }
1550 false
1551 }
1552
1553 /// Puts a downloaded program in place, with nothing running on it.
1554 fn put(&mut self, version: String, bytes: &[u8]) {
1555 match native::put(&folder(&self.app), bytes, &version) {
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1556 Ok(()) => {
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.1557 self.write_log(&format!("g1t: g1t-runner {version} is in place."));
1558 if self.run.as_ref().is_none_or(|run| run.engine != Engine::Launcher) {
1559 self.note(None, Some(version));
1560 }
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1561 }
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.1562 Err(why) => {
1563 let why = format!("g1t-runner {version} couldn't be put in place: {why}");
1564 self.write_log(&format!("g1t: {why}"));
1565 self.update_failed = Some((Code::UpdateFailed, why));
1566 }
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1567 }
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.1568 self.done_with(|updates| {
1569 updates.program = false;
1570 updates.installing.retain(|kind| kind == "linux");
1571 });
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1572 }
1573
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.1574 /// Part of an update is done: what waits is said again.
1575 fn done_with(&mut self, change: impl FnOnce(&mut Updates)) {
1576 let left = self.with_updates(|updates| {
1577 change(updates);
1578 if !updates.program && !updates.image {
1579 updates.ready = None;
1580 }
1581 updates.program || updates.image
1582 });
1583 if !left {
1584 self.asked = false;
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1585 }
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.1586 publish(&self.app);
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1587 }
1588
1589 // --- The sandbox (launcher.rs) ------------------------------------------------
1590
1591 /// Starts the sandbox for `workspace`: registered with it the first
1592 /// time, with a token from the site; from its own registration after.
1593 fn start_sandbox(&mut self, workspace: String) {
1594 let app = self.app.clone();
1595 let server = config::server(&app);
1596 let binary = match self.ensure_binary(&server) {
1597 Ok(binary) => binary,
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.1598 Err(fault) => return self.not_installed(workspace, None, fault),
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1599 };
1600 let dir = native_dir(&app, Backend::Sandboxed);
1601 let mut status = match launcher::status(&binary, &dir) {
1602 Ok(status) => status,
1603 Err(why) => return self.no_status(why),
1604 };
1605 self.seen(&status);
1606 // Nothing here can be sandboxed: why, and how to fix it; looked at again in a while.
1607 if !status.anything_available() {
1608 self.not_before = Instant::now() + LOOK_AGAIN;
1609 let (code, extra) = shown(&status);
1610 return self.set(code, extra);
1611 }
1612 // Another backend's registration goes; the sandbox's own is kept for its workspace.
1613 if Registration::load(&app).is_some_and(|r| r.backend != Backend::Sandboxed) {
1614 self.set(Code::Starting, detail("Removing this computer's earlier registration."));
1615 self.forget();
1616 }
1617 let kept = SandboxRegistration::load(&app);
1618 let ours = kept.as_ref().is_some_and(|k| k.server == server.as_str() && k.workspace == workspace);
1619 let made = status.capabilities.iter().any(|c| c.state != State::Absent) || status.state != State::Absent;
1620 if !ours && made && kept.is_none() {
1621 // Made by hand (`g1t-runner sandbox up`), and there is one per computer: it is the person's to remove.
1622 self.not_before = Instant::now() + LOOK_AGAIN;
1623 return self.set(Code::SandboxTaken, Extra::default());
1624 }
1625 if !ours && made {
1626 // The app's sandbox, registered with another workspace or server.
1627 self.set(Code::Starting, detail("Removing this computer's sandbox for another workspace."));
1628 self.forget_sandbox();
1629 status = match launcher::status(&binary, &dir) {
1630 Ok(status) => status,
1631 Err(why) => return self.no_status(why),
1632 };
1633 }
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.1634 // The kinds of job the person stopped are stopped here too, before
1635 // anything starts (the launcher keeps them off); with every kind
1636 // stopped, nothing starts.
1637 let kinds = config::settings(&app).runner_capabilities;
1638 if status.per_kind() {
1639 let stopping: Vec<String> = status.capabilities.iter().filter(|c| !kinds.on(&c.id) && !c.off()).map(|c| c.id.clone()).collect();
1640 for kind in &stopping {
1641 if let Err(why) = launcher::down_only(&binary, &dir, kind) {
1642 self.write_log(&format!("g1t: {} couldn't be stopped: {why}", diagnosis::kind_label(kind)));
1643 }
1644 }
1645 if !stopping.is_empty() {
1646 status = match launcher::status(&binary, &dir) {
1647 Ok(status) => status,
1648 Err(why) => return self.no_status(why),
1649 };
1650 self.seen(&status);
1651 }
1652 }
1653 let wanted: Vec<String> = status.capabilities.iter().filter(|c| c.available && kinds.on(&c.id)).map(|c| c.id.clone()).collect();
1654 if status.capabilities.iter().any(|c| c.available) && wanted.is_empty() {
1655 self.not_before = Instant::now() + LOOK_AGAIN;
1656 return self.set(Code::KindsStopped, Extra::default());
1657 }
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1658 let mut token = None;
1659 if !ours || !kept.as_ref().is_some_and(|k| k.registered) || status.unregistered() {
1660 match self.token.take() {
1661 Some((for_workspace, given)) if for_workspace == workspace => token = Some(given),
1662 _ => return self.want(&workspace),
1663 }
1664 SandboxRegistration { server: server.to_string(), workspace: workspace.clone(), registered: false }.save(&app);
1665 }
1666 self.hold_workspace(&workspace);
1667 let generation = self.generation + 1;
1668 self.generation = generation;
1669 self.run = Some(Run::new(generation, Engine::Launcher, Backend::Sandboxed, workspace.clone(), Process::Sandbox(None), token.is_some()));
1670 // A new release's image first, then the sandbox starts again.
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.1671 let image = self.with_updates(|updates| std::mem::take(&mut updates.image));
1672 let step = if image && status.linux().state != State::Absent { Step::Update } else { Step::Up };
1673 if step == Step::Update {
1674 self.with_updates(|updates| updates.installing.push("linux".into()));
1675 } else if image {
1676 self.done_with(|_| {});
1677 }
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1678 let name = backend::machine_name();
1679 let url = server.as_str().trim_end_matches('/').to_owned();
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.1680 let up = launcher::Up { url: &url, name: &name, labels: "desktop", token: token.as_deref(), only: wanted.iter().map(String::as_str).collect() };
1681 let (args, starting) = match step {
1682 Step::Update => (launcher::update_args(&dir), vec![]),
1683 _ => (launcher::up_args(&dir, &up), wanted.clone()),
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1684 };
A registration token never shows in the process list. The desktop app passed it to g1t-runner sandbox up as --token, which anyone on the computer can read while up runs; it goes in up's environment now, as G1T_RUNNER_TOKEN, which the launcher reads when --token is left out, as register-and-run already did.1685 if let Err(why) = self.spawn_step(step, args, up.token) {
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1686 self.run = None;
1687 return self.not_started(workspace, token, why);
1688 }
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.1689 self.now_starting(starting);
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1690 self.set(Code::Starting, detail(STARTING_SANDBOX));
1691 }
1692
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.1693 /// Says which kinds of job a launcher command is starting now.
1694 fn now_starting(&self, kinds: Vec<String>) {
1695 let changed = {
1696 let shared = self.app.state::<Shared>();
1697 let mut now = shared.now.lock().unwrap();
1698 let changed = now.starting != kinds;
1699 now.starting = kinds;
1700 changed
1701 };
1702 if changed {
1703 publish(&self.app);
1704 }
1705 }
1706
1707 /// Each kind of job as the person has it: one they stopped is stopped
1708 /// (`down --only`), one they started is started (`up --only`), and one
1709 /// that should run and doesn't while another runs is tried again in a
1710 /// while: every five minutes while the release has no sandbox images.
1711 /// One launcher command at a time, each leaving the others as they are.
1712 fn kinds(&mut self, workspace: Option<&str>) {
1713 if !self.run.as_ref().is_some_and(|run| run.engine == Engine::Launcher) {
1714 return;
1715 }
1716 let Some(status) = self.app.state::<Shared>().now.lock().unwrap().sandbox.clone() else { return };
1717 if !status.per_kind() {
1718 return;
1719 }
1720 let on = config::settings(&self.app).runner_capabilities;
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.1721 // A kind of job drained for a new program starts on the new one, once it is in.
1722 let replacing = self.program.is_some();
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.1723 for capability in &status.capabilities {
1724 if !on.on(&capability.id) && !capability.off() {
1725 return self.kind_step(Step::Stop(capability.id.clone()), None);
1726 }
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.1727 if on.on(&capability.id) && capability.off() && capability.available && !(replacing && capability.id != "linux") {
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.1728 return self.start_kind(capability, workspace);
1729 }
1730 }
1731 // Nothing runs: the sandbox as a whole starts again (sandbox_stopped).
1732 if !status.running() {
1733 self.retry.clear();
1734 return;
1735 }
1736 let now = Instant::now();
1737 for capability in &status.capabilities {
1738 let id = capability.id.as_str();
1739 let wanting = on.on(id) && !capability.off() && capability.available && matches!(capability.state, State::Error | State::Stopped | State::Absent);
1740 if !wanting {
1741 self.retry.retain(|(kind, _, _)| kind != id);
1742 continue;
1743 }
1744 let images = id == "linux" && status.trouble().is_some_and(|(cause, _)| launcher::why(&cause) == launcher::Why::NoImages);
1745 match self.retry.iter().position(|(kind, _, _)| kind == id) {
1746 // Just seen not running: tried again in a while.
1747 None => self.retry.push((id.to_owned(), now + again(images, 1), 1)),
1748 Some(at) if now >= self.retry[at].1 => {
1749 let tries = self.retry[at].2 + 1;
1750 self.retry[at] = (id.to_owned(), now + again(images, tries), tries);
1751 self.write_log(&format!("g1t: trying {} again.", diagnosis::kind_label(id)));
1752 return self.start_kind(capability, workspace);
1753 }
1754 Some(_) => {}
1755 }
1756 }
1757 }
1758
1759 /// Starts one kind of job; one never registered needs a token from the
1760 /// site first, which is asked for, and it starts once it is here.
1761 fn start_kind(&mut self, capability: &launcher::Capability, workspace: Option<&str>) {
1762 let mut token = None;
1763 if capability.state == State::Absent {
1764 let Some(workspace) = workspace else { return };
1765 match self.token.take() {
1766 Some((for_workspace, given)) if for_workspace == workspace => token = Some(given),
1767 other => {
1768 self.token = other;
1769 if wanted(&self.app).is_none() {
1770 *self.app.state::<Shared>().wanted.lock().unwrap() = Some(workspace.to_owned());
1771 let _ = self.app.emit_to(EventTarget::webview(frame::SITE), WANTED_EVENT, workspace.to_owned());
1772 }
1773 return;
1774 }
1775 }
1776 }
1777 self.kind_step(Step::Start(capability.id.clone()), token);
1778 }
1779
1780 /// `up --only` or `down --only` one kind of job, while the rest run on.
1781 fn kind_step(&mut self, step: Step, token: Option<String>) {
1782 let dir = native_dir(&self.app, Backend::Sandboxed);
1783 let (args, starting, said) = match &step {
1784 Step::Start(kind) => {
1785 let url = config::server(&self.app).as_str().trim_end_matches('/').to_owned();
1786 let name = backend::machine_name();
1787 let up = launcher::Up { url: &url, name: &name, labels: "desktop", token: token.as_deref(), only: vec![kind.as_str()] };
1788 (launcher::up_args(&dir, &up), vec![kind.clone()], format!("starting {}", diagnosis::kind_label(kind)))
1789 }
1790 Step::Stop(kind) => (launcher::down_only_args(&dir, kind), vec![], format!("stopping {}", diagnosis::kind_label(kind))),
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.1791 Step::Drain(kind) => (launcher::drain_only_args(&dir, kind), vec![], format!("stopping {} after their job, to update g1t-runner", diagnosis::kind_label(kind))),
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.1792 Step::Up | Step::Update => return,
1793 };
1794 self.write_log(&format!("g1t: {said}."));
1795 match self.spawn_step(step, args, token.as_deref()) {
1796 Ok(()) => self.now_starting(starting),
1797 Err(why) => self.write_log(&format!("g1t: {why}")),
1798 }
1799 }
1800
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1801 /// `sandbox status` didn't answer: a `g1t-runner` from before its
1802 /// launcher, which a newer release replaces, or one that failed.
1803 fn no_status(&mut self, why: String) {
1804 self.write_log(&format!("g1t: the sandbox's status: {why}"));
1805 self.failures += 1;
1806 if why.contains("older than its sandbox launcher") {
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.1807 let (server, folder) = (config::server(&self.app), folder(&self.app));
1808 if let Ok(Found { version, program: Some(bytes), .. }) = look_for_release(&server, &folder, None)
1809 && native::put(&folder, &bytes, &version).is_ok()
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1810 {
1811 self.note(None, Some(version));
1812 self.not_before = Instant::now();
1813 return;
1814 }
1815 self.not_before = Instant::now() + MOST_BACKOFF;
1816 return self.set(Code::NoLauncher, detail(why));
1817 }
1818 self.not_before = Instant::now() + LOOK_AGAIN;
1819 self.set(Code::SandboxFailed, Extra { detail: Some(why), wait_seconds: Some(LOOK_AGAIN.as_secs()), ..Extra::default() });
1820 }
1821
1822 /// Runs a launcher command for the sandbox's run, reading its output.
A registration token never shows in the process list. The desktop app passed it to g1t-runner sandbox up as --token, which anyone on the computer can read while up runs; it goes in up's environment now, as G1T_RUNNER_TOKEN, which the launcher reads when --token is left out, as register-and-run already did.1823 fn spawn_step(&mut self, step: Step, args: Vec<std::ffi::OsString>, token: Option<&str>) -> Result<(), String> {
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1824 let binary = native::binary(&folder(&self.app));
A registration token never shows in the process list. The desktop app passed it to g1t-runner sandbox up as --token, which anyone on the computer can read while up runs; it goes in up's environment now, as G1T_RUNNER_TOKEN, which the launcher reads when --token is left out, as register-and-run already did.1825 let mut command = launcher::command(&binary, args);
1826 launcher::with_token(&mut command, token);
1827 let mut child = command.spawn().map_err(|e| format!("g1t-runner sandbox {} could not start: {e}", step.name()))?;
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1828 let _ = std::fs::write(folder(&self.app).join("pid"), child.id().to_string());
1829 let Some(generation) = self.run.as_ref().map(|run| run.generation) else {
1830 native::stop(&mut child);
1831 return Ok(());
1832 };
1833 self.follow(generation, &mut child);
1834 let run = self.run.as_mut().expect("checked above");
1835 run.json = None;
1836 run.process = Process::Sandbox(Some((step, child)));
1837 Ok(())
1838 }
1839
1840 /// `update` on the running sandbox, while it has no job.
1841 fn sandbox_step(&mut self, step: Step) {
1842 if self.run.as_ref().is_none_or(|run| run.step().is_some()) {
1843 return;
1844 }
1845 let dir = native_dir(&self.app, Backend::Sandboxed);
A registration token never shows in the process list. The desktop app passed it to g1t-runner sandbox up as --token, which anyone on the computer can read while up runs; it goes in up's environment now, as G1T_RUNNER_TOKEN, which the launcher reads when --token is left out, as register-and-run already did.1846 if let Err(why) = self.spawn_step(step, launcher::update_args(&dir), None) {
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1847 self.write_log(&format!("g1t: {why}"));
1848 }
1849 }
1850
1851 /// Asks the sandbox how it is, every few seconds, on a thread of its own.
1852 fn poll(&mut self) {
1853 let Some(run) = self.run.as_mut().filter(|run| run.engine == Engine::Launcher) else { return };
1854 if run.polling || run.polled_at.is_some_and(|at| at.elapsed() < launcher::STATUS_EVERY) {
1855 return;
1856 }
1857 run.polling = true;
1858 run.polled_at = Some(Instant::now());
1859 let (tx, generation) = (self.tx.clone(), run.generation);
1860 let binary = native::binary(&folder(&self.app));
1861 let dir = native_dir(&self.app, Backend::Sandboxed);
1862 let spawned = std::thread::Builder::new().name("g1t-sandbox-status".into()).spawn(move || {
1863 let _ = tx.send(Ask::Sandbox(generation, launcher::status(&binary, &dir).map(Box::new)));
1864 });
1865 if spawned.is_err() {
1866 run.polling = false;
1867 }
1868 }
1869
1870 /// What a poll said.
1871 fn polled(&mut self, generation: u64, status: Result<Box<Status>, String>) {
1872 let Some(run) = self.run.as_mut().filter(|run| run.generation == generation) else { return };
1873 run.polling = false;
1874 if std::mem::take(&mut run.stale) {
1875 return;
1876 }
1877 match status {
1878 Ok(status) => self.sandbox_now(*status),
1879 // Once is nothing: the next poll asks again.
1880 Err(why) => self.write_log(&format!("g1t: the sandbox's status: {why}")),
1881 }
1882 }
1883
1884 /// Keeps what a status says for the tray, settings and the site.
1885 fn seen(&mut self, status: &Status) {
1886 let shared = self.app.state::<Shared>();
1887 let mut now = shared.now.lock().unwrap();
1888 now.sandbox = Some(status.clone());
1889 if let Some(version) = status.version() {
1890 now.version = Some(version);
1891 }
1892 now.job = status.job().map(launcher::Job::said);
1893 }
1894
1895 /// The sandbox as it is now, while it is the run.
1896 fn sandbox_now(&mut self, status: Status) {
1897 let had = self.app.state::<Shared>().now.lock().unwrap().job.clone();
1898 let Some(run) = self.run.as_mut() else { return };
1899 let step = run.step();
1900 let job = status.job().map(launcher::Job::said);
1901 let was = had.filter(|_| run.busy);
1902 run.busy = job.is_some();
1903 run.listening = status.running();
1904 let update_failed = self.update_failed.clone();
1905 if was != job {
1906 if let Some(ended) = &was {
1907 self.write_log(&format!("g1t: {ended} ended."));
1908 }
1909 if let Some(took) = &job {
1910 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.1911 }
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1912 }
1913 self.seen(&status);
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.1914 match &step {
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1915 Some(Step::Up) => return self.set(Code::Starting, detail(STARTING_SANDBOX)),
1916 Some(Step::Update) if status.state == State::Updating => return self.set(Code::Updating, detail("Updating the sandbox to the newest image.")),
1917 _ => {}
1918 }
1919 let (code, extra) = shown(&status);
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.1920 // Every kind of job stopped by the person: nothing to start again.
1921 if step.is_none() && status.all_off() && !status.running() {
1922 self.run = None;
1923 self.generation += 1;
1924 self.retry.clear();
1925 self.not_before = Instant::now() + LOOK_AGAIN;
1926 return self.set(Code::KindsStopped, Extra::default());
1927 }
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1928 if step.is_none() && !status.running() && !matches!(status.state, State::Starting | State::Updating) {
1929 return self.sandbox_stopped(&status, code, extra);
1930 }
1931 match (code, update_failed) {
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.1932 (Code::Idle, Some((failed, why))) => self.set(failed, detail(why)),
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1933 (code, _) => self.set(code, extra),
1934 }
1935 }
1936
1937 /// The sandbox stopped by itself, or never got going: started again,
1938 /// waiting longer each time, or registered again when that is why.
1939 fn sandbox_stopped(&mut self, status: &Status, code: Code, extra: Extra) {
1940 let error = status.error.clone().or_else(|| extra.detail.clone()).unwrap_or_default();
1941 if self.registration_trouble(&error) {
1942 return;
1943 }
1944 self.run = None;
1945 self.generation += 1;
1946 self.failures += 1;
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.1947 // A release without the sandbox's images is looked at again every few minutes.
1948 let wait = if code == Code::WaitingForImage { launcher::IMAGES_AGAIN } else { again(false, self.failures) };
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1949 self.not_before = Instant::now() + wait;
1950 let (code, extra) = match code {
1951 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() }),
1952 code => (code, extra),
1953 };
1954 self.set(code, Extra { wait_seconds: Some(wait.as_secs()), ..extra });
1955 }
1956
1957 /// When the sandbox's trouble is its registration: true when the app
1958 /// has seen to it, and the next start registers again.
1959 fn registration_trouble(&mut self, error: &str) -> bool {
1960 match launcher::why(error) {
1961 launcher::Why::TokenRefused | launcher::Why::Unregistered => {
1962 if let Some(mut kept) = SandboxRegistration::load(&self.app) {
1963 kept.registered = false;
1964 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.1965 }
1966 self.halt();
1967 self.not_before = Instant::now();
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1968 true
1969 }
1970 launcher::Why::Forgotten => {
1971 self.halt();
1972 self.forget_sandbox();
1973 self.not_before = Instant::now();
1974 true
1975 }
1976 _ => false,
1977 }
1978 }
1979
1980 /// A launcher command ended: what it printed last is the sandbox's status.
1981 fn launched(&mut self, generation: u64) {
1982 let Some(run) = self.run.as_mut().filter(|run| run.generation == generation) else { return };
1983 let Process::Sandbox(slot) = &mut run.process else { return };
1984 let Some((step, mut child)) = slot.take() else { return };
1985 let worked = child.wait().is_ok_and(|status| status.success());
1986 let _ = std::fs::remove_file(folder(&self.app).join("pid"));
1987 let printed = run.json.take().unwrap_or_default().join("\n");
1988 run.stale = run.polling;
1989 let with_token = run.with_token;
1990 let status = launcher::parse(&printed);
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.1991 {
1992 let shared = self.app.state::<Shared>();
1993 let mut now = shared.now.lock().unwrap();
1994 now.starting.clear();
1995 now.download = None;
1996 }
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1997 let error = match (&status, worked) {
1998 (Ok(status), true) => status.error.clone().filter(|_| !status.running()),
1999 (Ok(status), false) => Some(status.error.clone().unwrap_or_else(|| format!("g1t-runner sandbox {} failed.", step.name()))),
2000 (Err(why), _) => Some(why.clone()),
2001 };
2002 let status = status.ok();
2003 let running = status.as_ref().is_some_and(Status::running);
2004 if let Some(error) = &error {
2005 self.write_log(&format!("g1t: sandbox {}: {error}", step.name()));
2006 }
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.2007 // Drained for a new program, which goes in on the next look; one
2008 // that didn't stop leaves the update for the next one, rather than
2009 // being asked again and again.
2010 if let (Step::Drain(_), Some(error)) = (&step, &error) {
2011 self.program = None;
2012 self.update_failed = Some((Code::UpdateFailed, format!("g1t-runner couldn't be updated: {error}")));
2013 self.done_with(|updates| {
2014 updates.program = false;
2015 updates.installing.retain(|kind| kind == "linux");
2016 });
2017 }
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.2018 // One kind of job started or stopped: how the sandbox is now says
2019 // the rest, and one that didn't start is tried again in a while.
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.2020 if matches!(step, Step::Start(_) | Step::Stop(_) | Step::Drain(_)) {
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.2021 if let Some(status) = status {
2022 self.sandbox_now(status);
2023 }
2024 return;
2025 }
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.2026 // Registered: from now on it starts with no token.
2027 if step == Step::Up && with_token && (error.is_none() || running) {
2028 let app = self.app.clone();
2029 if let Some(run) = self.run.as_mut() {
2030 run.with_token = false;
2031 let registration = Registration { server: config::server(&app).to_string(), workspace: run.workspace.clone(), backend: Backend::Sandboxed };
2032 SandboxRegistration { server: registration.server.clone(), workspace: registration.workspace.clone(), registered: true }.save(&app);
2033 registration.save(&app);
2034 }
2035 }
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.2036 // The image is taken, or failed to be: the next look says whether another waits.
2037 if step == Step::Update {
2038 self.done_with(|updates| {
2039 updates.image = false;
2040 updates.installing.retain(|kind| kind != "linux");
2041 });
2042 }
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.2043 if let Some(error) = error.clone() {
2044 if self.registration_trouble(&error) {
2045 return;
2046 }
2047 if step == Step::Update {
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.2048 self.update_failed = Some((Code::UpdateFailed, format!("The sandbox's image couldn't be updated: {error}")));
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.2049 } else if !running {
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.2050 // Nothing runs: tidied away, and tried again in a while;
2051 // a release without the sandbox's images every few minutes.
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.2052 self.halt();
2053 self.failures += 1;
2054 let (code, cause, fix) = status
2055 .as_ref()
2056 .and_then(diagnosis::sandbox_trouble)
2057 .unwrap_or_else(|| (diagnosis::sandbox_code(launcher::why(&error)), error.clone(), None));
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.2058 let wait = if code == Code::WaitingForImage { launcher::IMAGES_AGAIN } else { again(false, self.failures) };
2059 self.not_before = Instant::now() + wait;
2060 // The cards say why from what `up` said, not from the stopped sandbox.
2061 if let Some(failed) = status {
2062 self.app.state::<Shared>().now.lock().unwrap().sandbox = Some(failed);
2063 }
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.2064 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.2065 }
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.2066 } else if step == Step::Up {
2067 self.failures = 0;
2068 } else {
2069 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.2070 }
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.2071 if step == Step::Update && !running {
2072 // Updated while stopped: it starts again with the new image.
2073 self.run = None;
2074 self.generation += 1;
2075 self.not_before = Instant::now();
2076 return;
2077 }
2078 if let Some(status) = status {
2079 self.sandbox_now(status);
2080 }
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.2081 }
2082}
2083
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.2084/// What the tray says while the sandbox starts.
2085const STARTING_SANDBOX: &str = "Starting the sandbox. The first time, it downloads and checks the sandbox's image, which takes a few minutes.";
2086
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.2087/// The newest release, signed, and its program when it is newer than the
2088/// one in `folder` and isn't already `waiting`: downloaded and checked
2089/// against its SHA-256. On the update thread, never the runner's.
2090fn look_for_release(server: &Url, folder: &std::path::Path, waiting: Option<&str>) -> Result<Found, Fault> {
2091 let manifest = native::latest(server)?;
2092 let newer = native::installed(folder).is_none_or(|have| native::newer(&manifest.version, &have));
2093 let program = if newer && waiting != Some(manifest.version.as_str()) {
2094 Some(native::download(server, &manifest).map_err(|why| Fault::Failed(format!("{} didn't download: {why}", manifest.version)))?)
2095 } else {
2096 None
2097 };
2098 Ok(Found { images: manifest.has_images(), version: manifest.version, newer, program })
2099}
2100
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.2101fn some_text(text: &str) -> Option<String> {
2102 Some(text.to_owned())
2103}
2104
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.2105/// How long before trying again, the `tries`th time: every few minutes
2106/// while the release has no sandbox images, else longer each time.
2107fn again(images: bool, tries: u32) -> Duration {
2108 if images { launcher::IMAGES_AGAIN } else { Duration::from_secs(15 * 2u64.pow(tries.min(6))).min(MOST_BACKOFF) }
2109}
2110
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.2111/// What the app shows for how the sandbox is, with no launcher command
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.2112/// running: what its runners do while one runs (each kind of job's card
2113/// says the rest), else what is wrong with Linux jobs.
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.2114fn shown(status: &Status) -> (Code, Extra) {
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.2115 if status.job().is_some() {
2116 return (Code::Busy, Extra::default());
2117 }
2118 if status.running() {
2119 return (Code::Idle, Extra::default());
2120 }
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.2121 if let Some((code, cause, fix)) = diagnosis::sandbox_trouble(status) {
2122 return (code, Extra { detail: Some(cause), fix, ..Extra::default() });
2123 }
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.2124 match status.linux().state {
2125 State::Starting => (Code::Starting, detail(STARTING_SANDBOX)),
2126 State::Updating => (Code::Updating, detail("Updating the sandbox to the newest image.")),
2127 _ if status.all_off() => (Code::KindsStopped, Extra::default()),
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.2128 _ => (Code::SandboxFailed, detail("The sandbox isn't running.")),
2129 }
2130}
2131
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.2132/// What `register-and-run` reads: where g1t is, the runner's name and
2133/// labels, and the token, the first time only. The app updates the runner,
2134/// so the runner does not update itself; registering again takes the
2135/// place of this computer's earlier runner of the same name.
2136fn environment(server: &Url, backend: Backend, token: Option<&str>) -> Vec<(&'static str, String)> {
2137 let mut envs = vec![
2138 ("G1T_RUNNER_URL", server.as_str().trim_end_matches('/').to_owned()),
2139 ("G1T_RUNNER_NAME", backend::machine_name()),
2140 ("G1T_RUNNER_LABELS", "desktop".to_owned()),
2141 ("G1T_RUNNER_REPLACE", "1".to_owned()),
2142 ("G1T_RUNNER_NO_AUTO_UPDATE", "1".to_owned()),
2143 ];
2144 if backend == Backend::Host {
2145 envs.push(("G1T_RUNNER_NO_DOCKER", "1".to_owned()));
2146 }
2147 if let Some(token) = token {
2148 envs.push(("G1T_RUNNER_TOKEN", token.to_owned()));
2149 }
2150 envs
2151}
2152
2153#[cfg(test)]
2154mod tests {
2155 use super::*;
2156
2157 fn view(code: Code, job: Option<&str>) -> View {
2158 let facts = Facts { workspace: Some("acme".into()), backend: "sandboxed", machine: "box".into(), job: job.map(str::to_owned), ..Facts::default() };
The desktop app is 0.3.4, and the runner has a window of its own. Beside the title bar's Settings button a chip says what the runner is doing, live, with a dot in its colour (Runner running, Running a job, Running 2 jobs, Runner starting, Runner updating, Runner stopped, Runner offline, Runner needs you), as Docker Desktop shows its engine; in a narrow window only the dot shows. It opens the Runner window, as the tray's runner row now does: at its top what the whole runner is doing, with what it is running, its version and workspace, and its own Stop or Start; then each kind of job's card as before; then its setup (running jobs here at all, how jobs run, the workspace, the computer with Check for runner updates and View on g1t); and its log, open and following along as it is written. Settings keeps the runner as one line, what it is doing with Open runner, and each window saves only its own settings over what the app has now, so neither undoes the other's. The chip's words are diagnosis.rs's, the Runner window is made off the main thread as Settings now is, and it has a capability of its own with only the commands it uses; the title bar may now ask the runner's status. Two things fixed on the way: a settings row hidden until it applies, Restart to change the title bar, showed all the time, since its row's display beat the hidden attribute; and in a narrow window the title bar's buttons ran over the search, which now shrinks instead. The desktop guide describes the chip, the Runner window and where each runner setting now is.2159 let diagnosed = diagnosis::diagnose(code, &facts);
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.2160 View {
The desktop app is 0.3.4, and the runner has a window of its own. Beside the title bar's Settings button a chip says what the runner is doing, live, with a dot in its colour (Runner running, Running a job, Running 2 jobs, Runner starting, Runner updating, Runner stopped, Runner offline, Runner needs you), as Docker Desktop shows its engine; in a narrow window only the dot shows. It opens the Runner window, as the tray's runner row now does: at its top what the whole runner is doing, with what it is running, its version and workspace, and its own Stop or Start; then each kind of job's card as before; then its setup (running jobs here at all, how jobs run, the workspace, the computer with Check for runner updates and View on g1t); and its log, open and following along as it is written. Settings keeps the runner as one line, what it is doing with Open runner, and each window saves only its own settings over what the app has now, so neither undoes the other's. The chip's words are diagnosis.rs's, the Runner window is made off the main thread as Settings now is, and it has a capability of its own with only the commands it uses; the title bar may now ask the runner's status. Two things fixed on the way: a settings row hidden until it applies, Restart to change the title bar, showed all the time, since its row's display beat the hidden attribute; and in a narrow window the title bar's buttons ran over the search, which now shrinks instead. The desktop guide describes the chip, the Runner window and where each runner setting now is.2161 chip: diagnosis::chip(&diagnosed),
2162 diagnosis: diagnosed,
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.2163 closed: diagnosis::closed(&facts),
2164 running: code == Code::Idle,
2165 version: None,
2166 backend: Backend::Sandboxed,
2167 workspace: Some("acme".into()),
2168 workspaces: vec![],
2169 enabled: true,
2170 paused: false,
2171 link: None,
2172 machine: "box".into(),
2173 platform: "windows".into(),
2174 log: vec![],
2175 }
2176 }
2177
2178 #[test]
2179 fn the_tray_says_what_the_runner_is_doing() {
2180 assert_eq!(headline(&view(Code::Idle, None)), "Runner: Waiting for jobs");
2181 assert_eq!(headline(&view(Code::Busy, Some("Build (acme/web)"))), "Runner: Running Build (acme/web)");
2182 assert_eq!(headline(&view(Code::NoDocker, None)), "Runner: Not running: Docker isn't running");
2183 assert_eq!(headline(&view(Code::Busy, Some(&"x".repeat(200)))).chars().count(), 60);
2184 }
2185
2186 #[test]
2187 fn the_runner_is_told_where_and_who_and_the_token_only_once() {
2188 let server = Url::parse("https://g1t.sh/").unwrap();
2189 let envs = environment(&server, Backend::Host, Some("g1trt_abc"));
2190 let get = |name: &str| envs.iter().find(|(n, _)| *n == name).map(|(_, v)| v.as_str());
2191 assert_eq!(get("G1T_RUNNER_URL"), Some("https://g1t.sh"));
2192 assert_eq!(get("G1T_RUNNER_LABELS"), Some("desktop"));
2193 assert_eq!(get("G1T_RUNNER_NO_DOCKER"), Some("1"));
2194 assert_eq!(get("G1T_RUNNER_NO_AUTO_UPDATE"), Some("1"));
2195 assert_eq!(get("G1T_RUNNER_TOKEN"), Some("g1trt_abc"));
2196 let again = environment(&server, Backend::Docker, None);
2197 assert!(!again.iter().any(|(name, _)| *name == "G1T_RUNNER_TOKEN" || *name == "G1T_RUNNER_NO_DOCKER"));
2198 }
2199
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.2200 fn status(json: &str) -> Status {
2201 launcher::parse(json).unwrap()
2202 }
2203
2204 #[test]
2205 fn the_sandbox_shows_what_its_runners_do_and_linux_trouble_first() {
2206 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)."}]}"#);
2207 assert_eq!(shown(&running).0, Code::Idle);
2208 let busy = status(r#"{"schema":1,"state":"running","job":{"kind":"workflow","id":"j","name":"build","repo":"acme/web"}}"#);
2209 assert_eq!(shown(&busy).0, Code::Busy);
2210 let starting = status(r#"{"schema":1,"state":"starting","capabilities":[{"id":"linux","available":true,"state":"starting"}]}"#);
2211 assert_eq!(shown(&starting).0, Code::Starting);
2212 let updating = status(r#"{"schema":1,"state":"updating","capabilities":[{"id":"linux","available":true,"state":"updating"}]}"#);
2213 assert_eq!(shown(&updating).0, Code::Updating);
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.2214 // Windows jobs run while WSL is missing: the runner waits for jobs,
2215 // and the Linux card says what is wrong (diagnosis.rs).
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.2216 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":[
2217 {"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`."},
2218 {"id":"windows","available":true,"state":"running"}]}"#);
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.2219 assert_eq!(shown(&no_wsl).0, Code::Idle);
2220 let (code, extra) = shown(&status(r#"{"schema":1,"state":"error","error":"WSL is not installed, and the sandbox for Linux jobs is a WSL distribution of its own.","capabilities":[
2221 {"id":"linux","available":false,"state":"error","cause":"WSL is not installed, and the sandbox for Linux jobs is a WSL distribution of its own."},
2222 {"id":"windows","available":false,"state":"absent"}]}"#));
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.2223 assert_eq!(code, Code::NoWsl);
2224 assert!(extra.detail.unwrap().starts_with("WSL is not installed"));
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.2225 // 0.3.0 without its images, on Home: waiting for them, not failing.
2226 let images = status(r#"{"schema":1,"state":"error","error":"release 0.3.0 has no sandbox images","capabilities":[{"id":"linux","available":true,"state":"error","turned_off":false},{"id":"windows","available":false,"state":"absent","turned_off":false}]}"#);
2227 assert_eq!(shown(&images).0, Code::WaitingForImage);
2228 assert_eq!(again(true, 5), launcher::IMAGES_AGAIN);
2229 assert_eq!(again(false, 1), Duration::from_secs(30));
2230 assert_eq!(again(false, 9), MOST_BACKOFF);
2231 // Every kind of job stopped by the person.
2232 let off = status(r#"{"schema":1,"state":"stopped","capabilities":[{"id":"linux","available":true,"state":"stopped","turned_off":true},{"id":"windows","available":true,"state":"stopped","turned_off":true}]}"#);
2233 assert_eq!(shown(&off).0, Code::KindsStopped);
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.2234 let failed = status(r#"{"schema":1,"state":"error","error":"the sandbox did not start within 180 seconds","capabilities":[{"id":"linux","available":true,"state":"error"}]}"#);
2235 let (code, extra) = shown(&failed);
2236 assert_eq!((code, extra.detail.as_deref()), (Code::SandboxFailed, Some("the sandbox did not start within 180 seconds")));
2237 let stopped = status(r#"{"schema":1,"state":"stopped","capabilities":[{"id":"linux","available":true,"state":"stopped"},{"id":"windows","available":true,"state":"stopped"}]}"#);
2238 assert_eq!(shown(&stopped).0, Code::SandboxFailed);
2239 }
2240
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.2241 #[test]
The desktop app's runner lives in its local data folder, which stays on the computer: %LOCALAPPDATA%\sh.g1t.desktop\runner on Windows, where it was in roaming data (%APPDATA%), which networks with roaming profiles copy to a profile server along with everything else there, the sandbox's VM disk and the runner's credential included. A computer whose runner was already set up in roaming data keeps it where it is, since its WSL distribution is registered at that path and moving its disk would break it; on macOS and Linux the two folders were always the same one. The desktop guide gives the new place.2242 fn a_new_runner_lives_in_local_data_and_an_old_one_stays_put() {
2243 let base = std::env::temp_dir().join(format!("g1t-desktop-folders-{}", std::process::id()));
2244 let _ = std::fs::remove_dir_all(&base);
2245 let (local, roaming) = (base.join("local").join("runner"), base.join("roaming").join("runner"));
2246 assert_eq!(chosen_folder(local.clone(), roaming.clone()), local);
2247 std::fs::create_dir_all(&roaming).unwrap();
2248 assert_eq!(chosen_folder(local.clone(), roaming.clone()), roaming);
2249 std::fs::create_dir_all(&local).unwrap();
2250 assert_eq!(chosen_folder(local.clone(), roaming), local);
2251 let _ = std::fs::remove_dir_all(&base);
2252 }
2253
2254 #[test]
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.2255 fn when_is_said_as_the_site_reads_it() {
2256 let at = |seconds: u64| rfc3339(SystemTime::UNIX_EPOCH + Duration::from_secs(seconds));
2257 assert_eq!(at(0), "1970-01-01T00:00:00Z");
2258 assert_eq!(at(1_791_806_645), "2026-10-12T12:04:05Z");
2259 // The day after February 29th of a leap year.
2260 assert_eq!(at(1_709_251_199 + 1), "2024-03-01T00:00:00Z");
2261 assert_eq!(at(951_782_400), "2000-02-29T00:00:00Z");
2262 }
2263
2264 /// What is running on the program itself, which an update drains first:
2265 /// Windows jobs running or starting, not Linux jobs, nor ones stopped.
2266 #[test]
2267 fn an_update_drains_only_what_runs_on_the_program() {
2268 let kinds = |json: &str| native_kinds(&status(json)).iter().map(|c| c.id.clone()).collect::<Vec<_>>();
2269 assert_eq!(
2270 kinds(r#"{"schema":1,"state":"running","capabilities":[{"id":"linux","available":true,"state":"running","turned_off":false},{"id":"windows","available":true,"state":"running","turned_off":false}]}"#),
2271 ["windows"]
2272 );
2273 assert!(kinds(r#"{"schema":1,"state":"running","capabilities":[{"id":"linux","available":true,"state":"running","turned_off":false},{"id":"windows","available":true,"state":"stopped","turned_off":true}]}"#).is_empty());
2274 assert!(kinds(r#"{"schema":1,"state":"running","capabilities":[{"id":"linux","available":true,"state":"running"},{"id":"windows","available":false,"state":"absent"}]}"#).is_empty());
2275 }
2276
2277 #[test]
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.2278 fn the_site_names_workspaces_in_its_own_words() {
2279 let offered: Vec<Offered> = serde_json::from_str(r#"[{"slug":"acme","name":"Acme"},{"slug":"solo"}]"#).unwrap();
2280 assert_eq!(offered[1], Offered { slug: "solo".into(), name: None });
2281 }
2282}

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