Skip to content

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

g1t.sh
2,278 linesCodeBlameRaw

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

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

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