Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
1,273 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//! What the runner is doing, and when it is not running or is failing, why
2//! and how to fix it: the one catalogue of those words. The tray shows a
3//! diagnosis's title, the settings window all of it with its buttons, and
4//! the site shows it as the app sends it, word for word, on its Runners
5//! page (and keeps it, so a person's other computers are listed with
6//! theirs). The site adds nothing of its own: what it says of a computer
7//! whose g1t is closed is `closed` here, sent with every report.
8//!
9//! Each action is an id the settings window and the site turn into a
10//! button; bridge.rs and settings.js do what it 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.11//!
12//! The sandbox (launcher.rs) says why something is wrong in its own
13//! sentences. Those this catalogue knows (WSL not installed, virtualization
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.14//! off, Windows Sandbox off, a release without the sandbox's images, …)
15//! are said in its words, with a fix and its buttons; any other is shown as
16//! the launcher wrote it. With the sandbox, each kind of job it runs (Linux
17//! jobs, Windows jobs) is a card of its own (`capabilities`), with its own
18//! state, cause, fix and Start or Stop, since one can run while the other
19//! cannot, or is stopped. The whole runner's diagnosis then only says what
20//! the cards don't (`in_capabilities` is false): signed out, no workspace,
21//! not allowed, no launcher, the whole sandbox 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.22
23use serde::Serialize;
24
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.25use super::launcher::{self, State, Status, Why};
26
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.27/// Every state the runner can be in.
28#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
29#[serde(rename_all = "snake_case")]
30pub enum Code {
31 /// Running jobs is turned off in settings.
32 #[default]
33 Off,
34 /// The person pressed Stop.
35 Stopped,
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.36 /// Sandboxed, the person stopped every kind of job there is here.
37 KindsStopped,
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.38 Starting,
39 /// Waiting for the site to make a registration token.
40 Registering,
41 /// Nobody is signed in to g1t in the app.
42 SignedOut,
43 /// Signed in, owning no workspace: nowhere to register.
44 NoWorkspace,
45 /// The person may not add runners to the workspace.
46 NotAllowed,
47 RegisterFailed,
48 Idle,
49 Busy,
50 NoDocker,
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 /// The `g1t-runner` here is from before its sandbox launcher.
52 NoLauncher,
53 /// Windows, without WSL, which holds the sandbox's Linux VM.
54 NoWsl,
55 /// WSL set to mirrored networking, which reaches this computer's localhost.
56 WslMirrored,
57 /// The computer's virtualization is turned off.
58 NoVirtualization,
59 /// macOS, without vfkit, which runs the sandbox's VM.
60 NoVfkit,
61 /// Linux, with neither Sysbox in Docker nor KVM.
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.62 NoSysbox,
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.63 /// The newest release is published without the sandbox's images yet.
64 WaitingForImage,
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.65 /// The sandbox's image couldn't be downloaded, or isn't the release's.
66 ImageFailed,
67 /// The sandbox can't be made here, for a reason in the launcher's words.
68 SandboxUnavailable,
69 /// The sandbox failed, in the launcher's words.
70 SandboxFailed,
71 /// The computer has a sandbox the app didn't make, and has room for one.
72 SandboxTaken,
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.73 Updating,
74 UpdateFailed,
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.75 /// The newest release's manifest isn't signed with g1t's release key.
76 UpdateRefused,
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.77 Offline,
78 Crashed,
79}
80
81/// How a state should look: a dot's colour.
82#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
83#[serde(rename_all = "snake_case")]
84pub enum Tone {
85 Ok,
86 Working,
87 Off,
88 Warning,
89 Problem,
90}
91
92/// A one-click fix.
93#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
94pub struct Action {
95 pub id: &'static str,
96 pub label: &'static str,
Merge the desktop app's settings and runner in the main window: the runner is a dot beside the title bar's Settings button with a menu of its own (its state, each kind of job and what it runs, Start and Stop, updates, the log), Settings opens as a dialog in the window with its sections down the side, the separate windows stay only for when the site isn't showing, and every button that asks the app for something shows it is working until something happens.97 /// Its button stays busy, after a click, until the runner's status
98 /// that follows changes (or an error, or a while with no change): the
99 /// runner starting, stopping, retrying or looking for updates. One
100 /// that opens a page, a window or copies is done once the app has it.
101 pub waits: bool,
102 /// What the person is asked before it goes ahead, for a fix that can't be undone.
103 #[serde(skip_serializing_if = "Option::is_none")]
104 pub confirm: Option<&'static str>,
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.105}
106
Merge the desktop app's settings and runner in the main window: the runner is a dot beside the title bar's Settings button with a menu of its own (its state, each kind of job and what it runs, Start and Stop, updates, the log), Settings opens as a dialog in the window with its sections down the side, the separate windows stay only for when the site isn't showing, and every button that asks the app for something shows it is working until something happens.107impl Action {
108 /// A fix that is done the moment the app has it.
109 const fn now(id: &'static str, label: &'static str) -> Self {
110 Self { id, label, waits: false, confirm: None }
111 }
112
113 /// A fix whose effect the runner's status shows when it comes.
114 const fn waits(id: &'static str, label: &'static str) -> Self {
115 Self { id, label, waits: true, confirm: None }
116 }
117
118 const fn confirming(self, question: &'static str) -> Self {
119 Self { confirm: Some(question), ..self }
120 }
121}
122
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.123#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
124pub struct Diagnosis {
125 pub code: Code,
126 pub tone: Tone,
127 /// One line: the tray's, and the heading everywhere else.
128 pub title: String,
129 /// Why, in a sentence.
130 pub cause: Option<String>,
131 /// What to do about it.
132 pub fix: Option<String>,
133 pub actions: Vec<Action>,
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.134 /// With the sandbox: each kind of job it runs, as a card of its own.
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.135 #[serde(skip_serializing_if = "Vec::is_empty")]
136 pub capabilities: Vec<CapabilityLine>,
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.137 /// The cards say all of this, each for its kind of job: a page that
138 /// shows them shows no line of the whole runner's as well.
139 #[serde(skip_serializing_if = "is_false")]
140 pub in_capabilities: bool,
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.141 /// How the runner's updates stand: when g1t last looked, and a newer
142 /// one waiting. None for what is said once g1t is closed.
143 #[serde(skip_serializing_if = "Option::is_none")]
144 pub update: Option<UpdateLine>,
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.145}
146
147fn is_false(value: &bool) -> bool {
148 !*value
149}
150
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.151/// The runner's updates, as settings and the site's Desktops show them.
152#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
153pub struct UpdateLine {
154 /// When g1t last heard from the runner's releases, RFC 3339: the site,
155 /// which shows a report for minutes, says how long ago itself.
156 pub checked_at: Option<String>,
157 /// `Checked 12 min ago`, `Checking for updates…`.
158 pub checked: String,
159 pub checking: bool,
160 /// A newer version, downloaded and checked, waiting to be installed.
161 pub ready: Option<String>,
162 /// What becomes of it, for a runner with no kinds of job of its own
163 /// (Docker, or on this computer): `0.3.2 ready, installs after this
164 /// job`. A sandbox's cards each say it for their own kind.
165 pub note: Option<String>,
166 /// Looks now: "Check for runner updates".
167 pub check: Action,
168}
169
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.170/// How one kind of job is, as its card's label says it.
171#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
172#[serde(rename_all = "snake_case")]
173pub enum KindState {
174 /// Running a job.
175 Busy,
176 /// Waiting for jobs.
177 Idle,
178 Starting,
179 /// Downloading the sandbox's image, which `progress` says how far.
180 Downloading,
181 Updating,
182 /// Waiting for something g1t sees to by itself, such as the release's images.
183 Waiting,
184 /// Stopped: by the person (`enabled` is false), or with the whole runner.
185 Stopped,
186 /// This computer can't run it, and `cause` says why.
187 Unavailable,
188 /// It should run and doesn't: `cause` and `fix`.
189 Problem,
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.190}
191
192/// One kind of job the sandbox runs, `linux` or `windows` (or `macos`),
193/// and how it is: Linux jobs can run while Windows jobs cannot.
194#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
195pub struct CapabilityLine {
196 pub id: String,
197 /// `Linux jobs`.
198 pub label: String,
199 pub tone: Tone,
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.200 pub state: KindState,
201 /// Its state in a few words, for its label: `Waiting for jobs`, `Not available`.
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.202 pub title: 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.203 /// What it is, in a line: `A Linux VM of its own, each job in a fresh container`.
204 pub about: String,
205 /// What is wrong, in a few words, over its cause: `WSL isn't installed`.
206 pub headline: 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.207 pub cause: Option<String>,
208 pub fix: Option<String>,
209 pub actions: Vec<Action>,
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.210 /// On in settings: the person hasn't stopped it.
211 pub enabled: bool,
212 /// This computer can run it.
213 pub available: bool,
214 /// It can be started and stopped on its own here (the launcher can).
215 pub can_toggle: bool,
216 /// What it is running now.
217 pub job: Option<JobLine>,
218 /// How far its image has downloaded, in percent.
219 pub progress: Option<u8>,
220 /// How it keeps jobs from the computer: `nested-container`, `vm`.
221 pub isolation: Option<String>,
222 /// Its runner's name on g1t: `build-box-linux`.
223 pub runner: Option<String>,
224 /// Its runner's version.
225 pub version: Option<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.226 /// A newer version waiting for it: `0.3.2 ready, installs after this job`.
227 pub update: Option<String>,
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.228}
229
230/// The job a kind of job is running, with where to see it on g1t.
231#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
232pub struct JobLine {
233 /// `workflow` or `agent`.
234 pub kind: String,
235 pub name: String,
236 pub repo: String,
237 /// The repository's Actions (or Agents) page, where its run is.
238 pub link: 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.239}
240
241/// What the words are filled in with.
242#[derive(Clone, Debug, Default)]
243pub struct Facts {
244 pub workspace: Option<String>,
245 /// How jobs run, as `sandboxed`, `in Docker`, `on this computer`.
246 pub backend: &'static str,
247 pub machine: String,
248 pub job: Option<String>,
249 pub detail: Option<String>,
250 pub exit_code: Option<i32>,
251 pub wait_seconds: Option<u64>,
252 /// The server's host: `g1t.sh`.
253 pub server: 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.254 /// The server's address, for links: `https://g1t.sh`.
255 pub site: 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.256 pub platform: 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.257 /// What the sandbox says to do, for a cause in its own words.
258 pub fix: Option<String>,
259 /// How the sandbox is, when jobs run sandboxed.
260 pub 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.261 /// The kinds of job the person stopped in settings.
262 pub off: Vec<String>,
263 /// The sandbox is up, or on its way: a kind of job that should run
264 /// and doesn't is failing, rather than stopped with everything else.
265 pub live: bool,
266 /// The kinds of job a launcher command is starting now.
267 pub starting: Vec<String>,
268 /// How far the sandbox's image has downloaded, while it does.
269 pub download: Option<launcher::Download>,
270 /// The person stopped the whole runner, or turned it off.
271 pub held: bool,
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.272 pub update: Update,
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.273}
274
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.275/// How the runner's updates stand.
276#[derive(Clone, Debug, Default, PartialEq, Eq)]
277pub struct Update {
278 /// The g1t-runner here.
279 pub installed: Option<String>,
280 /// When g1t last heard from the releases, RFC 3339, and how long ago.
281 pub checked_at: Option<String>,
282 pub checked_ago: Option<u64>,
283 pub checking: bool,
284 /// A newer release, downloaded and checked, waiting to be put in place.
285 pub ready: Option<String>,
286 /// What is still to take it: `linux` (the sandbox's image), `windows`
287 /// (the program its runner is), or `runner` for a runner with no kinds
288 /// of job of its own.
289 pub waiting: Vec<String>,
290 /// Those taking it now.
291 pub installing: Vec<String>,
292}
293
294/// When g1t last looked for a newer runner: `Checked 12 min ago`.
295pub fn checked(update: &Update) -> String {
296 if update.checking {
297 return "Checking for updates…".into();
298 }
299 let Some(ago) = update.checked_ago else { return "Not checked yet".into() };
300 let plural = |n: u64, one: &str| if n == 1 { format!("Checked 1 {one} ago") } else { format!("Checked {n} {one}s ago") };
301 match ago {
302 0..=59 => "Checked just now".into(),
303 60..=3599 => format!("Checked {} min ago", ago / 60),
304 3600..=86_399 => plural(ago / 3600, "hour"),
305 _ => plural(ago / 86_400, "day"),
306 }
307}
308
309/// What becomes of a newer version waiting for `what` (a kind of job, or
310/// `runner`), whose job is running when `busy`. None when nothing waits.
311fn waiting(update: &Update, what: &str, busy: bool) -> Option<String> {
312 let version = update.ready.as_deref()?;
313 if update.installing.iter().any(|kind| kind == what) {
314 return some(format!("Installing {version}"));
315 }
316 if !update.waiting.iter().any(|kind| kind == what) {
317 return None;
318 }
319 some(if busy { format!("{version} ready, installs after this job") } else { format!("{version} ready, installs in a moment") })
320}
321
322fn update_line(facts: &Facts, code: Code) -> UpdateLine {
323 let update = &facts.update;
324 UpdateLine {
325 checked_at: update.checked_at.clone(),
326 checked: checked(update),
327 checking: update.checking,
328 ready: update.ready.clone(),
329 note: waiting(update, "runner", code == Code::Busy),
330 check: CHECK_UPDATES,
331 }
332}
333
Merge the desktop app's settings and runner in the main window: the runner is a dot beside the title bar's Settings button with a menu of its own (its state, each kind of job and what it runs, Start and Stop, updates, the log), Settings opens as a dialog in the window with its sections down the side, the separate windows stay only for when the site isn't showing, and every button that asks the app for something shows it is working until something happens.334const START: Action = Action::waits("start", "Start");
335const TURN_ON: Action = Action::waits("start", "Turn on");
336const RETRY: Action = Action::waits("retry", "Retry");
337const CHECK_NOW: Action = Action::waits("retry", "Check 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.338/// Looks for a newer runner now, and installs it as soon as its kinds of job are idle.
Merge the desktop app's settings and runner in the main window: the runner is a dot beside the title bar's Settings button with a menu of its own (its state, each kind of job and what it runs, Start and Stop, updates, the log), Settings opens as a dialog in the window with its sections down the side, the separate windows stay only for when the site isn't showing, and every button that asks the app for something shows it is working until something happens.339const CHECK_UPDATES: Action = Action::waits("check_updates", "Check for runner updates");
340const CHECK_AGAIN: Action = Action::waits("check_updates", "Check again");
A computer with a sandbox the desktop app has no record of making can be put right with one button. The app said "this computer has another sandbox" and told the person to run `g1t-runner sandbox down --delete`, which looks in the default folder rather than the app's own, so it removed nothing. Most often the sandbox was the app's own from before it was uninstalled with its data deleted and installed again: deleting the app's folder left the WSL distribution behind. Now the message's button is Remove it and start again: it asks first, then removes the sandbox with its VM and registration from the app's own folder and starts a sandbox of the app's own, registered with the workspace. Uninstalling on Windows with Delete the application data ticked removes the sandbox too (an installer hook; not on an update, which keeps everything). And in the Runner window a pressed fix shows a spinner and stays pressed until the status that follows, rather than looking dead until something happens. The desktop guide says so.341/// Removes the sandbox this computer has (`sandbox down --delete`, in the
342/// app's own folder for it) and starts the app's own: what the command a
343/// person would type does, without needing to know where that folder is.
Merge the desktop app's settings and runner in the main window: the runner is a dot beside the title bar's Settings button with a menu of its own (its state, each kind of job and what it runs, Start and Stop, updates, the log), Settings opens as a dialog in the window with its sections down the side, the separate windows stay only for when the site isn't showing, and every button that asks the app for something shows it is working until something happens.344const REMOVE_SANDBOX: Action = Action::waits("remove_sandbox", "Remove it and start again").confirming(
345 "Remove this computer's sandbox? Its VM and registration go, and g1t makes its own in their place, registered with your workspace. Its old runners stay listed on g1t until you remove them there.",
346);
347const RESTART: Action = Action::waits("restart", "Restart");
348const SIGN_IN: Action = Action::now("sign_in", "Sign in");
349const CHOOSE_WORKSPACE: Action = Action::now("choose_workspace", "Choose workspace");
350const NEW_WORKSPACE: Action = Action::now("new_workspace", "Create a workspace");
351const CHOOSE_BACKEND: Action = Action::now("choose_backend", "Choose how jobs run");
352/// Docker takes a while to start; the runner's status says when it has.
353const OPEN_DOCKER: Action = Action::waits("open_docker", "Open Docker");
354const GET_DOCKER: Action = Action::now("get_docker", "Get Docker");
355const INSTALL_SYSBOX: Action = Action::now("install_sysbox", "Install Sysbox");
356const COPY_WSL_INSTALL: Action = Action::now("copy_wsl_install", "Copy command");
357const COPY_VFKIT_INSTALL: Action = Action::now("copy_vfkit_install", "Copy command");
358const WINDOWS_FEATURES: Action = Action::now("windows_features", "Open Windows features");
359const LEARN_MORE: Action = Action::now("learn_more", "Learn more");
360const VIEW_LOG: Action = Action::now("view_log", "View log");
361const REPORT: Action = Action::now("report", "Report");
362const VIEW_RUNNERS: Action = Action::now("view_runners", "View on g1t");
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.363
364fn some(text: impl Into<String>) -> Option<String> {
365 Some(text.into())
366}
367
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.368/// A wait, said: `30 seconds`, `5 minutes`.
369fn said_wait(seconds: u64) -> String {
370 match seconds {
371 0..=1 => "a moment".into(),
372 2..=89 => format!("{seconds} seconds"),
373 _ => format!("{} minutes", seconds.div_ceil(60)),
374 }
375}
376
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.377/// The catalogue.
378pub fn diagnose(code: Code, facts: &Facts) -> Diagnosis {
379 let workspace = facts.workspace.as_deref().unwrap_or("your workspace");
380 let detail = facts.detail.clone();
381 let (tone, title, cause, fix, actions): (Tone, String, Option<String>, Option<String>, Vec<Action>) = match code {
382 Code::Off => (
383 Tone::Off,
384 "Not running: turned off".into(),
385 some("Running jobs on this computer is turned off in g1t's settings."),
386 some("Turn it on to run your workspace's self-hosted jobs here while g1t is open."),
387 vec![TURN_ON],
388 ),
389 Code::Stopped => (
390 Tone::Off,
391 "Not running: stopped".into(),
392 some("You stopped the runner. It starts again when you press Start, or when g1t next opens."),
393 None,
394 vec![START],
395 ),
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.396 Code::KindsStopped => (
397 Tone::Off,
398 "Not running: every kind of job is stopped".into(),
399 some("You stopped each kind of job this computer can run."),
400 some("Start one to run jobs here again."),
401 vec![],
402 ),
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.403 Code::Starting => (Tone::Working, "Starting".into(), detail, None, vec![]),
404 Code::Registering => (
405 Tone::Working,
406 format!("Registering with {workspace}"),
407 some(format!("g1t is registering {} as a runner for {workspace}.", facts.machine)),
408 some("Keep g1t open and signed in; it takes a few seconds."),
409 vec![],
410 ),
411 Code::SignedOut => (
412 Tone::Warning,
413 "Not running: signed out".into(),
414 some("This computer isn't registered yet, and registering it needs you signed in to g1t."),
415 some("Sign in, as an owner of the workspace the runner is for."),
416 vec![SIGN_IN],
417 ),
418 Code::NoWorkspace => (
419 Tone::Warning,
420 "Not running: no workspace to run jobs for".into(),
421 some("Only a workspace's owners add runners to it, and you don't own one."),
422 some("Ask an owner of your workspace to run it, or create a workspace of your own."),
423 vec![NEW_WORKSPACE],
424 ),
425 Code::NotAllowed => (
426 Tone::Problem,
427 format!("Couldn't register with {workspace}"),
428 some(format!("You can't add runners to {workspace}: only its owners can.")),
429 some(format!("Ask an owner of {workspace} to make you one, or choose a workspace you own.")),
430 vec![CHOOSE_WORKSPACE],
431 ),
432 Code::RegisterFailed => (
433 Tone::Problem,
434 format!("Couldn't register with {workspace}"),
435 detail.or_else(|| some("g1t didn't make a registration for this computer.")),
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.436 some("g1t asks again by itself in a few minutes; Retry asks now. Its log says more."),
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.437 vec![RETRY, VIEW_LOG],
438 ),
439 Code::Idle => (
440 Tone::Ok,
441 "Waiting for jobs".into(),
442 some(format!("Running {} for {workspace} on {}.", facts.backend, facts.machine)),
443 None,
444 vec![VIEW_RUNNERS],
445 ),
446 Code::Busy => (
447 Tone::Working,
448 format!("Running {}", facts.job.as_deref().unwrap_or("a job")),
449 some(format!("Running {} for {workspace} on {}.", facts.backend, facts.machine)),
450 None,
451 vec![VIEW_RUNNERS],
452 ),
453 Code::NoDocker => {
454 let app = match facts.platform.as_str() {
455 "windows" | "macos" => "Docker Desktop",
456 _ => "Docker",
457 };
458 (
459 Tone::Problem,
460 "Not running: Docker isn't running".into(),
461 some(format!("Running jobs {} needs Docker, and Docker isn't running on this computer, or isn't installed.", facts.backend)),
462 some(format!("Start {app}, or install it. The runner starts by itself once Docker answers.")),
463 if app == "Docker Desktop" { vec![OPEN_DOCKER, GET_DOCKER] } else { vec![GET_DOCKER] },
464 )
465 }
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.466 Code::NoLauncher => (
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.467 Tone::Warning,
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.468 "Not running: this g1t-runner has no sandbox".into(),
469 some("The g1t-runner g1t downloaded is older than its sandbox launcher, so it can't make a sandbox."),
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.470 some("g1t looks for a newer one by itself every few minutes; Retry looks now. Or choose to run jobs in Docker or directly on this computer: neither is a sandbox, and settings say what each can reach."),
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.471 vec![RETRY, CHOOSE_BACKEND],
472 ),
473 Code::NoWsl => (
474 Tone::Problem,
475 "Not running: WSL isn't installed".into(),
476 some("Linux jobs run in a WSL distribution of the runner's own, and WSL isn't installed on this computer."),
477 some(format!("Run `{}` in an admin terminal, then restart Windows. It works on every edition of Windows 11, Home included.", launcher::WSL_INSTALL)),
478 vec![COPY_WSL_INSTALL, LEARN_MORE],
479 ),
480 Code::WslMirrored => (
481 Tone::Problem,
482 "Not running: WSL uses mirrored networking".into(),
483 some("WSL is set to mirrored networking, in which Linux reaches this computer's own localhost, so the sandbox won't start."),
484 some("Remove networkingMode=mirrored from %USERPROFILE%\\.wslconfig, run `wsl --shutdown`, then Retry."),
485 vec![RETRY, LEARN_MORE],
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.486 ),
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.487 Code::NoVirtualization => (
488 Tone::Problem,
489 "Not running: virtualization is off".into(),
490 some("The sandbox is a small virtual machine, and this computer has virtualization turned off."),
491 some(if facts.platform == "windows" {
492 "Turn on virtualization (Intel VT-x or AMD-V) in this computer's firmware settings, and Virtual Machine Platform in Windows features; then restart."
493 } else {
494 "Turn on virtualization (Intel VT-x or AMD-V) in this computer's firmware settings, then restart."
495 }),
496 if facts.platform == "windows" { vec![WINDOWS_FEATURES, LEARN_MORE] } else { vec![RETRY, LEARN_MORE] },
497 ),
498 Code::NoVfkit => (
499 Tone::Problem,
500 "Not running: vfkit isn't installed".into(),
501 some("Linux jobs run in a small VM that vfkit runs with Apple's Virtualization framework, and vfkit isn't installed."),
502 some(format!("Run `{}` in Terminal (macOS 13 or later). The runner starts by itself once it's there.", launcher::VFKIT_INSTALL)),
503 vec![COPY_VFKIT_INSTALL, LEARN_MORE],
504 ),
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.505 Code::NoSysbox => (
506 Tone::Problem,
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.507 "Not running: no Sysbox or KVM".into(),
508 some("The sandbox on Linux is the runner's image under Sysbox, or a VM under QEMU with KVM, and this computer has neither."),
509 some("Install Sysbox 0.7 or later, or turn on virtualization and install QEMU so /dev/kvm exists. The runner starts by itself once one is there."),
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.510 vec![INSTALL_SYSBOX, CHOOSE_BACKEND],
511 ),
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.512 Code::WaitingForImage => {
513 let (release, image) = detail.as_deref().and_then(launcher::missing_images).map_or((None, None), |(release, image)| (Some(release), image));
514 let release = release.map_or_else(|| "The newest g1t-runner".to_owned(), |release| format!("g1t-runner {release}"));
515 (
516 Tone::Warning,
517 "Waiting for the sandbox's image".into(),
518 some(match image {
519 Some(image) => format!("{release} is published without the sandbox's image for this kind of computer ({image}) yet, so Linux jobs can't start."),
520 None => format!("{release} is published without the sandbox's images yet, so Linux jobs can't start."),
521 }),
522 some("g1t checks again every few minutes and starts Linux jobs once they're out. There's nothing for you to do."),
523 vec![CHECK_NOW],
524 )
525 }
526 Code::ImageFailed => {
527 let said = detail.clone().unwrap_or_default().to_ascii_lowercase();
528 if said.contains("sha-256") || said.contains("signature") {
529 (
530 Tone::Problem,
531 "Not running: the sandbox's image didn't check out".into(),
532 some("The sandbox's image that downloaded isn't the one g1t's release signed, so it wasn't used."),
533 some(format!(
534 "g1t downloads it again by itself{}. If this keeps happening, something between this computer and {} is changing downloads, such as a proxy or antivirus: report it.",
535 facts.wait_seconds.map(|wait| format!(" in {}", said_wait(wait))).unwrap_or_default(),
536 facts.server
537 )),
538 vec![RETRY, REPORT],
539 )
540 } else {
541 (
542 Tone::Problem,
543 "Not running: the sandbox's image didn't download".into(),
544 some(match &detail {
545 Some(detail) => format!("The sandbox's image couldn't be downloaded: {detail}"),
546 None => "The sandbox's image couldn't be downloaded.".into(),
547 }),
548 some(format!(
549 "g1t tries again by itself{}. Check that this computer can reach {}; Retry tries now.",
550 facts.wait_seconds.map(|wait| format!(" in {}", said_wait(wait))).unwrap_or_default(),
551 facts.server
552 )),
553 vec![RETRY, VIEW_LOG],
554 )
555 }
556 }
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.557 Code::SandboxUnavailable => (
558 Tone::Problem,
559 "Not running: the sandbox can't run here".into(),
560 detail.or_else(|| some("The sandbox can't be made on this computer.")),
561 facts.fix.clone().or_else(|| some("Choose to run jobs in Docker or directly on this computer instead: neither is a sandbox, and settings say what each can reach.")),
562 vec![RETRY, LEARN_MORE, CHOOSE_BACKEND],
563 ),
564 Code::SandboxFailed => (
565 Tone::Problem,
566 "Not running: the sandbox failed".into(),
567 detail.or_else(|| some("The sandbox stopped, and didn't say why.")),
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.568 some(match facts.wait_seconds {
569 Some(wait) => format!("It starts again by itself in {}. If it keeps stopping, its log says why; Retry starts it now.", said_wait(wait)),
570 None => "Retry starts it again; its log says why it stopped.".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.571 }),
572 vec![RETRY, VIEW_LOG],
573 ),
574 Code::SandboxTaken => (
575 Tone::Warning,
576 "Not running: this computer has another sandbox".into(),
A computer with a sandbox the desktop app has no record of making can be put right with one button. The app said "this computer has another sandbox" and told the person to run `g1t-runner sandbox down --delete`, which looks in the default folder rather than the app's own, so it removed nothing. Most often the sandbox was the app's own from before it was uninstalled with its data deleted and installed again: deleting the app's folder left the WSL distribution behind. Now the message's button is Remove it and start again: it asks first, then removes the sandbox with its VM and registration from the app's own folder and starts a sandbox of the app's own, registered with the workspace. Uninstalling on Windows with Delete the application data ticked removes the sandbox too (an installer hook; not on an update, which keeps everything). And in the Runner window a pressed fix shows a spinner and stays pressed until the status that follows, rather than looking dead until something happens. The desktop guide says so.577 some("This computer already has a g1t-runner sandbox that this copy of g1t has no record of making, such as one from before g1t was installed again, or one started with `g1t-runner sandbox up`. A computer has room for one."),
578 some("Remove it and start again: g1t removes it, with the jobs' VM and its registration, and makes its own, registered with your workspace. Its old runners stay listed on g1t, offline, until you remove them there. Or keep it, and turn off running jobs in g1t."),
579 vec![REMOVE_SANDBOX, LEARN_MORE],
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.580 ),
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.581 Code::Updating => (Tone::Working, "Updating".into(), detail, some("The runner starts again as soon as it's done."), vec![]),
582 Code::UpdateFailed => (
583 Tone::Warning,
584 "Update failed".into(),
585 detail,
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.586 some("The runner keeps running the version it has, and g1t tries the update again within the hour. Check again tries now; if it keeps failing, report it."),
587 vec![CHECK_AGAIN, REPORT],
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.588 ),
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.589 Code::UpdateRefused => {
590 let why = detail.unwrap_or_else(|| "its signature didn't check out".into());
591 let tampered = format!(
592 "If this keeps happening, something between this computer and {} is changing downloads, such as a proxy or antivirus: report it.",
593 facts.server
594 );
595 match facts.update.installed.as_deref() {
596 Some(version) => (
597 Tone::Warning,
598 "Update refused: not signed by g1t".into(),
599 some(format!("The newest g1t-runner release from {} isn't signed with g1t's release key ({why}), so g1t didn't install it.", facts.server)),
600 some(format!("The runner keeps running g1t-runner {version}, and g1t checks again within the hour. {tampered}")),
601 vec![CHECK_AGAIN, REPORT],
602 ),
603 None => (
604 Tone::Problem,
605 "Not running: the runner's release isn't signed by g1t".into(),
606 some(format!("The g1t-runner release from {} isn't signed with g1t's release key ({why}), so g1t didn't install it, and has no runner to start.", facts.server)),
607 some(format!("g1t tries again by itself in a few minutes; Retry tries now. {tampered}")),
608 vec![RETRY, REPORT],
609 ),
610 }
611 }
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.612 Code::Offline => (
613 Tone::Warning,
614 format!("Offline: {} unreachable", facts.server),
615 some(format!("This computer can't reach {}, so the runner can't take jobs.", facts.server)),
616 some("Check the connection. The runner carries on by itself when it's back."),
617 vec![RETRY],
618 ),
619 Code::Crashed => (
620 Tone::Problem,
621 match facts.exit_code {
622 Some(exit) => format!("Runner stopped (exit code {exit})"),
623 None => "Runner stopped".into(),
624 },
625 detail,
626 Some(match facts.wait_seconds {
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.627 Some(wait) => format!("It starts again by itself in {}. If it keeps stopping, its log says why; Restart starts it now.", said_wait(wait)),
628 None => "Restart starts it again; its log says why it stopped.".into(),
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.629 }),
630 vec![RESTART, VIEW_LOG],
631 ),
632 };
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.633 let capabilities = facts.sandbox.as_ref().map(|status| capabilities(status, facts)).unwrap_or_default();
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.634 let in_capabilities = !capabilities.is_empty()
635 && match code {
636 Code::Idle | Code::Busy | Code::KindsStopped => true,
637 Code::Starting | Code::Updating => facts.live,
638 // Said by a card, for its kind of job, word for word.
639 Code::NoWsl
640 | Code::WslMirrored
641 | Code::NoVirtualization
642 | Code::NoVfkit
643 | Code::NoSysbox
644 | Code::WaitingForImage
645 | Code::ImageFailed
646 | Code::SandboxUnavailable
647 | Code::SandboxFailed => cause.is_some() && capabilities.iter().any(|c| c.cause == cause),
648 _ => false,
649 };
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.650 let update = Some(update_line(facts, code));
651 Diagnosis { code, tone, title, cause, fix, actions, capabilities, in_capabilities, update }
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.652}
653
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.654/// What the app calls a sandbox's problem with Linux jobs, from the
655/// launcher's sentence about it, which goes with it as its detail.
656pub fn sandbox_code(why: Why) -> Code {
657 match why {
658 Why::NoWsl => Code::NoWsl,
659 Why::WslMirrored => Code::WslMirrored,
660 Why::NoVirtualization => Code::NoVirtualization,
661 Why::NoVfkit => Code::NoVfkit,
662 Why::NoSysboxOrKvm => Code::NoSysbox,
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.663 Why::NoImages => Code::WaitingForImage,
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.664 Why::Image => Code::ImageFailed,
665 _ => Code::SandboxFailed,
666 }
667}
668
669/// The code for what is wrong with Linux jobs, as a status says it, and
670/// the launcher's own words for it.
671pub fn sandbox_trouble(status: &Status) -> Option<(Code, String, Option<String>)> {
672 let (cause, fix) = status.trouble()?;
673 let code = match launcher::why(&cause) {
674 Why::Other if !status.linux().available => Code::SandboxUnavailable,
675 why => sandbox_code(why),
676 };
677 Some((code, cause, fix))
678}
679
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.680/// Each kind of job the sandbox runs, as a card of its own.
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.681fn capabilities(status: &Status, facts: &Facts) -> Vec<CapabilityLine> {
682 let listed = if status.capabilities.is_empty() { vec![status.linux()] } else { status.capabilities.clone() };
683 listed.iter().map(|capability| capability_line(status, capability, facts)).collect()
684}
685
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.686/// `Linux jobs`.
687pub fn kind_label(id: &str) -> String {
688 match id {
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.689 "linux" => "Linux jobs".to_owned(),
690 "windows" => "Windows jobs".to_owned(),
691 "macos" => "macOS jobs".to_owned(),
692 other => format!("{other} jobs"),
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 }
694}
695
696/// What a kind of job is, in a line, by how the sandbox makes it.
697fn about(id: &str, backend: Option<&str>) -> String {
698 match (id, backend) {
699 ("linux", Some("sysbox")) => "The runner under Sysbox, each job in a fresh container".into(),
700 ("linux", _) => "A Linux VM of its own, each job in a fresh container".into(),
701 ("windows", _) => "Each job in a Windows Sandbox, discarded after".into(),
702 ("macos", _) => "Each job in a macOS VM of its own, discarded after".into(),
703 _ => "Each job sandboxed".into(),
704 }
705}
706
707/// A catalogue title as a card's headline: `Not running: virtualization is off` is `Virtualization is off`.
708fn headline(title: &str) -> String {
709 let said = title.strip_prefix("Not running: ").unwrap_or(title);
710 let mut letters = said.chars();
711 letters.next().map(|first| first.to_uppercase().chain(letters).collect()).unwrap_or_default()
712}
713
714fn capability_line(status: &Status, capability: &launcher::Capability, facts: &Facts) -> CapabilityLine {
715 let id = capability.id.as_str();
716 let label = kind_label(id);
717 let enabled = !facts.off.iter().any(|off| off == id);
718 let per_kind = status.per_kind();
719 let mut line = CapabilityLine {
720 id: id.to_owned(),
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.721 label: label.clone(),
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.722 tone: Tone::Off,
723 state: KindState::Stopped,
724 title: "Stopped".into(),
725 about: about(id, status.backend.as_deref()),
726 headline: None,
727 cause: None,
728 fix: None,
729 actions: vec![],
730 enabled,
731 available: capability.available,
732 // Nothing to start or stop where it can't run, nor while the whole runner is
733 // stopped, whose own Start starts it (one the person stopped can be started).
734 can_toggle: per_kind && (capability.available || !enabled) && !(facts.held && enabled),
735 job: None,
736 progress: None,
737 isolation: capability.isolation.clone().filter(|_| capability.available),
738 runner: capability.available.then(|| format!("{}-{id}", facts.machine)),
739 version: capability.runner_version.clone(),
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.740 update: waiting(&facts.update, id, capability.job.is_some()),
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.741 };
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.742 let said = |line: &mut CapabilityLine, state: KindState, tone: Tone, cause: Option<String>, fix: Option<String>, actions: Vec<Action>| {
743 line.state = state;
744 line.tone = tone;
745 line.title = match state {
746 KindState::Busy => "Running a job",
747 KindState::Idle => "Waiting for jobs",
748 KindState::Starting => "Starting",
749 KindState::Downloading => "Downloading image",
750 KindState::Updating => "Updating",
751 KindState::Waiting => "Waiting for image",
752 KindState::Stopped => "Stopped",
753 KindState::Unavailable => "Not available",
754 KindState::Problem => "Problem",
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.755 }
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.756 .into();
757 line.cause = cause;
758 line.fix = fix;
759 line.actions = actions;
760 };
761 let again = |what: &str| format!("g1t {what} again by itself in a few minutes; Retry {what} now.");
762 let starting = facts.starting.iter().any(|kind| kind == id) || matches!(capability.state, State::Starting);
763
764 // Stopped by the person: nothing is wrong with it.
765 if capability.off() || (!enabled && per_kind) {
766 said(&mut line, KindState::Stopped, Tone::Off, some(format!("You stopped {label} on this computer. They stay stopped until you start them.")), None, vec![]);
767 return line;
768 }
769 // This computer can't run it, and why.
770 if !capability.available {
771 if id == "linux"
772 && let Some((code, cause, fix)) = sandbox_trouble(status)
773 {
774 let whole = diagnose(code, &Facts { detail: Some(cause), fix, sandbox: None, ..facts.clone() });
775 said(&mut line, KindState::Unavailable, Tone::Warning, whole.cause, whole.fix, whole.actions);
776 line.headline = Some(headline(&whole.title));
777 return line;
778 }
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.779 let cause = capability.cause.clone();
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.780 let (headline, cause, fix, actions) = match cause.as_deref().map(launcher::why) {
781 Some(Why::WindowsSandboxOff) => (
782 "Windows Sandbox is off",
783 some("Windows Sandbox is turned off, or this edition of Windows has none: Home doesn't."),
784 some("On Windows 11 Pro, Enterprise or Education, turn on Windows Sandbox in Windows features, then restart."),
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.785 vec![WINDOWS_FEATURES],
786 ),
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.787 Some(Why::NoWsb) => (
788 "Windows needs an update",
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.789 some("This Windows has Windows Sandbox, but not its wsb command, which came with Windows 11 24H2."),
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.790 some("Update Windows to 24H2 or later, then restart g1t."),
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.791 vec![],
792 ),
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.793 Some(Why::MacNotReleased) => (
794 "Not released yet",
795 some("Sandboxed macOS jobs aren't released yet."),
796 some("They come with a g1t-runner update, which g1t downloads by itself. There's nothing for you to do."),
797 vec![],
798 ),
799 _ => ("Can't run here", cause, capability.fix.clone(), vec![LEARN_MORE]),
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 };
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.801 said(&mut line, KindState::Unavailable, Tone::Off, cause, fix, actions);
802 line.headline = Some(headline.into());
803 return line;
804 }
805 // Linux jobs that should run and don't, in the catalogue's words.
806 if id == "linux"
807 && !starting
808 && !matches!(capability.state, State::Updating)
809 && let Some((code, cause, fix)) = sandbox_trouble(status)
810 {
811 let wait_seconds = if facts.live { None } else { facts.wait_seconds };
812 let whole = diagnose(code, &Facts { detail: Some(cause), fix, wait_seconds, sandbox: None, ..facts.clone() });
813 let state = if code == Code::WaitingForImage { KindState::Waiting } else { KindState::Problem };
814 said(&mut line, state, whole.tone, whole.cause, whole.fix, whole.actions);
815 line.headline = Some(headline(&whole.title));
816 return line;
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.817 }
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.818 // A native kind of job whose runner didn't start, in the launcher's words.
819 if id != "linux" && capability.state == State::Error && !starting {
820 let cause = capability.cause.clone().or_else(|| some("Its runner didn't start, and didn't say why."));
821 said(&mut line, KindState::Problem, Tone::Problem, cause, some(again("tries")), vec![RETRY, VIEW_LOG]);
822 line.headline = Some("Its runner didn't start".into());
823 return line;
824 }
825 let downloading = facts.download.as_ref().filter(|_| id == "linux");
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.826 match capability.state {
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.827 _ if starting || capability.state == State::Updating => match downloading {
828 Some(download) => {
829 said(&mut line, KindState::Downloading, Tone::Working, some("Downloading and checking the sandbox's image, once: it takes a few minutes."), None, vec![]);
830 line.progress = Some(download.percent);
831 }
832 None if capability.state == State::Updating => said(&mut line, KindState::Updating, Tone::Working, some("Taking the newest sandbox image; it takes jobs again when it's done."), None, vec![]),
833 None if capability.state == State::Absent => said(&mut line, KindState::Starting, Tone::Working, some(format!("Registering it with {}.", facts.workspace.as_deref().unwrap_or("your workspace"))), None, vec![]),
834 None => said(&mut line, KindState::Starting, Tone::Working, None, None, vec![]),
835 },
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.836 State::Running => match &capability.job {
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.837 Some(job) => {
838 said(&mut line, KindState::Busy, Tone::Working, None, None, vec![]);
839 let page = if job.kind == "agent" { "agents" } else { "actions" };
840 let link = (!job.repo.is_empty() && !facts.site.is_empty()).then(|| format!("{}/{}/{page}", facts.site.trim_end_matches('/'), job.repo));
841 line.job = Some(JobLine { kind: job.kind.clone(), name: job.name.clone(), repo: job.repo.clone(), link });
842 }
843 None => said(&mut line, KindState::Idle, Tone::Ok, None, None, vec![]),
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.844 },
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.845 // Up while the others run, yet not running: it stopped by itself,
846 // or is to be registered.
847 State::Stopped | State::Absent if facts.live && status.running() => {
848 if capability.state == State::Absent {
849 said(&mut line, KindState::Starting, Tone::Working, some(format!("Registering it with {}.", facts.workspace.as_deref().unwrap_or("your workspace"))), None, vec![]);
850 } else {
851 said(&mut line, KindState::Problem, Tone::Problem, some("Its runner stopped by itself."), some(again("starts it")), vec![RETRY, VIEW_LOG]);
852 line.headline = Some("Its runner stopped".into());
853 }
854 }
855 State::Stopped | State::Absent => said(&mut line, KindState::Stopped, Tone::Off, None, None, vec![]),
856 State::Error | State::Unknown | State::Starting | State::Updating => {
857 said(&mut line, KindState::Problem, Tone::Problem, capability.cause.clone().or_else(|| status.error.clone()), capability.fix.clone(), vec![RETRY, VIEW_LOG]);
858 }
859 }
860 // The person stopped it, and this launcher can't stop one kind alone yet.
861 if !enabled && line.fix.is_none() {
862 line.fix = some(format!("This g1t-runner can't stop {label} on their own; the next one can, and g1t downloads it by itself."));
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.863 }
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.864 line
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.865}
866
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.867/// The runner in a few words, for the chip beside the title bar's settings
868/// button, which opens the Runner window: `Runner running`, `Running 2 jobs`.
869#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
870pub struct Chip {
871 pub tone: Tone,
872 pub label: String,
873}
874
875/// The chip, from the whole runner's diagnosis and its kinds of job: what
876/// it is doing, and whether anything there wants the person.
877pub fn chip(diagnosis: &Diagnosis) -> Chip {
878 let jobs = diagnosis.capabilities.iter().filter(|kind| kind.job.is_some()).count().max(usize::from(diagnosis.code == Code::Busy));
879 let wanting = diagnosis.capabilities.iter().any(|kind| kind.enabled && matches!(kind.tone, Tone::Problem | Tone::Warning));
880 let (tone, label) = match diagnosis.code {
881 _ if jobs > 1 => (Tone::Working, format!("Running {jobs} jobs")),
882 _ if jobs == 1 => (Tone::Working, "Running a job".to_owned()),
883 Code::Idle => (if wanting { Tone::Warning } else { Tone::Ok }, "Runner running".to_owned()),
884 Code::Starting | Code::Registering => (Tone::Working, "Runner starting".to_owned()),
885 Code::Updating => (Tone::Working, "Runner updating".to_owned()),
886 Code::UpdateFailed | Code::UpdateRefused => (Tone::Warning, "Runner running".to_owned()),
887 Code::Off => (Tone::Off, "Runner off".to_owned()),
888 Code::Stopped | Code::KindsStopped => (Tone::Off, "Runner stopped".to_owned()),
889 Code::Offline => (Tone::Warning, "Runner offline".to_owned()),
890 _ => (diagnosis.tone, "Runner needs you".to_owned()),
891 };
892 Chip { tone, label }
893}
894
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.895/// What the site says of this computer while g1t is closed on it.
896pub fn closed(facts: &Facts) -> Diagnosis {
897 Diagnosis {
898 code: Code::Off,
899 tone: Tone::Off,
900 title: format!("g1t is closed on {}", facts.machine),
901 cause: some(format!("This runner runs while g1t is open on {}.", facts.machine)),
902 fix: some(format!("Open g1t on {} to run jobs there again.", facts.machine)),
903 actions: vec![],
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.904 capabilities: vec![],
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.905 in_capabilities: false,
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.906 update: 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.907 }
908}
909
910#[cfg(test)]
911mod tests {
912 use super::*;
913
914 fn facts() -> Facts {
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.915 Facts {
916 workspace: Some("acme".into()),
917 backend: "sandboxed",
918 machine: "build-box".into(),
919 server: "g1t.sh".into(),
920 site: "https://g1t.sh".into(),
921 platform: "windows".into(),
922 ..Facts::default()
923 }
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.924 }
925
926 #[test]
Merge the desktop app's settings and runner in the main window: the runner is a dot beside the title bar's Settings button with a menu of its own (its state, each kind of job and what it runs, Start and Stop, updates, the log), Settings opens as a dialog in the window with its sections down the side, the separate windows stay only for when the site isn't showing, and every button that asks the app for something shows it is working until something happens.927 fn a_fix_that_changes_the_runner_waits_for_its_status() {
928 for action in [START, TURN_ON, RETRY, CHECK_NOW, CHECK_UPDATES, CHECK_AGAIN, REMOVE_SANDBOX, RESTART, OPEN_DOCKER] {
929 assert!(action.waits, "{} waits for the status that follows", action.id);
930 }
931 for action in [SIGN_IN, CHOOSE_WORKSPACE, CHOOSE_BACKEND, GET_DOCKER, COPY_WSL_INSTALL, LEARN_MORE, VIEW_LOG, VIEW_RUNNERS] {
932 assert!(!action.waits, "{} is done once the app has it", action.id);
933 }
934 // Only what can't be undone asks first.
935 assert!(REMOVE_SANDBOX.confirm.is_some_and(|question| question.ends_with('.') && question.contains('?')));
936 assert!(RETRY.confirm.is_none());
937 let said = serde_json::to_value(RETRY).unwrap();
938 assert_eq!(said, serde_json::json!({ "id": "retry", "label": "Retry", "waits": true }));
939 }
940
941 #[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.942 fn every_failure_says_why_and_how_to_fix_it() {
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.943 for code in [
944 Code::SignedOut,
945 Code::NoWorkspace,
946 Code::NotAllowed,
947 Code::RegisterFailed,
948 Code::NoDocker,
949 Code::NoLauncher,
950 Code::NoWsl,
951 Code::WslMirrored,
952 Code::NoVirtualization,
953 Code::NoVfkit,
954 Code::NoSysbox,
955 Code::WaitingForImage,
956 Code::ImageFailed,
957 Code::SandboxUnavailable,
958 Code::SandboxFailed,
959 Code::SandboxTaken,
960 Code::UpdateFailed,
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.961 Code::UpdateRefused,
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.962 Code::Offline,
963 Code::Crashed,
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.964 ] {
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.965 let diagnosis = diagnose(code, &Facts { detail: Some("why".into()), ..facts() });
966 assert!(diagnosis.cause.is_some(), "{code:?} has no cause");
967 assert!(diagnosis.fix.is_some(), "{code:?} has no fix");
968 assert!(!diagnosis.actions.is_empty(), "{code:?} has no action");
969 assert!(matches!(diagnosis.tone, Tone::Warning | Tone::Problem), "{code:?}");
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.970 // No fix sends the person off to check their connection or report it
971 // when that isn't what is wrong.
972 let fix = diagnosis.fix.unwrap();
973 if !matches!(code, Code::Offline | Code::ImageFailed) {
974 assert!(!fix.contains("check the connection") && !fix.contains("Check the connection"), "{code:?}: {fix}");
975 }
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.976 }
977 }
978
979 #[test]
980 fn the_words_name_the_workspace_the_machine_and_the_job() {
981 let not_allowed = diagnose(Code::NotAllowed, &facts());
982 assert_eq!(not_allowed.cause.as_deref(), Some("You can't add runners to acme: only its owners can."));
983 assert_eq!(not_allowed.actions[0].id, "choose_workspace");
984 let busy = diagnose(Code::Busy, &Facts { job: Some("Build (acme/web)".into()), ..facts() });
985 assert_eq!(busy.title, "Running Build (acme/web)");
986 assert_eq!(busy.cause.as_deref(), Some("Running sandboxed for acme on build-box."));
987 let crashed = diagnose(Code::Crashed, &Facts { exit_code: Some(101), wait_seconds: Some(30), ..facts() });
988 assert_eq!(crashed.title, "Runner stopped (exit code 101)");
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.989 assert_eq!(crashed.fix.as_deref(), Some("It starts again by itself in 30 seconds. If it keeps stopping, its log says why; Restart starts it 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.990 assert_eq!(crashed.actions.iter().map(|a| a.id).collect::<Vec<_>>(), ["restart", "view_log"]);
991 assert_eq!(diagnose(Code::NoDocker, &facts()).actions[0].id, "open_docker");
992 assert_eq!(closed(&facts()).title, "g1t is closed on build-box");
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.993 assert_eq!(said_wait(300), "5 minutes");
994 assert_eq!(said_wait(45), "45 seconds");
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers 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 }
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
997 fn status(json: &str) -> Status {
998 launcher::parse(json).unwrap()
999 }
1000
1001 const NO_WSL_PRO: &str = r#"{"schema":1,"state":"error","backend":"wsl2","launcher_version":"0.2.1",
1002 "error":"WSL is not installed, and the sandbox for Linux jobs is a WSL distribution of its own.",
1003 "capabilities":[
1004 {"id":"linux","available":false,"state":"error","isolation":"nested-container",
1005 "cause":"WSL is not installed, and the sandbox for Linux jobs is a WSL distribution of its own.",
1006 "fix":"Open a terminal as administrator and run `wsl --install --no-distribution`, then restart Windows. It works on every edition of Windows 11, Home included."},
1007 {"id":"windows","available":true,"state":"running","isolation":"vm","runner_version":"0.2.1","job":null}]}"#;
1008
1009 #[test]
1010 fn without_wsl_the_fix_is_the_command_to_copy_and_windows_jobs_still_show() {
1011 let status = status(NO_WSL_PRO);
1012 let (code, cause, _) = sandbox_trouble(&status).unwrap();
1013 assert_eq!(code, Code::NoWsl);
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.1014 let diagnosis = diagnose(code, &Facts { detail: Some(cause), sandbox: Some(status), live: true, ..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.1015 assert_eq!(diagnosis.title, "Not running: WSL isn't installed");
1016 assert_eq!(diagnosis.fix.as_deref(), Some("Run `wsl --install --no-distribution` in an admin terminal, then restart Windows. It works on every edition of Windows 11, Home included."));
1017 assert_eq!(diagnosis.actions[0], COPY_WSL_INSTALL);
1018 let [linux, windows] = &diagnosis.capabilities[..] else { panic!("{:?}", diagnosis.capabilities) };
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.1019 assert_eq!((linux.label.as_str(), linux.title.as_str(), linux.state), ("Linux jobs", "Not available", KindState::Unavailable));
1020 assert_eq!(linux.headline.as_deref(), Some("WSL isn't installed"));
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.1021 assert_eq!(linux.fix, diagnosis.fix);
1022 assert_eq!(linux.actions[0].id, "copy_wsl_install");
1023 assert_eq!((windows.label.as_str(), windows.title.as_str(), windows.tone), ("Windows jobs", "Waiting for jobs", Tone::Ok));
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.1024 assert_eq!(windows.runner.as_deref(), Some("build-box-windows"));
1025 assert_eq!(windows.about, "Each job in a Windows Sandbox, discarded after");
1026 // The Linux card says it all: the whole runner's line isn't shown as well.
1027 assert!(diagnosis.in_capabilities);
1028 // An older launcher can't stop one kind of job alone.
1029 assert!(!windows.can_toggle);
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.1030 }
1031
1032 #[test]
1033 fn on_home_windows_jobs_are_unavailable_and_linux_jobs_still_run() {
1034 let status = status(
1035 r#"{"schema":1,"state":"running","backend":"wsl2","runner_version":"0.3.0","launcher_version":"0.3.0",
1036 "job":{"kind":"workflow","id":"job_1","name":"build","repo":"acme/web"},
1037 "capabilities":[
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.1038 {"id":"linux","available":true,"state":"running","runner_version":"0.3.0","isolation":"nested-container","turned_off":false,"job":{"kind":"workflow","id":"job_1","name":"build","repo":"acme/web"}},
1039 {"id":"windows","available":false,"state":"absent","turned_off":false,"cause":"Windows Sandbox is not turned on, or this edition of Windows has none (Home does not).","fix":"On Windows 11 Pro…"}]}"#,
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.1040 );
1041 assert_eq!(sandbox_trouble(&status), 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.1042 let busy = diagnose(Code::Busy, &Facts { job: status.job().map(launcher::Job::said), sandbox: Some(status), live: true, ..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.1043 assert_eq!(busy.title, "Running build (acme/web)");
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.1044 assert!(busy.in_capabilities);
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1045 let [linux, windows] = &busy.capabilities[..] else { panic!() };
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.1046 assert_eq!((linux.title.as_str(), linux.state, linux.tone), ("Running a job", KindState::Busy, Tone::Working));
1047 let job = linux.job.as_ref().unwrap();
1048 assert_eq!((job.name.as_str(), job.repo.as_str(), job.link.as_deref()), ("build", "acme/web", Some("https://g1t.sh/acme/web/actions")));
1049 assert!(linux.can_toggle && linux.enabled);
1050 assert_eq!((windows.title.as_str(), windows.tone, windows.state), ("Not available", Tone::Off, KindState::Unavailable));
1051 assert_eq!(windows.headline.as_deref(), Some("Windows Sandbox is off"));
1052 assert_eq!(windows.cause.as_deref(), Some("Windows Sandbox is turned off, or this edition of Windows has none: Home doesn't."));
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.1053 assert_eq!(windows.actions[0].id, "windows_features");
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.1054 // Nothing to start or stop where it can't run.
1055 assert!(!windows.can_toggle);
1056 assert_eq!(windows.runner, None);
1057 }
1058
1059 /// 0.3.0 was published without the sandbox's images: Linux jobs wait for
1060 /// them, plainly, while Windows jobs run.
1061 const NO_IMAGES: &str = r#"{"schema":1,"state":"error","backend":"wsl2","launcher_version":"0.3.0",
1062 "error":"Windows jobs are sandboxed and running, but Linux jobs are not: release 0.3.0 has no sandbox images",
1063 "capabilities":[
1064 {"id":"linux","available":true,"state":"error","isolation":"nested-container","turned_off":false},
1065 {"id":"windows","available":true,"state":"running","isolation":"vm","runner_version":"0.3.0","turned_off":false}]}"#;
1066
1067 #[test]
1068 fn a_release_without_images_is_waited_for_not_reported() {
1069 let status = status(NO_IMAGES);
1070 let (code, cause, _) = sandbox_trouble(&status).unwrap();
1071 assert_eq!(code, Code::WaitingForImage);
1072 let whole = diagnose(code, &Facts { detail: Some(cause), ..facts() });
1073 assert_eq!(whole.title, "Waiting for the sandbox's image");
1074 assert_eq!(whole.cause.as_deref(), Some("g1t-runner 0.3.0 is published without the sandbox's images yet, so Linux jobs can't start."));
1075 assert_eq!(whole.fix.as_deref(), Some("g1t checks again every few minutes and starts Linux jobs once they're out. There's nothing for you to do."));
1076 assert_eq!(whole.actions, [CHECK_NOW]);
1077 assert!(!whole.fix.unwrap().contains("report"));
1078 // Windows jobs run: the runner waits for jobs, and the Linux card says what it waits for.
1079 let idle = diagnose(Code::Idle, &Facts { sandbox: Some(status), live: true, ..facts() });
1080 assert!(idle.in_capabilities);
1081 let [linux, windows] = &idle.capabilities[..] else { panic!() };
1082 assert_eq!((linux.state, linux.title.as_str(), linux.tone), (KindState::Waiting, "Waiting for image", Tone::Warning));
1083 assert_eq!(linux.headline.as_deref(), Some("Waiting for the sandbox's image"));
1084 assert_eq!(windows.state, KindState::Idle);
1085 let image = diagnose(Code::ImageFailed, &Facts { detail: Some("the sandbox image wsl-x64.tar.gz's SHA-256 is not the release's; not using it".into()), ..facts() });
1086 assert_eq!(image.title, "Not running: the sandbox's image didn't check out");
1087 let fetch = diagnose(Code::ImageFailed, &Facts { detail: Some("could not fetch https://g1t.sh/x: dns error".into()), wait_seconds: Some(60), ..facts() });
1088 assert_eq!(fetch.cause.as_deref(), Some("The sandbox's image couldn't be downloaded: could not fetch https://g1t.sh/x: dns error"));
1089 assert_eq!(fetch.fix.as_deref(), Some("g1t tries again by itself in 60 seconds. Check that this computer can reach g1t.sh; Retry tries 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.1090 }
1091
1092 #[test]
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.1093 fn a_kind_the_person_stopped_says_so_and_is_not_failing() {
1094 let status = status(
1095 r#"{"schema":1,"state":"running","backend":"wsl2","capabilities":[
1096 {"id":"linux","available":true,"state":"running","turned_off":false},
1097 {"id":"windows","available":true,"state":"stopped","turned_off":true}]}"#,
1098 );
1099 let idle = diagnose(Code::Idle, &Facts { sandbox: Some(status.clone()), live: true, off: vec!["windows".into()], ..facts() });
1100 let [linux, windows] = &idle.capabilities[..] else { panic!() };
1101 assert_eq!(linux.state, KindState::Idle);
1102 assert_eq!((windows.state, windows.tone, windows.enabled), (KindState::Stopped, Tone::Off, false));
1103 assert_eq!(windows.cause.as_deref(), Some("You stopped Windows jobs on this computer. They stay stopped until you start them."));
1104 assert!(windows.can_toggle && windows.actions.is_empty());
1105 // Stopped by itself while Linux jobs run is a problem, with Retry.
1106 let died = launcher::parse(&serde_json::to_string(&serde_json::json!({"schema":1,"state":"running","capabilities":[
1107 {"id":"linux","available":true,"state":"running","turned_off":false},
1108 {"id":"windows","available":true,"state":"stopped","turned_off":false}]})).unwrap()).unwrap();
1109 let idle = diagnose(Code::Idle, &Facts { sandbox: Some(died.clone()), live: true, ..facts() });
1110 assert_eq!((idle.capabilities[1].state, idle.capabilities[1].actions[0].id), (KindState::Problem, "retry"));
1111 // With the whole runner stopped, it is only stopped.
1112 let stopped = diagnose(Code::Stopped, &Facts { sandbox: Some(died), live: false, held: true, ..facts() });
1113 // Its own Start or Stop waits for the runner's: the line above the cards starts it.
1114 assert!(!stopped.capabilities[0].can_toggle);
1115 assert_eq!(stopped.capabilities[1].state, KindState::Stopped);
1116 assert!(!stopped.in_capabilities);
1117 }
1118
1119 #[test]
1120 fn a_download_shows_how_far_it_is() {
1121 let status = status(r#"{"schema":1,"state":"starting","capabilities":[{"id":"linux","available":true,"state":"starting","turned_off":false},{"id":"windows","available":true,"state":"absent","turned_off":false}]}"#);
1122 let facts = Facts { sandbox: Some(status), live: true, starting: vec!["linux".into(), "windows".into()], download: Some(launcher::Download { key: "wsl-x64".into(), percent: 40 }), ..facts() };
1123 let starting = diagnose(Code::Starting, &facts);
1124 assert!(starting.in_capabilities);
1125 let [linux, windows] = &starting.capabilities[..] else { panic!() };
1126 assert_eq!((linux.state, linux.title.as_str(), linux.progress), (KindState::Downloading, "Downloading image", Some(40)));
1127 assert_eq!((windows.state, windows.progress), (KindState::Starting, None));
1128 }
1129
1130 #[test]
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.1131 fn each_launcher_trouble_has_its_words() {
1132 let trouble = |linux_available: bool, text: &str| {
1133 let json = serde_json::json!({
1134 "schema": 1, "state": "error", "error": text,
1135 "capabilities": [{"id": "linux", "available": linux_available, "state": "error", "cause": if linux_available { None } else { Some(text) }, "fix": "the launcher's fix"}]
1136 });
1137 sandbox_trouble(&status(&json.to_string())).unwrap()
1138 };
1139 assert_eq!(trouble(true, "WSL could not import the sandbox: Please enable the Virtual Machine Platform Windows feature and ensure virtualization is enabled in the BIOS.").0, Code::NoVirtualization);
1140 assert_eq!(trouble(false, "vfkit, which runs the sandbox's VM with Apple's Virtualization framework, is not installed.").0, Code::NoVfkit);
1141 assert_eq!(trouble(false, "This machine has neither Sysbox nor KVM, so there is nothing to sandbox the runner with.").0, Code::NoSysbox);
1142 assert_eq!(trouble(false, "WSL is set to mirrored networking, in which Linux reaches this computer's own localhost.").0, Code::WslMirrored);
1143 assert_eq!(trouble(true, "the sandbox image wsl-x64.tar.gz's SHA-256 is not the release's; not using it").0, Code::ImageFailed);
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.1144 assert_eq!(trouble(true, "release 0.3.0 has no sandbox images").0, Code::WaitingForImage);
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.1145 // Unknown words: shown as the launcher wrote them, its fix with them.
1146 let (code, cause, fix) = trouble(false, "This user may not use /dev/kvm.");
1147 assert_eq!(code, Code::SandboxUnavailable);
1148 let unavailable = diagnose(code, &Facts { detail: Some(cause), fix, ..facts() });
1149 assert_eq!(unavailable.cause.as_deref(), Some("This user may not use /dev/kvm."));
1150 assert_eq!(unavailable.fix.as_deref(), Some("the launcher's fix"));
1151 let (code, cause, _) = trouble(true, "the sandbox did not start within 600 seconds");
1152 assert_eq!(code, Code::SandboxFailed);
1153 let failed = diagnose(code, &Facts { detail: Some(cause), wait_seconds: Some(60), ..facts() });
1154 assert_eq!(failed.cause.as_deref(), Some("the sandbox did not start within 600 seconds"));
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.1155 assert_eq!(failed.fix.as_deref(), Some("It starts again by itself in 60 seconds. If it keeps stopping, its log says why; Retry starts it 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.1156 assert_eq!(diagnose(Code::NoVirtualization, &Facts { platform: "macos".into(), ..facts() }).actions[0].id, "retry");
1157 assert_eq!(diagnose(Code::NoVfkit, &facts()).actions[0].id, "copy_vfkit_install");
1158 }
1159
1160 #[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.1161 fn when_g1t_last_looked_for_a_runner_is_said_plainly() {
1162 let ago = |seconds: u64| checked(&Update { checked_ago: Some(seconds), ..Update::default() });
1163 assert_eq!(checked(&Update::default()), "Not checked yet");
1164 assert_eq!(checked(&Update { checking: true, checked_ago: Some(30), ..Update::default() }), "Checking for updates…");
1165 assert_eq!(ago(20), "Checked just now");
1166 assert_eq!(ago(12 * 60 + 5), "Checked 12 min ago");
1167 assert_eq!(ago(3600), "Checked 1 hour ago");
1168 assert_eq!(ago(5 * 3600), "Checked 5 hours ago");
1169 assert_eq!(ago(2 * 86_400), "Checked 2 days ago");
1170 }
1171
1172 /// 0.3.2 is downloaded: Linux jobs take its image after the job they
1173 /// are running, Windows jobs, idle, take its program now.
1174 #[test]
1175 fn a_waiting_update_is_said_on_each_kind_of_jobs_card() {
1176 let status = status(
1177 r#"{"schema":1,"state":"running","backend":"wsl2","image_version":"0.3.1","capabilities":[
1178 {"id":"linux","available":true,"state":"running","runner_version":"0.3.1","turned_off":false,"job":{"kind":"workflow","id":"j","name":"build","repo":"acme/web"}},
1179 {"id":"windows","available":true,"state":"running","runner_version":"0.3.1","turned_off":false}]}"#,
1180 );
1181 let update = Update {
1182 installed: Some("0.3.1".into()),
1183 checked_at: Some("2026-10-12T12:00:00Z".into()),
1184 checked_ago: Some(12 * 60),
1185 ready: Some("0.3.2".into()),
1186 waiting: vec!["linux".into(), "windows".into()],
1187 ..Update::default()
1188 };
1189 let busy = diagnose(Code::Busy, &Facts { sandbox: Some(status.clone()), live: true, update: update.clone(), ..facts() });
1190 let [linux, windows] = &busy.capabilities[..] else { panic!() };
1191 assert_eq!(linux.version.as_deref(), Some("0.3.1"));
1192 assert_eq!(linux.update.as_deref(), Some("0.3.2 ready, installs after this job"));
1193 assert_eq!(windows.update.as_deref(), Some("0.3.2 ready, installs in a moment"));
1194 let line = busy.update.unwrap();
1195 assert_eq!((line.checked.as_str(), line.ready.as_deref(), line.checked_at.as_deref()), ("Checked 12 min ago", Some("0.3.2"), Some("2026-10-12T12:00:00Z")));
1196 // The cards say it, each for its own kind: nothing is said for the whole runner.
1197 assert_eq!(line.note, None);
1198 assert_eq!((line.check.id, line.check.label), ("check_updates", "Check for runner updates"));
1199 // Windows jobs stop while the program is replaced: installing, then nothing waits.
1200 let installing = Update { installing: vec!["windows".into()], ..update.clone() };
1201 let busy = diagnose(Code::Busy, &Facts { sandbox: Some(status.clone()), live: true, update: installing, ..facts() });
1202 assert_eq!(busy.capabilities[1].update.as_deref(), Some("Installing 0.3.2"));
1203 let done = Update { waiting: vec!["linux".into()], ..update.clone() };
1204 let busy = diagnose(Code::Busy, &Facts { sandbox: Some(status), live: true, update: done, ..facts() });
1205 assert_eq!(busy.capabilities[1].update, None);
1206 // In Docker or on this computer there are no cards: the whole runner says it.
1207 let docker = Update { waiting: vec!["runner".into()], ..update };
1208 let busy = diagnose(Code::Busy, &Facts { backend: "in Docker", job: Some("build (acme/web)".into()), update: docker.clone(), ..facts() });
1209 assert_eq!(busy.update.unwrap().note.as_deref(), Some("0.3.2 ready, installs after this job"));
1210 let idle = diagnose(Code::Idle, &Facts { backend: "in Docker", update: docker, ..facts() });
1211 assert_eq!(idle.update.unwrap().note.as_deref(), Some("0.3.2 ready, installs in a moment"));
1212 // What is said once g1t is closed knows nothing of updates.
1213 assert_eq!(closed(&facts()).update, None);
1214 }
1215
1216 #[test]
1217 fn a_release_not_signed_by_g1t_is_refused_and_said_so() {
1218 let detail = Some("the release's signature isn't g1t's".to_owned());
1219 let running = diagnose(Code::UpdateRefused, &Facts { detail: detail.clone(), update: Update { installed: Some("0.3.1".into()), ..Update::default() }, ..facts() });
1220 assert_eq!(running.title, "Update refused: not signed by g1t");
1221 assert_eq!(
1222 running.cause.as_deref(),
1223 Some("The newest g1t-runner release from g1t.sh isn't signed with g1t's release key (the release's signature isn't g1t's), so g1t didn't install it.")
1224 );
1225 assert!(running.fix.as_deref().unwrap().starts_with("The runner keeps running g1t-runner 0.3.1, and g1t checks again within the hour."));
1226 assert_eq!(running.actions, [CHECK_AGAIN, REPORT]);
1227 assert_eq!(running.tone, Tone::Warning);
1228 // With no runner yet, nothing runs until a signed one downloads.
1229 let nothing = diagnose(Code::UpdateRefused, &Facts { detail, ..facts() });
1230 assert_eq!(nothing.title, "Not running: the runner's release isn't signed by g1t");
1231 assert_eq!((nothing.tone, nothing.actions[0].id), (Tone::Problem, "retry"));
1232 assert!(!nothing.in_capabilities);
1233 }
1234
1235 #[test]
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.1236 fn the_title_bars_chip_says_the_runner_in_a_few_words() {
1237 let said = |code: Code, facts: &Facts| {
1238 let chip = chip(&diagnose(code, facts));
1239 (chip.tone, chip.label)
1240 };
1241 assert_eq!(said(Code::Idle, &facts()), (Tone::Ok, "Runner running".into()));
1242 assert_eq!(said(Code::Busy, &Facts { job: Some("build (acme/web)".into()), ..facts() }), (Tone::Working, "Running a job".into()));
1243 assert_eq!(said(Code::Starting, &facts()), (Tone::Working, "Runner starting".into()));
1244 assert_eq!(said(Code::Stopped, &facts()), (Tone::Off, "Runner stopped".into()));
1245 assert_eq!(said(Code::Off, &facts()), (Tone::Off, "Runner off".into()));
1246 assert_eq!(said(Code::NoWsl, &facts()), (Tone::Problem, "Runner needs you".into()));
1247 assert_eq!(said(Code::UpdateFailed, &Facts { detail: Some("x".into()), ..facts() }).1, "Runner running");
1248 // Both kinds of job busy at once.
1249 let both = status(
1250 r#"{"schema":1,"state":"running","capabilities":[
1251 {"id":"linux","available":true,"state":"running","turned_off":false,"job":{"kind":"workflow","id":"a","name":"build","repo":"acme/web"}},
1252 {"id":"windows","available":true,"state":"running","turned_off":false,"job":{"kind":"workflow","id":"b","name":"installer","repo":"acme/app"}}]}"#,
1253 );
1254 assert_eq!(said(Code::Busy, &Facts { sandbox: Some(both), live: true, ..facts() }), (Tone::Working, "Running 2 jobs".into()));
1255 // Linux jobs run while Windows jobs can't start: running, and wanting a look.
1256 let one_failing = status(NO_WSL_PRO);
1257 assert_eq!(said(Code::Idle, &Facts { sandbox: Some(one_failing), live: true, ..facts() }), (Tone::Warning, "Runner running".into()));
1258 }
1259
1260 #[test]
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.1261 fn a_diagnosis_without_the_sandbox_lists_no_capabilities() {
1262 let json = serde_json::to_value(diagnose(Code::Idle, &Facts { backend: "in Docker", ..facts() })).unwrap();
1263 assert!(json.get("capabilities").is_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.1264 assert!(json.get("in_capabilities").is_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.1265 let sandboxed = serde_json::to_value(diagnose(Code::Idle, &Facts { sandbox: Some(status(NO_WSL_PRO)), ..facts() })).unwrap();
1266 assert_eq!(sandboxed["capabilities"][1]["id"], "windows");
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.1267 assert_eq!(sandboxed["capabilities"][1]["state"], "idle");
1268 assert_eq!(sandboxed["in_capabilities"], true);
1269 // What affects everything is said once, above the cards.
1270 let signed_out = diagnose(Code::SignedOut, &Facts { sandbox: Some(status(NO_WSL_PRO)), ..facts() });
1271 assert!(!signed_out.in_capabilities);
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.1272 }
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.1273}

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