Skip to content
2,217 linesCodeBlameRaw

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

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

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