Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 1 | //! What the runner is doing, and when it is not running or is failing, why |
| 2 | //! and how to fix it: the one catalogue of those words. The tray shows a | |
| 3 | //! diagnosis's title, the settings window all of it with its buttons, and | |
| 4 | //! the site shows it as the app sends it, word for word, on its Runners | |
| 5 | //! page (and keeps it, so a person's other computers are listed with | |
| 6 | //! theirs). The site adds nothing of its own: what it says of a computer | |
| 7 | //! whose g1t is closed is `closed` here, sent with every report. | |
| 8 | //! | |
| 9 | //! Each action is an id the settings window and the site turn into a | |
| 10 | //! button; bridge.rs and settings.js do what it says. | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 11 | //! |
| 12 | //! The sandbox (launcher.rs) says why something is wrong in its own | |
| 13 | //! sentences. Those this catalogue knows (WSL not installed, virtualization | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 14 | //! off, Windows Sandbox off, a release without the sandbox's images, …) |
| 15 | //! are said in its words, with a fix and its buttons; any other is shown as | |
| 16 | //! the launcher wrote it. With the sandbox, each kind of job it runs (Linux | |
| 17 | //! jobs, Windows jobs) is a card of its own (`capabilities`), with its own | |
| 18 | //! state, cause, fix and Start or Stop, since one can run while the other | |
| 19 | //! cannot, or is stopped. The whole runner's diagnosis then only says what | |
| 20 | //! the cards don't (`in_capabilities` is false): signed out, no workspace, | |
| 21 | //! not allowed, no launcher, the whole sandbox down. | |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 22 | |
| 23 | use serde::Serialize; | |
| 24 | ||
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 25 | use super::launcher::{self, State, Status, Why}; |
| 26 | ||
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 27 | /// Every state the runner can be in. |
| 28 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)] | |
| 29 | #[serde(rename_all = "snake_case")] | |
| 30 | pub enum Code { | |
| 31 | /// Running jobs is turned off in settings. | |
| 32 | #[default] | |
| 33 | Off, | |
| 34 | /// The person pressed Stop. | |
| 35 | Stopped, | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 36 | /// Sandboxed, the person stopped every kind of job there is here. |
| 37 | KindsStopped, | |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 38 | Starting, |
| 39 | /// Waiting for the site to make a registration token. | |
| 40 | Registering, | |
| 41 | /// Nobody is signed in to g1t in the app. | |
| 42 | SignedOut, | |
| 43 | /// Signed in, owning no workspace: nowhere to register. | |
| 44 | NoWorkspace, | |
| 45 | /// The person may not add runners to the workspace. | |
| 46 | NotAllowed, | |
| 47 | RegisterFailed, | |
| 48 | Idle, | |
| 49 | Busy, | |
| 50 | NoDocker, | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 51 | /// The `g1t-runner` here is from before its sandbox launcher. |
| 52 | NoLauncher, | |
| 53 | /// Windows, without WSL, which holds the sandbox's Linux VM. | |
| 54 | NoWsl, | |
| 55 | /// WSL set to mirrored networking, which reaches this computer's localhost. | |
| 56 | WslMirrored, | |
| 57 | /// The computer's virtualization is turned off. | |
| 58 | NoVirtualization, | |
| 59 | /// macOS, without vfkit, which runs the sandbox's VM. | |
| 60 | NoVfkit, | |
| 61 | /// Linux, with neither Sysbox in Docker nor KVM. | |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 62 | NoSysbox, |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 63 | /// The newest release is published without the sandbox's images yet. |
| 64 | WaitingForImage, | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 65 | /// The sandbox's image couldn't be downloaded, or isn't the release's. |
| 66 | ImageFailed, | |
| 67 | /// The sandbox can't be made here, for a reason in the launcher's words. | |
| 68 | SandboxUnavailable, | |
| 69 | /// The sandbox failed, in the launcher's words. | |
| 70 | SandboxFailed, | |
| 71 | /// The computer has a sandbox the app didn't make, and has room for one. | |
| 72 | SandboxTaken, | |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 73 | Updating, |
| 74 | UpdateFailed, | |
| 75 | Offline, | |
| 76 | Crashed, | |
| 77 | } | |
| 78 | ||
| 79 | /// How a state should look: a dot's colour. | |
| 80 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] | |
| 81 | #[serde(rename_all = "snake_case")] | |
| 82 | pub enum Tone { | |
| 83 | Ok, | |
| 84 | Working, | |
| 85 | Off, | |
| 86 | Warning, | |
| 87 | Problem, | |
| 88 | } | |
| 89 | ||
| 90 | /// A one-click fix. | |
| 91 | #[derive(Clone, Debug, PartialEq, Eq, Serialize)] | |
| 92 | pub struct Action { | |
| 93 | pub id: &'static str, | |
| 94 | pub label: &'static str, | |
| 95 | } | |
| 96 | ||
| 97 | #[derive(Clone, Debug, PartialEq, Eq, Serialize)] | |
| 98 | pub struct Diagnosis { | |
| 99 | pub code: Code, | |
| 100 | pub tone: Tone, | |
| 101 | /// One line: the tray's, and the heading everywhere else. | |
| 102 | pub title: String, | |
| 103 | /// Why, in a sentence. | |
| 104 | pub cause: Option<String>, | |
| 105 | /// What to do about it. | |
| 106 | pub fix: Option<String>, | |
| 107 | pub actions: Vec<Action>, | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 108 | /// With the sandbox: each kind of job it runs, as a card of its own. |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 109 | #[serde(skip_serializing_if = "Vec::is_empty")] |
| 110 | pub capabilities: Vec<CapabilityLine>, | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 111 | /// The cards say all of this, each for its kind of job: a page that |
| 112 | /// shows them shows no line of the whole runner's as well. | |
| 113 | #[serde(skip_serializing_if = "is_false")] | |
| 114 | pub in_capabilities: bool, | |
| 115 | } | |
| 116 | ||
| 117 | fn is_false(value: &bool) -> bool { | |
| 118 | !*value | |
| 119 | } | |
| 120 | ||
| 121 | /// How one kind of job is, as its card's label says it. | |
| 122 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] | |
| 123 | #[serde(rename_all = "snake_case")] | |
| 124 | pub enum KindState { | |
| 125 | /// Running a job. | |
| 126 | Busy, | |
| 127 | /// Waiting for jobs. | |
| 128 | Idle, | |
| 129 | Starting, | |
| 130 | /// Downloading the sandbox's image, which `progress` says how far. | |
| 131 | Downloading, | |
| 132 | Updating, | |
| 133 | /// Waiting for something g1t sees to by itself, such as the release's images. | |
| 134 | Waiting, | |
| 135 | /// Stopped: by the person (`enabled` is false), or with the whole runner. | |
| 136 | Stopped, | |
| 137 | /// This computer can't run it, and `cause` says why. | |
| 138 | Unavailable, | |
| 139 | /// It should run and doesn't: `cause` and `fix`. | |
| 140 | Problem, | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 141 | } |
| 142 | ||
| 143 | /// One kind of job the sandbox runs, `linux` or `windows` (or `macos`), | |
| 144 | /// and how it is: Linux jobs can run while Windows jobs cannot. | |
| 145 | #[derive(Clone, Debug, PartialEq, Eq, Serialize)] | |
| 146 | pub struct CapabilityLine { | |
| 147 | pub id: String, | |
| 148 | /// `Linux jobs`. | |
| 149 | pub label: String, | |
| 150 | pub tone: Tone, | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 151 | pub state: KindState, |
| 152 | /// Its state in a few words, for its label: `Waiting for jobs`, `Not available`. | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 153 | pub title: String, |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 154 | /// What it is, in a line: `A Linux VM of its own, each job in a fresh container`. |
| 155 | pub about: String, | |
| 156 | /// What is wrong, in a few words, over its cause: `WSL isn't installed`. | |
| 157 | pub headline: Option<String>, | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 158 | pub cause: Option<String>, |
| 159 | pub fix: Option<String>, | |
| 160 | pub actions: Vec<Action>, | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 161 | /// On in settings: the person hasn't stopped it. |
| 162 | pub enabled: bool, | |
| 163 | /// This computer can run it. | |
| 164 | pub available: bool, | |
| 165 | /// It can be started and stopped on its own here (the launcher can). | |
| 166 | pub can_toggle: bool, | |
| 167 | /// What it is running now. | |
| 168 | pub job: Option<JobLine>, | |
| 169 | /// How far its image has downloaded, in percent. | |
| 170 | pub progress: Option<u8>, | |
| 171 | /// How it keeps jobs from the computer: `nested-container`, `vm`. | |
| 172 | pub isolation: Option<String>, | |
| 173 | /// Its runner's name on g1t: `build-box-linux`. | |
| 174 | pub runner: Option<String>, | |
| 175 | /// Its runner's version. | |
| 176 | pub version: Option<String>, | |
| 177 | } | |
| 178 | ||
| 179 | /// The job a kind of job is running, with where to see it on g1t. | |
| 180 | #[derive(Clone, Debug, PartialEq, Eq, Serialize)] | |
| 181 | pub struct JobLine { | |
| 182 | /// `workflow` or `agent`. | |
| 183 | pub kind: String, | |
| 184 | pub name: String, | |
| 185 | pub repo: String, | |
| 186 | /// The repository's Actions (or Agents) page, where its run is. | |
| 187 | pub link: Option<String>, | |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 188 | } |
| 189 | ||
| 190 | /// What the words are filled in with. | |
| 191 | #[derive(Clone, Debug, Default)] | |
| 192 | pub struct Facts { | |
| 193 | pub workspace: Option<String>, | |
| 194 | /// How jobs run, as `sandboxed`, `in Docker`, `on this computer`. | |
| 195 | pub backend: &'static str, | |
| 196 | pub machine: String, | |
| 197 | pub job: Option<String>, | |
| 198 | pub detail: Option<String>, | |
| 199 | pub exit_code: Option<i32>, | |
| 200 | pub wait_seconds: Option<u64>, | |
| 201 | /// The server's host: `g1t.sh`. | |
| 202 | pub server: String, | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 203 | /// The server's address, for links: `https://g1t.sh`. |
| 204 | pub site: String, | |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 205 | pub platform: String, |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 206 | /// What the sandbox says to do, for a cause in its own words. |
| 207 | pub fix: Option<String>, | |
| 208 | /// How the sandbox is, when jobs run sandboxed. | |
| 209 | pub sandbox: Option<Status>, | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 210 | /// The kinds of job the person stopped in settings. |
| 211 | pub off: Vec<String>, | |
| 212 | /// The sandbox is up, or on its way: a kind of job that should run | |
| 213 | /// and doesn't is failing, rather than stopped with everything else. | |
| 214 | pub live: bool, | |
| 215 | /// The kinds of job a launcher command is starting now. | |
| 216 | pub starting: Vec<String>, | |
| 217 | /// How far the sandbox's image has downloaded, while it does. | |
| 218 | pub download: Option<launcher::Download>, | |
| 219 | /// The person stopped the whole runner, or turned it off. | |
| 220 | pub held: 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. | 221 | } |
| 222 | ||
| 223 | const START: Action = Action { id: "start", label: "Start" }; | |
| 224 | const TURN_ON: Action = Action { id: "start", label: "Turn on" }; | |
| 225 | const RETRY: Action = Action { id: "retry", label: "Retry" }; | |
| 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. | 226 | const CHECK_NOW: Action = Action { id: "retry", label: "Check 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. | 227 | const RESTART: Action = Action { id: "restart", label: "Restart" }; |
| 228 | const SIGN_IN: Action = Action { id: "sign_in", label: "Sign in" }; | |
| 229 | const CHOOSE_WORKSPACE: Action = Action { id: "choose_workspace", label: "Choose workspace" }; | |
| 230 | const NEW_WORKSPACE: Action = Action { id: "new_workspace", label: "Create a workspace" }; | |
| 231 | const CHOOSE_BACKEND: Action = Action { id: "choose_backend", label: "Choose how jobs run" }; | |
| 232 | const OPEN_DOCKER: Action = Action { id: "open_docker", label: "Open Docker" }; | |
| 233 | const GET_DOCKER: Action = Action { id: "get_docker", label: "Get Docker" }; | |
| 234 | const INSTALL_SYSBOX: Action = Action { id: "install_sysbox", label: "Install Sysbox" }; | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 235 | const COPY_WSL_INSTALL: Action = Action { id: "copy_wsl_install", label: "Copy command" }; |
| 236 | const COPY_VFKIT_INSTALL: Action = Action { id: "copy_vfkit_install", label: "Copy command" }; | |
| 237 | const WINDOWS_FEATURES: Action = Action { id: "windows_features", label: "Open 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. | 238 | const LEARN_MORE: Action = Action { id: "learn_more", label: "Learn more" }; |
| 239 | const VIEW_LOG: Action = Action { id: "view_log", label: "View log" }; | |
| 240 | const REPORT: Action = Action { id: "report", label: "Report" }; | |
| 241 | const VIEW_RUNNERS: Action = Action { id: "view_runners", label: "View on g1t" }; | |
| 242 | ||
| 243 | fn some(text: impl Into<String>) -> Option<String> { | |
| 244 | Some(text.into()) | |
| 245 | } | |
| 246 | ||
| 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. | 247 | /// A wait, said: `30 seconds`, `5 minutes`. |
| 248 | fn said_wait(seconds: u64) -> String { | |
| 249 | match seconds { | |
| 250 | 0..=1 => "a moment".into(), | |
| 251 | 2..=89 => format!("{seconds} seconds"), | |
| 252 | _ => format!("{} minutes", seconds.div_ceil(60)), | |
| 253 | } | |
| 254 | } | |
| 255 | ||
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 256 | /// The catalogue. |
| 257 | pub fn diagnose(code: Code, facts: &Facts) -> Diagnosis { | |
| 258 | let workspace = facts.workspace.as_deref().unwrap_or("your workspace"); | |
| 259 | let detail = facts.detail.clone(); | |
| 260 | let (tone, title, cause, fix, actions): (Tone, String, Option<String>, Option<String>, Vec<Action>) = match code { | |
| 261 | Code::Off => ( | |
| 262 | Tone::Off, | |
| 263 | "Not running: turned off".into(), | |
| 264 | some("Running jobs on this computer is turned off in g1t's settings."), | |
| 265 | some("Turn it on to run your workspace's self-hosted jobs here while g1t is open."), | |
| 266 | vec![TURN_ON], | |
| 267 | ), | |
| 268 | Code::Stopped => ( | |
| 269 | Tone::Off, | |
| 270 | "Not running: stopped".into(), | |
| 271 | some("You stopped the runner. It starts again when you press Start, or when g1t next opens."), | |
| 272 | None, | |
| 273 | vec![START], | |
| 274 | ), | |
| 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. | 275 | Code::KindsStopped => ( |
| 276 | Tone::Off, | |
| 277 | "Not running: every kind of job is stopped".into(), | |
| 278 | some("You stopped each kind of job this computer can run."), | |
| 279 | some("Start one to run jobs here again."), | |
| 280 | vec![], | |
| 281 | ), | |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 282 | Code::Starting => (Tone::Working, "Starting".into(), detail, None, vec![]), |
| 283 | Code::Registering => ( | |
| 284 | Tone::Working, | |
| 285 | format!("Registering with {workspace}"), | |
| 286 | some(format!("g1t is registering {} as a runner for {workspace}.", facts.machine)), | |
| 287 | some("Keep g1t open and signed in; it takes a few seconds."), | |
| 288 | vec![], | |
| 289 | ), | |
| 290 | Code::SignedOut => ( | |
| 291 | Tone::Warning, | |
| 292 | "Not running: signed out".into(), | |
| 293 | some("This computer isn't registered yet, and registering it needs you signed in to g1t."), | |
| 294 | some("Sign in, as an owner of the workspace the runner is for."), | |
| 295 | vec![SIGN_IN], | |
| 296 | ), | |
| 297 | Code::NoWorkspace => ( | |
| 298 | Tone::Warning, | |
| 299 | "Not running: no workspace to run jobs for".into(), | |
| 300 | some("Only a workspace's owners add runners to it, and you don't own one."), | |
| 301 | some("Ask an owner of your workspace to run it, or create a workspace of your own."), | |
| 302 | vec![NEW_WORKSPACE], | |
| 303 | ), | |
| 304 | Code::NotAllowed => ( | |
| 305 | Tone::Problem, | |
| 306 | format!("Couldn't register with {workspace}"), | |
| 307 | some(format!("You can't add runners to {workspace}: only its owners can.")), | |
| 308 | some(format!("Ask an owner of {workspace} to make you one, or choose a workspace you own.")), | |
| 309 | vec![CHOOSE_WORKSPACE], | |
| 310 | ), | |
| 311 | Code::RegisterFailed => ( | |
| 312 | Tone::Problem, | |
| 313 | format!("Couldn't register with {workspace}"), | |
| 314 | detail.or_else(|| some("g1t didn't make a registration for this computer.")), | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 315 | some("g1t asks again by itself in a few minutes; Retry asks now. Its log says more."), |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 316 | vec![RETRY, VIEW_LOG], |
| 317 | ), | |
| 318 | Code::Idle => ( | |
| 319 | Tone::Ok, | |
| 320 | "Waiting for jobs".into(), | |
| 321 | some(format!("Running {} for {workspace} on {}.", facts.backend, facts.machine)), | |
| 322 | None, | |
| 323 | vec![VIEW_RUNNERS], | |
| 324 | ), | |
| 325 | Code::Busy => ( | |
| 326 | Tone::Working, | |
| 327 | format!("Running {}", facts.job.as_deref().unwrap_or("a job")), | |
| 328 | some(format!("Running {} for {workspace} on {}.", facts.backend, facts.machine)), | |
| 329 | None, | |
| 330 | vec![VIEW_RUNNERS], | |
| 331 | ), | |
| 332 | Code::NoDocker => { | |
| 333 | let app = match facts.platform.as_str() { | |
| 334 | "windows" | "macos" => "Docker Desktop", | |
| 335 | _ => "Docker", | |
| 336 | }; | |
| 337 | ( | |
| 338 | Tone::Problem, | |
| 339 | "Not running: Docker isn't running".into(), | |
| 340 | some(format!("Running jobs {} needs Docker, and Docker isn't running on this computer, or isn't installed.", facts.backend)), | |
| 341 | some(format!("Start {app}, or install it. The runner starts by itself once Docker answers.")), | |
| 342 | if app == "Docker Desktop" { vec![OPEN_DOCKER, GET_DOCKER] } else { vec![GET_DOCKER] }, | |
| 343 | ) | |
| 344 | } | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 345 | Code::NoLauncher => ( |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 346 | Tone::Warning, |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 347 | "Not running: this g1t-runner has no sandbox".into(), |
| 348 | some("The g1t-runner g1t downloaded is older than its sandbox launcher, so it can't make a sandbox."), | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 349 | some("g1t looks for a newer one by itself every few minutes; Retry looks now. Or choose to run jobs in Docker or directly on this computer: neither is a sandbox, and settings say what each can reach."), |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 350 | vec![RETRY, CHOOSE_BACKEND], |
| 351 | ), | |
| 352 | Code::NoWsl => ( | |
| 353 | Tone::Problem, | |
| 354 | "Not running: WSL isn't installed".into(), | |
| 355 | some("Linux jobs run in a WSL distribution of the runner's own, and WSL isn't installed on this computer."), | |
| 356 | some(format!("Run `{}` in an admin terminal, then restart Windows. It works on every edition of Windows 11, Home included.", launcher::WSL_INSTALL)), | |
| 357 | vec![COPY_WSL_INSTALL, LEARN_MORE], | |
| 358 | ), | |
| 359 | Code::WslMirrored => ( | |
| 360 | Tone::Problem, | |
| 361 | "Not running: WSL uses mirrored networking".into(), | |
| 362 | some("WSL is set to mirrored networking, in which Linux reaches this computer's own localhost, so the sandbox won't start."), | |
| 363 | some("Remove networkingMode=mirrored from %USERPROFILE%\\.wslconfig, run `wsl --shutdown`, then Retry."), | |
| 364 | vec![RETRY, LEARN_MORE], | |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 365 | ), |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 366 | Code::NoVirtualization => ( |
| 367 | Tone::Problem, | |
| 368 | "Not running: virtualization is off".into(), | |
| 369 | some("The sandbox is a small virtual machine, and this computer has virtualization turned off."), | |
| 370 | some(if facts.platform == "windows" { | |
| 371 | "Turn on virtualization (Intel VT-x or AMD-V) in this computer's firmware settings, and Virtual Machine Platform in Windows features; then restart." | |
| 372 | } else { | |
| 373 | "Turn on virtualization (Intel VT-x or AMD-V) in this computer's firmware settings, then restart." | |
| 374 | }), | |
| 375 | if facts.platform == "windows" { vec![WINDOWS_FEATURES, LEARN_MORE] } else { vec![RETRY, LEARN_MORE] }, | |
| 376 | ), | |
| 377 | Code::NoVfkit => ( | |
| 378 | Tone::Problem, | |
| 379 | "Not running: vfkit isn't installed".into(), | |
| 380 | some("Linux jobs run in a small VM that vfkit runs with Apple's Virtualization framework, and vfkit isn't installed."), | |
| 381 | some(format!("Run `{}` in Terminal (macOS 13 or later). The runner starts by itself once it's there.", launcher::VFKIT_INSTALL)), | |
| 382 | vec![COPY_VFKIT_INSTALL, LEARN_MORE], | |
| 383 | ), | |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 384 | Code::NoSysbox => ( |
| 385 | Tone::Problem, | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 386 | "Not running: no Sysbox or KVM".into(), |
| 387 | some("The sandbox on Linux is the runner's image under Sysbox, or a VM under QEMU with KVM, and this computer has neither."), | |
| 388 | some("Install Sysbox 0.7 or later, or turn on virtualization and install QEMU so /dev/kvm exists. The runner starts by itself once one is there."), | |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 389 | vec![INSTALL_SYSBOX, CHOOSE_BACKEND], |
| 390 | ), | |
| 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. | 391 | Code::WaitingForImage => { |
| 392 | let (release, image) = detail.as_deref().and_then(launcher::missing_images).map_or((None, None), |(release, image)| (Some(release), image)); | |
| 393 | let release = release.map_or_else(|| "The newest g1t-runner".to_owned(), |release| format!("g1t-runner {release}")); | |
| 394 | ( | |
| 395 | Tone::Warning, | |
| 396 | "Waiting for the sandbox's image".into(), | |
| 397 | some(match image { | |
| 398 | Some(image) => format!("{release} is published without the sandbox's image for this kind of computer ({image}) yet, so Linux jobs can't start."), | |
| 399 | None => format!("{release} is published without the sandbox's images yet, so Linux jobs can't start."), | |
| 400 | }), | |
| 401 | some("g1t checks again every few minutes and starts Linux jobs once they're out. There's nothing for you to do."), | |
| 402 | vec![CHECK_NOW], | |
| 403 | ) | |
| 404 | } | |
| 405 | Code::ImageFailed => { | |
| 406 | let said = detail.clone().unwrap_or_default().to_ascii_lowercase(); | |
| 407 | if said.contains("sha-256") || said.contains("signature") { | |
| 408 | ( | |
| 409 | Tone::Problem, | |
| 410 | "Not running: the sandbox's image didn't check out".into(), | |
| 411 | some("The sandbox's image that downloaded isn't the one g1t's release signed, so it wasn't used."), | |
| 412 | some(format!( | |
| 413 | "g1t downloads it again by itself{}. If this keeps happening, something between this computer and {} is changing downloads, such as a proxy or antivirus: report it.", | |
| 414 | facts.wait_seconds.map(|wait| format!(" in {}", said_wait(wait))).unwrap_or_default(), | |
| 415 | facts.server | |
| 416 | )), | |
| 417 | vec![RETRY, REPORT], | |
| 418 | ) | |
| 419 | } else { | |
| 420 | ( | |
| 421 | Tone::Problem, | |
| 422 | "Not running: the sandbox's image didn't download".into(), | |
| 423 | some(match &detail { | |
| 424 | Some(detail) => format!("The sandbox's image couldn't be downloaded: {detail}"), | |
| 425 | None => "The sandbox's image couldn't be downloaded.".into(), | |
| 426 | }), | |
| 427 | some(format!( | |
| 428 | "g1t tries again by itself{}. Check that this computer can reach {}; Retry tries now.", | |
| 429 | facts.wait_seconds.map(|wait| format!(" in {}", said_wait(wait))).unwrap_or_default(), | |
| 430 | facts.server | |
| 431 | )), | |
| 432 | vec![RETRY, VIEW_LOG], | |
| 433 | ) | |
| 434 | } | |
| 435 | } | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 436 | Code::SandboxUnavailable => ( |
| 437 | Tone::Problem, | |
| 438 | "Not running: the sandbox can't run here".into(), | |
| 439 | detail.or_else(|| some("The sandbox can't be made on this computer.")), | |
| 440 | facts.fix.clone().or_else(|| some("Choose to run jobs in Docker or directly on this computer instead: neither is a sandbox, and settings say what each can reach.")), | |
| 441 | vec![RETRY, LEARN_MORE, CHOOSE_BACKEND], | |
| 442 | ), | |
| 443 | Code::SandboxFailed => ( | |
| 444 | Tone::Problem, | |
| 445 | "Not running: the sandbox failed".into(), | |
| 446 | detail.or_else(|| some("The sandbox stopped, and didn't say why.")), | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 447 | some(match facts.wait_seconds { |
| 448 | Some(wait) => format!("It starts again by itself in {}. If it keeps stopping, its log says why; Retry starts it now.", said_wait(wait)), | |
| 449 | None => "Retry starts it again; its log says why it stopped.".into(), | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 450 | }), |
| 451 | vec![RETRY, VIEW_LOG], | |
| 452 | ), | |
| 453 | Code::SandboxTaken => ( | |
| 454 | Tone::Warning, | |
| 455 | "Not running: this computer has another sandbox".into(), | |
| 456 | some("This computer already has a g1t-runner sandbox that g1t didn't make, such as one started with `g1t-runner sandbox up`, and it has room for one."), | |
| 457 | some("Remove it with `g1t-runner sandbox down --delete`, then Retry; its runners stay listed on g1t until you remove them there. Or keep it, and turn off running jobs in g1t."), | |
| 458 | vec![RETRY, LEARN_MORE], | |
| 459 | ), | |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 460 | Code::Updating => (Tone::Working, "Updating".into(), detail, some("The runner starts again as soon as it's done."), vec![]), |
| 461 | Code::UpdateFailed => ( | |
| 462 | Tone::Warning, | |
| 463 | "Update failed".into(), | |
| 464 | detail, | |
| 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. | 465 | some("The runner keeps running the version it has, and g1t tries the update again in a few hours. Retry tries now; if it keeps failing, report it."), |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 466 | vec![RETRY, REPORT], |
| 467 | ), | |
| 468 | Code::Offline => ( | |
| 469 | Tone::Warning, | |
| 470 | format!("Offline: {} unreachable", facts.server), | |
| 471 | some(format!("This computer can't reach {}, so the runner can't take jobs.", facts.server)), | |
| 472 | some("Check the connection. The runner carries on by itself when it's back."), | |
| 473 | vec![RETRY], | |
| 474 | ), | |
| 475 | Code::Crashed => ( | |
| 476 | Tone::Problem, | |
| 477 | match facts.exit_code { | |
| 478 | Some(exit) => format!("Runner stopped (exit code {exit})"), | |
| 479 | None => "Runner stopped".into(), | |
| 480 | }, | |
| 481 | detail, | |
| 482 | Some(match facts.wait_seconds { | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 483 | Some(wait) => format!("It starts again by itself in {}. If it keeps stopping, its log says why; Restart starts it now.", said_wait(wait)), |
| 484 | None => "Restart starts it again; its log says why it stopped.".into(), | |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 485 | }), |
| 486 | vec![RESTART, VIEW_LOG], | |
| 487 | ), | |
| 488 | }; | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 489 | let capabilities = facts.sandbox.as_ref().map(|status| capabilities(status, facts)).unwrap_or_default(); |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 490 | let in_capabilities = !capabilities.is_empty() |
| 491 | && match code { | |
| 492 | Code::Idle | Code::Busy | Code::KindsStopped => true, | |
| 493 | Code::Starting | Code::Updating => facts.live, | |
| 494 | // Said by a card, for its kind of job, word for word. | |
| 495 | Code::NoWsl | |
| 496 | | Code::WslMirrored | |
| 497 | | Code::NoVirtualization | |
| 498 | | Code::NoVfkit | |
| 499 | | Code::NoSysbox | |
| 500 | | Code::WaitingForImage | |
| 501 | | Code::ImageFailed | |
| 502 | | Code::SandboxUnavailable | |
| 503 | | Code::SandboxFailed => cause.is_some() && capabilities.iter().any(|c| c.cause == cause), | |
| 504 | _ => false, | |
| 505 | }; | |
| 506 | Diagnosis { code, tone, title, cause, fix, actions, capabilities, in_capabilities } | |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 507 | } |
| 508 | ||
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 509 | /// What the app calls a sandbox's problem with Linux jobs, from the |
| 510 | /// launcher's sentence about it, which goes with it as its detail. | |
| 511 | pub fn sandbox_code(why: Why) -> Code { | |
| 512 | match why { | |
| 513 | Why::NoWsl => Code::NoWsl, | |
| 514 | Why::WslMirrored => Code::WslMirrored, | |
| 515 | Why::NoVirtualization => Code::NoVirtualization, | |
| 516 | Why::NoVfkit => Code::NoVfkit, | |
| 517 | Why::NoSysboxOrKvm => Code::NoSysbox, | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 518 | Why::NoImages => Code::WaitingForImage, |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 519 | Why::Image => Code::ImageFailed, |
| 520 | _ => Code::SandboxFailed, | |
| 521 | } | |
| 522 | } | |
| 523 | ||
| 524 | /// The code for what is wrong with Linux jobs, as a status says it, and | |
| 525 | /// the launcher's own words for it. | |
| 526 | pub fn sandbox_trouble(status: &Status) -> Option<(Code, String, Option<String>)> { | |
| 527 | let (cause, fix) = status.trouble()?; | |
| 528 | let code = match launcher::why(&cause) { | |
| 529 | Why::Other if !status.linux().available => Code::SandboxUnavailable, | |
| 530 | why => sandbox_code(why), | |
| 531 | }; | |
| 532 | Some((code, cause, fix)) | |
| 533 | } | |
| 534 | ||
| 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. | 535 | /// Each kind of job the sandbox runs, as a card of its own. |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 536 | fn capabilities(status: &Status, facts: &Facts) -> Vec<CapabilityLine> { |
| 537 | let listed = if status.capabilities.is_empty() { vec![status.linux()] } else { status.capabilities.clone() }; | |
| 538 | listed.iter().map(|capability| capability_line(status, capability, facts)).collect() | |
| 539 | } | |
| 540 | ||
| 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. | 541 | /// `Linux jobs`. |
| 542 | pub fn kind_label(id: &str) -> String { | |
| 543 | match id { | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 544 | "linux" => "Linux jobs".to_owned(), |
| 545 | "windows" => "Windows jobs".to_owned(), | |
| 546 | "macos" => "macOS jobs".to_owned(), | |
| 547 | other => format!("{other} jobs"), | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 548 | } |
| 549 | } | |
| 550 | ||
| 551 | /// What a kind of job is, in a line, by how the sandbox makes it. | |
| 552 | fn about(id: &str, backend: Option<&str>) -> String { | |
| 553 | match (id, backend) { | |
| 554 | ("linux", Some("sysbox")) => "The runner under Sysbox, each job in a fresh container".into(), | |
| 555 | ("linux", _) => "A Linux VM of its own, each job in a fresh container".into(), | |
| 556 | ("windows", _) => "Each job in a Windows Sandbox, discarded after".into(), | |
| 557 | ("macos", _) => "Each job in a macOS VM of its own, discarded after".into(), | |
| 558 | _ => "Each job sandboxed".into(), | |
| 559 | } | |
| 560 | } | |
| 561 | ||
| 562 | /// A catalogue title as a card's headline: `Not running: virtualization is off` is `Virtualization is off`. | |
| 563 | fn headline(title: &str) -> String { | |
| 564 | let said = title.strip_prefix("Not running: ").unwrap_or(title); | |
| 565 | let mut letters = said.chars(); | |
| 566 | letters.next().map(|first| first.to_uppercase().chain(letters).collect()).unwrap_or_default() | |
| 567 | } | |
| 568 | ||
| 569 | fn capability_line(status: &Status, capability: &launcher::Capability, facts: &Facts) -> CapabilityLine { | |
| 570 | let id = capability.id.as_str(); | |
| 571 | let label = kind_label(id); | |
| 572 | let enabled = !facts.off.iter().any(|off| off == id); | |
| 573 | let per_kind = status.per_kind(); | |
| 574 | let mut line = CapabilityLine { | |
| 575 | id: id.to_owned(), | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 576 | label: label.clone(), |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 577 | tone: Tone::Off, |
| 578 | state: KindState::Stopped, | |
| 579 | title: "Stopped".into(), | |
| 580 | about: about(id, status.backend.as_deref()), | |
| 581 | headline: None, | |
| 582 | cause: None, | |
| 583 | fix: None, | |
| 584 | actions: vec![], | |
| 585 | enabled, | |
| 586 | available: capability.available, | |
| 587 | // Nothing to start or stop where it can't run, nor while the whole runner is | |
| 588 | // stopped, whose own Start starts it (one the person stopped can be started). | |
| 589 | can_toggle: per_kind && (capability.available || !enabled) && !(facts.held && enabled), | |
| 590 | job: None, | |
| 591 | progress: None, | |
| 592 | isolation: capability.isolation.clone().filter(|_| capability.available), | |
| 593 | runner: capability.available.then(|| format!("{}-{id}", facts.machine)), | |
| 594 | version: capability.runner_version.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. | 595 | }; |
| 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. | 596 | let said = |line: &mut CapabilityLine, state: KindState, tone: Tone, cause: Option<String>, fix: Option<String>, actions: Vec<Action>| { |
| 597 | line.state = state; | |
| 598 | line.tone = tone; | |
| 599 | line.title = match state { | |
| 600 | KindState::Busy => "Running a job", | |
| 601 | KindState::Idle => "Waiting for jobs", | |
| 602 | KindState::Starting => "Starting", | |
| 603 | KindState::Downloading => "Downloading image", | |
| 604 | KindState::Updating => "Updating", | |
| 605 | KindState::Waiting => "Waiting for image", | |
| 606 | KindState::Stopped => "Stopped", | |
| 607 | KindState::Unavailable => "Not available", | |
| 608 | KindState::Problem => "Problem", | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 609 | } |
| 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. | 610 | .into(); |
| 611 | line.cause = cause; | |
| 612 | line.fix = fix; | |
| 613 | line.actions = actions; | |
| 614 | }; | |
| 615 | let again = |what: &str| format!("g1t {what} again by itself in a few minutes; Retry {what} now."); | |
| 616 | let starting = facts.starting.iter().any(|kind| kind == id) || matches!(capability.state, State::Starting); | |
| 617 | ||
| 618 | // Stopped by the person: nothing is wrong with it. | |
| 619 | if capability.off() || (!enabled && per_kind) { | |
| 620 | said(&mut line, KindState::Stopped, Tone::Off, some(format!("You stopped {label} on this computer. They stay stopped until you start them.")), None, vec![]); | |
| 621 | return line; | |
| 622 | } | |
| 623 | // This computer can't run it, and why. | |
| 624 | if !capability.available { | |
| 625 | if id == "linux" | |
| 626 | && let Some((code, cause, fix)) = sandbox_trouble(status) | |
| 627 | { | |
| 628 | let whole = diagnose(code, &Facts { detail: Some(cause), fix, sandbox: None, ..facts.clone() }); | |
| 629 | said(&mut line, KindState::Unavailable, Tone::Warning, whole.cause, whole.fix, whole.actions); | |
| 630 | line.headline = Some(headline(&whole.title)); | |
| 631 | return line; | |
| 632 | } | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 633 | let cause = capability.cause.clone(); |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 634 | let (headline, cause, fix, actions) = match cause.as_deref().map(launcher::why) { |
| 635 | Some(Why::WindowsSandboxOff) => ( | |
| 636 | "Windows Sandbox is off", | |
| 637 | some("Windows Sandbox is turned off, or this edition of Windows has none: Home doesn't."), | |
| 638 | some("On Windows 11 Pro, Enterprise or Education, turn on Windows Sandbox in Windows features, then restart."), | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 639 | vec![WINDOWS_FEATURES], |
| 640 | ), | |
| 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. | 641 | Some(Why::NoWsb) => ( |
| 642 | "Windows needs an update", | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 643 | some("This Windows has Windows Sandbox, but not its wsb command, which came with Windows 11 24H2."), |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 644 | some("Update Windows to 24H2 or later, then restart g1t."), |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 645 | vec![], |
| 646 | ), | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 647 | Some(Why::MacNotReleased) => ( |
| 648 | "Not released yet", | |
| 649 | some("Sandboxed macOS jobs aren't released yet."), | |
| 650 | some("They come with a g1t-runner update, which g1t downloads by itself. There's nothing for you to do."), | |
| 651 | vec![], | |
| 652 | ), | |
| 653 | _ => ("Can't run here", cause, capability.fix.clone(), vec![LEARN_MORE]), | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 654 | }; |
| 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. | 655 | said(&mut line, KindState::Unavailable, Tone::Off, cause, fix, actions); |
| 656 | line.headline = Some(headline.into()); | |
| 657 | return line; | |
| 658 | } | |
| 659 | // Linux jobs that should run and don't, in the catalogue's words. | |
| 660 | if id == "linux" | |
| 661 | && !starting | |
| 662 | && !matches!(capability.state, State::Updating) | |
| 663 | && let Some((code, cause, fix)) = sandbox_trouble(status) | |
| 664 | { | |
| 665 | let wait_seconds = if facts.live { None } else { facts.wait_seconds }; | |
| 666 | let whole = diagnose(code, &Facts { detail: Some(cause), fix, wait_seconds, sandbox: None, ..facts.clone() }); | |
| 667 | let state = if code == Code::WaitingForImage { KindState::Waiting } else { KindState::Problem }; | |
| 668 | said(&mut line, state, whole.tone, whole.cause, whole.fix, whole.actions); | |
| 669 | line.headline = Some(headline(&whole.title)); | |
| 670 | return line; | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 671 | } |
| 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. | 672 | // A native kind of job whose runner didn't start, in the launcher's words. |
| 673 | if id != "linux" && capability.state == State::Error && !starting { | |
| 674 | let cause = capability.cause.clone().or_else(|| some("Its runner didn't start, and didn't say why.")); | |
| 675 | said(&mut line, KindState::Problem, Tone::Problem, cause, some(again("tries")), vec![RETRY, VIEW_LOG]); | |
| 676 | line.headline = Some("Its runner didn't start".into()); | |
| 677 | return line; | |
| 678 | } | |
| 679 | let downloading = facts.download.as_ref().filter(|_| id == "linux"); | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 680 | match capability.state { |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 681 | _ if starting || capability.state == State::Updating => match downloading { |
| 682 | Some(download) => { | |
| 683 | said(&mut line, KindState::Downloading, Tone::Working, some("Downloading and checking the sandbox's image, once: it takes a few minutes."), None, vec![]); | |
| 684 | line.progress = Some(download.percent); | |
| 685 | } | |
| 686 | None if capability.state == State::Updating => said(&mut line, KindState::Updating, Tone::Working, some("Taking the newest sandbox image; it takes jobs again when it's done."), None, vec![]), | |
| 687 | None if capability.state == State::Absent => said(&mut line, KindState::Starting, Tone::Working, some(format!("Registering it with {}.", facts.workspace.as_deref().unwrap_or("your workspace"))), None, vec![]), | |
| 688 | None => said(&mut line, KindState::Starting, Tone::Working, None, None, vec![]), | |
| 689 | }, | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 690 | State::Running => match &capability.job { |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 691 | Some(job) => { |
| 692 | said(&mut line, KindState::Busy, Tone::Working, None, None, vec![]); | |
| 693 | let page = if job.kind == "agent" { "agents" } else { "actions" }; | |
| 694 | let link = (!job.repo.is_empty() && !facts.site.is_empty()).then(|| format!("{}/{}/{page}", facts.site.trim_end_matches('/'), job.repo)); | |
| 695 | line.job = Some(JobLine { kind: job.kind.clone(), name: job.name.clone(), repo: job.repo.clone(), link }); | |
| 696 | } | |
| 697 | None => said(&mut line, KindState::Idle, Tone::Ok, None, None, vec![]), | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 698 | }, |
| 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 | // Up while the others run, yet not running: it stopped by itself, |
| 700 | // or is to be registered. | |
| 701 | State::Stopped | State::Absent if facts.live && status.running() => { | |
| 702 | if capability.state == State::Absent { | |
| 703 | said(&mut line, KindState::Starting, Tone::Working, some(format!("Registering it with {}.", facts.workspace.as_deref().unwrap_or("your workspace"))), None, vec![]); | |
| 704 | } else { | |
| 705 | said(&mut line, KindState::Problem, Tone::Problem, some("Its runner stopped by itself."), some(again("starts it")), vec![RETRY, VIEW_LOG]); | |
| 706 | line.headline = Some("Its runner stopped".into()); | |
| 707 | } | |
| 708 | } | |
| 709 | State::Stopped | State::Absent => said(&mut line, KindState::Stopped, Tone::Off, None, None, vec![]), | |
| 710 | State::Error | State::Unknown | State::Starting | State::Updating => { | |
| 711 | said(&mut line, KindState::Problem, Tone::Problem, capability.cause.clone().or_else(|| status.error.clone()), capability.fix.clone(), vec![RETRY, VIEW_LOG]); | |
| 712 | } | |
| 713 | } | |
| 714 | // The person stopped it, and this launcher can't stop one kind alone yet. | |
| 715 | if !enabled && line.fix.is_none() { | |
| 716 | line.fix = some(format!("This g1t-runner can't stop {label} on their own; the next one can, and g1t downloads it by itself.")); | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 717 | } |
| 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. | 718 | line |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 719 | } |
| 720 | ||
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 721 | /// What the site says of this computer while g1t is closed on it. |
| 722 | pub fn closed(facts: &Facts) -> Diagnosis { | |
| 723 | Diagnosis { | |
| 724 | code: Code::Off, | |
| 725 | tone: Tone::Off, | |
| 726 | title: format!("g1t is closed on {}", facts.machine), | |
| 727 | cause: some(format!("This runner runs while g1t is open on {}.", facts.machine)), | |
| 728 | fix: some(format!("Open g1t on {} to run jobs there again.", facts.machine)), | |
| 729 | actions: vec![], | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 730 | capabilities: vec![], |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 731 | in_capabilities: false, |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 732 | } |
| 733 | } | |
| 734 | ||
| 735 | #[cfg(test)] | |
| 736 | mod tests { | |
| 737 | use super::*; | |
| 738 | ||
| 739 | fn facts() -> Facts { | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 740 | Facts { |
| 741 | workspace: Some("acme".into()), | |
| 742 | backend: "sandboxed", | |
| 743 | machine: "build-box".into(), | |
| 744 | server: "g1t.sh".into(), | |
| 745 | site: "https://g1t.sh".into(), | |
| 746 | platform: "windows".into(), | |
| 747 | ..Facts::default() | |
| 748 | } | |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 749 | } |
| 750 | ||
| 751 | #[test] | |
| 752 | fn every_failure_says_why_and_how_to_fix_it() { | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 753 | for code in [ |
| 754 | Code::SignedOut, | |
| 755 | Code::NoWorkspace, | |
| 756 | Code::NotAllowed, | |
| 757 | Code::RegisterFailed, | |
| 758 | Code::NoDocker, | |
| 759 | Code::NoLauncher, | |
| 760 | Code::NoWsl, | |
| 761 | Code::WslMirrored, | |
| 762 | Code::NoVirtualization, | |
| 763 | Code::NoVfkit, | |
| 764 | Code::NoSysbox, | |
| 765 | Code::WaitingForImage, | |
| 766 | Code::ImageFailed, | |
| 767 | Code::SandboxUnavailable, | |
| 768 | Code::SandboxFailed, | |
| 769 | Code::SandboxTaken, | |
| 770 | Code::UpdateFailed, | |
| 771 | Code::Offline, | |
| 772 | Code::Crashed, | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 773 | ] { |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 774 | let diagnosis = diagnose(code, &Facts { detail: Some("why".into()), ..facts() }); |
| 775 | assert!(diagnosis.cause.is_some(), "{code:?} has no cause"); | |
| 776 | assert!(diagnosis.fix.is_some(), "{code:?} has no fix"); | |
| 777 | assert!(!diagnosis.actions.is_empty(), "{code:?} has no action"); | |
| 778 | assert!(matches!(diagnosis.tone, Tone::Warning | Tone::Problem), "{code:?}"); | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 779 | // No fix sends the person off to check their connection or report it |
| 780 | // when that isn't what is wrong. | |
| 781 | let fix = diagnosis.fix.unwrap(); | |
| 782 | if !matches!(code, Code::Offline | Code::ImageFailed) { | |
| 783 | assert!(!fix.contains("check the connection") && !fix.contains("Check the connection"), "{code:?}: {fix}"); | |
| 784 | } | |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 785 | } |
| 786 | } | |
| 787 | ||
| 788 | #[test] | |
| 789 | fn the_words_name_the_workspace_the_machine_and_the_job() { | |
| 790 | let not_allowed = diagnose(Code::NotAllowed, &facts()); | |
| 791 | assert_eq!(not_allowed.cause.as_deref(), Some("You can't add runners to acme: only its owners can.")); | |
| 792 | assert_eq!(not_allowed.actions[0].id, "choose_workspace"); | |
| 793 | let busy = diagnose(Code::Busy, &Facts { job: Some("Build (acme/web)".into()), ..facts() }); | |
| 794 | assert_eq!(busy.title, "Running Build (acme/web)"); | |
| 795 | assert_eq!(busy.cause.as_deref(), Some("Running sandboxed for acme on build-box.")); | |
| 796 | let crashed = diagnose(Code::Crashed, &Facts { exit_code: Some(101), wait_seconds: Some(30), ..facts() }); | |
| 797 | assert_eq!(crashed.title, "Runner stopped (exit code 101)"); | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 798 | assert_eq!(crashed.fix.as_deref(), Some("It starts again by itself in 30 seconds. If it keeps stopping, its log says why; Restart starts it now.")); |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 799 | assert_eq!(crashed.actions.iter().map(|a| a.id).collect::<Vec<_>>(), ["restart", "view_log"]); |
| 800 | assert_eq!(diagnose(Code::NoDocker, &facts()).actions[0].id, "open_docker"); | |
| 801 | assert_eq!(closed(&facts()).title, "g1t is closed on build-box"); | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 802 | assert_eq!(said_wait(300), "5 minutes"); |
| 803 | assert_eq!(said_wait(45), "45 seconds"); | |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 804 | } |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 805 | |
| 806 | fn status(json: &str) -> Status { | |
| 807 | launcher::parse(json).unwrap() | |
| 808 | } | |
| 809 | ||
| 810 | const NO_WSL_PRO: &str = r#"{"schema":1,"state":"error","backend":"wsl2","launcher_version":"0.2.1", | |
| 811 | "error":"WSL is not installed, and the sandbox for Linux jobs is a WSL distribution of its own.", | |
| 812 | "capabilities":[ | |
| 813 | {"id":"linux","available":false,"state":"error","isolation":"nested-container", | |
| 814 | "cause":"WSL is not installed, and the sandbox for Linux jobs is a WSL distribution of its own.", | |
| 815 | "fix":"Open a terminal as administrator and run `wsl --install --no-distribution`, then restart Windows. It works on every edition of Windows 11, Home included."}, | |
| 816 | {"id":"windows","available":true,"state":"running","isolation":"vm","runner_version":"0.2.1","job":null}]}"#; | |
| 817 | ||
| 818 | #[test] | |
| 819 | fn without_wsl_the_fix_is_the_command_to_copy_and_windows_jobs_still_show() { | |
| 820 | let status = status(NO_WSL_PRO); | |
| 821 | let (code, cause, _) = sandbox_trouble(&status).unwrap(); | |
| 822 | assert_eq!(code, Code::NoWsl); | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 823 | let diagnosis = diagnose(code, &Facts { detail: Some(cause), sandbox: Some(status), live: true, ..facts() }); |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 824 | assert_eq!(diagnosis.title, "Not running: WSL isn't installed"); |
| 825 | assert_eq!(diagnosis.fix.as_deref(), Some("Run `wsl --install --no-distribution` in an admin terminal, then restart Windows. It works on every edition of Windows 11, Home included.")); | |
| 826 | assert_eq!(diagnosis.actions[0], COPY_WSL_INSTALL); | |
| 827 | let [linux, windows] = &diagnosis.capabilities[..] else { panic!("{:?}", diagnosis.capabilities) }; | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 828 | assert_eq!((linux.label.as_str(), linux.title.as_str(), linux.state), ("Linux jobs", "Not available", KindState::Unavailable)); |
| 829 | assert_eq!(linux.headline.as_deref(), Some("WSL isn't installed")); | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 830 | assert_eq!(linux.fix, diagnosis.fix); |
| 831 | assert_eq!(linux.actions[0].id, "copy_wsl_install"); | |
| 832 | assert_eq!((windows.label.as_str(), windows.title.as_str(), windows.tone), ("Windows jobs", "Waiting for jobs", Tone::Ok)); | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 833 | assert_eq!(windows.runner.as_deref(), Some("build-box-windows")); |
| 834 | assert_eq!(windows.about, "Each job in a Windows Sandbox, discarded after"); | |
| 835 | // The Linux card says it all: the whole runner's line isn't shown as well. | |
| 836 | assert!(diagnosis.in_capabilities); | |
| 837 | // An older launcher can't stop one kind of job alone. | |
| 838 | assert!(!windows.can_toggle); | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 839 | } |
| 840 | ||
| 841 | #[test] | |
| 842 | fn on_home_windows_jobs_are_unavailable_and_linux_jobs_still_run() { | |
| 843 | let status = status( | |
| 844 | r#"{"schema":1,"state":"running","backend":"wsl2","runner_version":"0.3.0","launcher_version":"0.3.0", | |
| 845 | "job":{"kind":"workflow","id":"job_1","name":"build","repo":"acme/web"}, | |
| 846 | "capabilities":[ | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 847 | {"id":"linux","available":true,"state":"running","runner_version":"0.3.0","isolation":"nested-container","turned_off":false,"job":{"kind":"workflow","id":"job_1","name":"build","repo":"acme/web"}}, |
| 848 | {"id":"windows","available":false,"state":"absent","turned_off":false,"cause":"Windows Sandbox is not turned on, or this edition of Windows has none (Home does not).","fix":"On Windows 11 Pro…"}]}"#, | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 849 | ); |
| 850 | assert_eq!(sandbox_trouble(&status), None); | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 851 | let busy = diagnose(Code::Busy, &Facts { job: status.job().map(launcher::Job::said), sandbox: Some(status), live: true, ..facts() }); |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 852 | assert_eq!(busy.title, "Running build (acme/web)"); |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 853 | assert!(busy.in_capabilities); |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 854 | let [linux, windows] = &busy.capabilities[..] else { panic!() }; |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 855 | assert_eq!((linux.title.as_str(), linux.state, linux.tone), ("Running a job", KindState::Busy, Tone::Working)); |
| 856 | let job = linux.job.as_ref().unwrap(); | |
| 857 | assert_eq!((job.name.as_str(), job.repo.as_str(), job.link.as_deref()), ("build", "acme/web", Some("https://g1t.sh/acme/web/actions"))); | |
| 858 | assert!(linux.can_toggle && linux.enabled); | |
| 859 | assert_eq!((windows.title.as_str(), windows.tone, windows.state), ("Not available", Tone::Off, KindState::Unavailable)); | |
| 860 | assert_eq!(windows.headline.as_deref(), Some("Windows Sandbox is off")); | |
| 861 | assert_eq!(windows.cause.as_deref(), Some("Windows Sandbox is turned off, or this edition of Windows has none: Home doesn't.")); | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 862 | assert_eq!(windows.actions[0].id, "windows_features"); |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 863 | // Nothing to start or stop where it can't run. |
| 864 | assert!(!windows.can_toggle); | |
| 865 | assert_eq!(windows.runner, None); | |
| 866 | } | |
| 867 | ||
| 868 | /// 0.3.0 was published without the sandbox's images: Linux jobs wait for | |
| 869 | /// them, plainly, while Windows jobs run. | |
| 870 | const NO_IMAGES: &str = r#"{"schema":1,"state":"error","backend":"wsl2","launcher_version":"0.3.0", | |
| 871 | "error":"Windows jobs are sandboxed and running, but Linux jobs are not: release 0.3.0 has no sandbox images", | |
| 872 | "capabilities":[ | |
| 873 | {"id":"linux","available":true,"state":"error","isolation":"nested-container","turned_off":false}, | |
| 874 | {"id":"windows","available":true,"state":"running","isolation":"vm","runner_version":"0.3.0","turned_off":false}]}"#; | |
| 875 | ||
| 876 | #[test] | |
| 877 | fn a_release_without_images_is_waited_for_not_reported() { | |
| 878 | let status = status(NO_IMAGES); | |
| 879 | let (code, cause, _) = sandbox_trouble(&status).unwrap(); | |
| 880 | assert_eq!(code, Code::WaitingForImage); | |
| 881 | let whole = diagnose(code, &Facts { detail: Some(cause), ..facts() }); | |
| 882 | assert_eq!(whole.title, "Waiting for the sandbox's image"); | |
| 883 | assert_eq!(whole.cause.as_deref(), Some("g1t-runner 0.3.0 is published without the sandbox's images yet, so Linux jobs can't start.")); | |
| 884 | assert_eq!(whole.fix.as_deref(), Some("g1t checks again every few minutes and starts Linux jobs once they're out. There's nothing for you to do.")); | |
| 885 | assert_eq!(whole.actions, [CHECK_NOW]); | |
| 886 | assert!(!whole.fix.unwrap().contains("report")); | |
| 887 | // Windows jobs run: the runner waits for jobs, and the Linux card says what it waits for. | |
| 888 | let idle = diagnose(Code::Idle, &Facts { sandbox: Some(status), live: true, ..facts() }); | |
| 889 | assert!(idle.in_capabilities); | |
| 890 | let [linux, windows] = &idle.capabilities[..] else { panic!() }; | |
| 891 | assert_eq!((linux.state, linux.title.as_str(), linux.tone), (KindState::Waiting, "Waiting for image", Tone::Warning)); | |
| 892 | assert_eq!(linux.headline.as_deref(), Some("Waiting for the sandbox's image")); | |
| 893 | assert_eq!(windows.state, KindState::Idle); | |
| 894 | let image = diagnose(Code::ImageFailed, &Facts { detail: Some("the sandbox image wsl-x64.tar.gz's SHA-256 is not the release's; not using it".into()), ..facts() }); | |
| 895 | assert_eq!(image.title, "Not running: the sandbox's image didn't check out"); | |
| 896 | let fetch = diagnose(Code::ImageFailed, &Facts { detail: Some("could not fetch https://g1t.sh/x: dns error".into()), wait_seconds: Some(60), ..facts() }); | |
| 897 | assert_eq!(fetch.cause.as_deref(), Some("The sandbox's image couldn't be downloaded: could not fetch https://g1t.sh/x: dns error")); | |
| 898 | assert_eq!(fetch.fix.as_deref(), Some("g1t tries again by itself in 60 seconds. Check that this computer can reach g1t.sh; Retry tries now.")); | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 899 | } |
| 900 | ||
| 901 | #[test] | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 902 | fn a_kind_the_person_stopped_says_so_and_is_not_failing() { |
| 903 | let status = status( | |
| 904 | r#"{"schema":1,"state":"running","backend":"wsl2","capabilities":[ | |
| 905 | {"id":"linux","available":true,"state":"running","turned_off":false}, | |
| 906 | {"id":"windows","available":true,"state":"stopped","turned_off":true}]}"#, | |
| 907 | ); | |
| 908 | let idle = diagnose(Code::Idle, &Facts { sandbox: Some(status.clone()), live: true, off: vec!["windows".into()], ..facts() }); | |
| 909 | let [linux, windows] = &idle.capabilities[..] else { panic!() }; | |
| 910 | assert_eq!(linux.state, KindState::Idle); | |
| 911 | assert_eq!((windows.state, windows.tone, windows.enabled), (KindState::Stopped, Tone::Off, false)); | |
| 912 | assert_eq!(windows.cause.as_deref(), Some("You stopped Windows jobs on this computer. They stay stopped until you start them.")); | |
| 913 | assert!(windows.can_toggle && windows.actions.is_empty()); | |
| 914 | // Stopped by itself while Linux jobs run is a problem, with Retry. | |
| 915 | let died = launcher::parse(&serde_json::to_string(&serde_json::json!({"schema":1,"state":"running","capabilities":[ | |
| 916 | {"id":"linux","available":true,"state":"running","turned_off":false}, | |
| 917 | {"id":"windows","available":true,"state":"stopped","turned_off":false}]})).unwrap()).unwrap(); | |
| 918 | let idle = diagnose(Code::Idle, &Facts { sandbox: Some(died.clone()), live: true, ..facts() }); | |
| 919 | assert_eq!((idle.capabilities[1].state, idle.capabilities[1].actions[0].id), (KindState::Problem, "retry")); | |
| 920 | // With the whole runner stopped, it is only stopped. | |
| 921 | let stopped = diagnose(Code::Stopped, &Facts { sandbox: Some(died), live: false, held: true, ..facts() }); | |
| 922 | // Its own Start or Stop waits for the runner's: the line above the cards starts it. | |
| 923 | assert!(!stopped.capabilities[0].can_toggle); | |
| 924 | assert_eq!(stopped.capabilities[1].state, KindState::Stopped); | |
| 925 | assert!(!stopped.in_capabilities); | |
| 926 | } | |
| 927 | ||
| 928 | #[test] | |
| 929 | fn a_download_shows_how_far_it_is() { | |
| 930 | let status = status(r#"{"schema":1,"state":"starting","capabilities":[{"id":"linux","available":true,"state":"starting","turned_off":false},{"id":"windows","available":true,"state":"absent","turned_off":false}]}"#); | |
| 931 | let facts = Facts { sandbox: Some(status), live: true, starting: vec!["linux".into(), "windows".into()], download: Some(launcher::Download { key: "wsl-x64".into(), percent: 40 }), ..facts() }; | |
| 932 | let starting = diagnose(Code::Starting, &facts); | |
| 933 | assert!(starting.in_capabilities); | |
| 934 | let [linux, windows] = &starting.capabilities[..] else { panic!() }; | |
| 935 | assert_eq!((linux.state, linux.title.as_str(), linux.progress), (KindState::Downloading, "Downloading image", Some(40))); | |
| 936 | assert_eq!((windows.state, windows.progress), (KindState::Starting, None)); | |
| 937 | } | |
| 938 | ||
| 939 | #[test] | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 940 | fn each_launcher_trouble_has_its_words() { |
| 941 | let trouble = |linux_available: bool, text: &str| { | |
| 942 | let json = serde_json::json!({ | |
| 943 | "schema": 1, "state": "error", "error": text, | |
| 944 | "capabilities": [{"id": "linux", "available": linux_available, "state": "error", "cause": if linux_available { None } else { Some(text) }, "fix": "the launcher's fix"}] | |
| 945 | }); | |
| 946 | sandbox_trouble(&status(&json.to_string())).unwrap() | |
| 947 | }; | |
| 948 | assert_eq!(trouble(true, "WSL could not import the sandbox: Please enable the Virtual Machine Platform Windows feature and ensure virtualization is enabled in the BIOS.").0, Code::NoVirtualization); | |
| 949 | assert_eq!(trouble(false, "vfkit, which runs the sandbox's VM with Apple's Virtualization framework, is not installed.").0, Code::NoVfkit); | |
| 950 | assert_eq!(trouble(false, "This machine has neither Sysbox nor KVM, so there is nothing to sandbox the runner with.").0, Code::NoSysbox); | |
| 951 | assert_eq!(trouble(false, "WSL is set to mirrored networking, in which Linux reaches this computer's own localhost.").0, Code::WslMirrored); | |
| 952 | assert_eq!(trouble(true, "the sandbox image wsl-x64.tar.gz's SHA-256 is not the release's; not using it").0, Code::ImageFailed); | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 953 | assert_eq!(trouble(true, "release 0.3.0 has no sandbox images").0, Code::WaitingForImage); |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 954 | // Unknown words: shown as the launcher wrote them, its fix with them. |
| 955 | let (code, cause, fix) = trouble(false, "This user may not use /dev/kvm."); | |
| 956 | assert_eq!(code, Code::SandboxUnavailable); | |
| 957 | let unavailable = diagnose(code, &Facts { detail: Some(cause), fix, ..facts() }); | |
| 958 | assert_eq!(unavailable.cause.as_deref(), Some("This user may not use /dev/kvm.")); | |
| 959 | assert_eq!(unavailable.fix.as_deref(), Some("the launcher's fix")); | |
| 960 | let (code, cause, _) = trouble(true, "the sandbox did not start within 600 seconds"); | |
| 961 | assert_eq!(code, Code::SandboxFailed); | |
| 962 | let failed = diagnose(code, &Facts { detail: Some(cause), wait_seconds: Some(60), ..facts() }); | |
| 963 | assert_eq!(failed.cause.as_deref(), Some("the sandbox did not start within 600 seconds")); | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 964 | assert_eq!(failed.fix.as_deref(), Some("It starts again by itself in 60 seconds. If it keeps stopping, its log says why; Retry starts it now.")); |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 965 | assert_eq!(diagnose(Code::NoVirtualization, &Facts { platform: "macos".into(), ..facts() }).actions[0].id, "retry"); |
| 966 | assert_eq!(diagnose(Code::NoVfkit, &facts()).actions[0].id, "copy_vfkit_install"); | |
| 967 | } | |
| 968 | ||
| 969 | #[test] | |
| 970 | fn a_diagnosis_without_the_sandbox_lists_no_capabilities() { | |
| 971 | let json = serde_json::to_value(diagnose(Code::Idle, &Facts { backend: "in Docker", ..facts() })).unwrap(); | |
| 972 | assert!(json.get("capabilities").is_none()); | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 973 | assert!(json.get("in_capabilities").is_none()); |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 974 | let sandboxed = serde_json::to_value(diagnose(Code::Idle, &Facts { sandbox: Some(status(NO_WSL_PRO)), ..facts() })).unwrap(); |
| 975 | assert_eq!(sandboxed["capabilities"][1]["id"], "windows"); | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 976 | assert_eq!(sandboxed["capabilities"][1]["state"], "idle"); |
| 977 | assert_eq!(sandboxed["in_capabilities"], true); | |
| 978 | // What affects everything is said once, above the cards. | |
| 979 | let signed_out = diagnose(Code::SignedOut, &Facts { sandbox: Some(status(NO_WSL_PRO)), ..facts() }); | |
| 980 | assert!(!signed_out.in_capabilities); | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 981 | } |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 982 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.