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 | ||
| 57 | mod backend; | |
| 58 | pub mod diagnosis; | |
| 59 | mod launcher; | |
| 60 | mod log; | |
| 61 | mod native; | |
| 62 | ||
| 63 | use std::collections::VecDeque; | |
| 64 | use std::fs::{File, OpenOptions}; | |
| 65 | use std::io::{BufRead, BufReader, Read, Write}; | |
| 66 | use 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. | 67 | use 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. | 68 | use std::sync::Mutex; |
| 69 | use 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. | 70 | use 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 | |
| 72 | use serde::{Deserialize, Serialize}; | |
| 73 | use tauri::{AppHandle, Emitter, EventTarget, Manager}; | |
| 74 | use url::Url; | |
| 75 | ||
| 76 | pub use backend::Backend; | |
| 77 | use backend::{Engine, Missing}; | |
| 78 | use 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. | 79 | use 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. | 80 | use 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 | |
| 82 | use crate::{config, frame, settings, tray}; | |
| 83 | ||
| 84 | /// What settings, the tray and the site listen for: the runner's status changed. | |
| 85 | pub const STATUS_EVENT: &str = "g1t-runner-status"; | |
| 86 | /// What the site's page listens for: the app needs a registration token for a workspace. | |
| 87 | pub const WANTED_EVENT: &str = "g1t-runner-wanted"; | |
| 88 | ||
| 89 | /// How long after the window the runner starts. | |
| 90 | const AFTER_START: Duration = Duration::from_secs(4); | |
| 91 | /// How often the thread looks at the runner when nothing has happened. | |
| 92 | const TICK: Duration = Duration::from_secs(5); | |
| 93 | /// How long after starting the first look for an update is, then how often. | |
| 94 | const 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. | 95 | const 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. |
| 97 | const LOOK_AGAIN: Duration = Duration::from_secs(30); | |
| 98 | /// How long the app waits for the site to hand over a token before asking again. | |
| 99 | const ASK_AGAIN: Duration = Duration::from_secs(60); | |
| 100 | /// The longest the app waits before starting a runner that stopped by itself again. | |
| 101 | const 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). | |
| 104 | const BACK_ONLINE: Duration = Duration::from_secs(75); | |
| 105 | /// The runner's log is started over when it is bigger than this. | |
| 106 | const LOG_LIMIT: u64 = 5 * 1024 * 1024; | |
| 107 | /// The last lines of the log settings show. | |
| 108 | const 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)] | |
| 112 | pub 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)] | |
| 120 | struct 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)] | |
| 141 | struct 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. | |
| 156 | struct 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)] | |
| 169 | struct 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)] | |
| 178 | pub 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")] | |
| 193 | pub 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. | |
| 218 | enum 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)] | |
| 244 | struct 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)] | |
| 254 | struct 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 | ||
| 261 | impl 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. | 285 | fn 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. |
| 293 | fn 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. | 297 | impl 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. | |
| 314 | fn 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. | |
| 325 | fn 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. |
| 339 | const 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. | 341 | fn 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. | |
| 371 | fn 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). | |
| 397 | fn 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`. | |
| 402 | fn 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. | 418 | pub 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. | |
| 445 | pub 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. |
| 451 | pub 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. | |
| 457 | fn 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 | ||
| 466 | fn 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. | |
| 473 | pub 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. | |
| 484 | pub fn settings_changed(app: &AppHandle) { | |
| 485 | ask(app, Ask::Look); | |
| 486 | publish(app); | |
| 487 | } | |
| 488 | ||
| 489 | /// Start or Stop, from settings or the tray. | |
| 490 | pub 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. | |
| 504 | pub 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. |
| 522 | pub 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. | |
| 528 | pub 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. | |
| 535 | pub 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. | |
| 551 | pub 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. | |
| 561 | pub 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. | |
| 608 | fn 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. |
| 625 | fn 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. | |
| 647 | fn 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. |
| 655 | pub 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. | |
| 660 | pub 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. | |
| 665 | pub 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. | |
| 676 | enum 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. | 686 | enum 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. | 698 | impl 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. | 708 | struct 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. | 733 | impl 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. | 763 | struct 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)] | |
| 792 | struct 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 | ||
| 799 | fn detail(text: impl Into<String>) -> Extra { | |
| 800 | Extra { detail: Some(text.into()), ..Extra::default() } | |
| 801 | } | |
| 802 | ||
| 803 | impl 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. |
| 2081 | const 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. | |
| 2086 | fn 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. | 2097 | fn 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. | |
| 2103 | fn 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. | 2110 | fn 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. | |
| 2132 | fn 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)] | |
| 2150 | mod 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.