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 | |
| 14 | //! off, Windows Sandbox off, …) are said in its words, with a fix and its | |
| 15 | //! buttons; any other is shown as the launcher wrote it. With the sandbox, | |
| 16 | //! each kind of job it runs (Linux jobs, Windows jobs) has a line of its | |
| 17 | //! own as well (`capabilities`), since one can run while the other cannot. | |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 18 | |
| 19 | use serde::Serialize; | |
| 20 | ||
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 21 | use super::launcher::{self, State, Status, Why}; |
| 22 | ||
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 23 | /// Every state the runner can be in. |
| 24 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)] | |
| 25 | #[serde(rename_all = "snake_case")] | |
| 26 | pub enum Code { | |
| 27 | /// Running jobs is turned off in settings. | |
| 28 | #[default] | |
| 29 | Off, | |
| 30 | /// The person pressed Stop. | |
| 31 | Stopped, | |
| 32 | Starting, | |
| 33 | /// Waiting for the site to make a registration token. | |
| 34 | Registering, | |
| 35 | /// Nobody is signed in to g1t in the app. | |
| 36 | SignedOut, | |
| 37 | /// Signed in, owning no workspace: nowhere to register. | |
| 38 | NoWorkspace, | |
| 39 | /// The person may not add runners to the workspace. | |
| 40 | NotAllowed, | |
| 41 | RegisterFailed, | |
| 42 | Idle, | |
| 43 | Busy, | |
| 44 | 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. | 45 | /// The `g1t-runner` here is from before its sandbox launcher. |
| 46 | NoLauncher, | |
| 47 | /// Windows, without WSL, which holds the sandbox's Linux VM. | |
| 48 | NoWsl, | |
| 49 | /// WSL set to mirrored networking, which reaches this computer's localhost. | |
| 50 | WslMirrored, | |
| 51 | /// The computer's virtualization is turned off. | |
| 52 | NoVirtualization, | |
| 53 | /// macOS, without vfkit, which runs the sandbox's VM. | |
| 54 | NoVfkit, | |
| 55 | /// 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. | 56 | NoSysbox, |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 57 | /// The sandbox's image couldn't be downloaded, or isn't the release's. |
| 58 | ImageFailed, | |
| 59 | /// The sandbox can't be made here, for a reason in the launcher's words. | |
| 60 | SandboxUnavailable, | |
| 61 | /// The sandbox failed, in the launcher's words. | |
| 62 | SandboxFailed, | |
| 63 | /// The computer has a sandbox the app didn't make, and has room for one. | |
| 64 | 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. | 65 | Updating, |
| 66 | UpdateFailed, | |
| 67 | Offline, | |
| 68 | Crashed, | |
| 69 | } | |
| 70 | ||
| 71 | /// How a state should look: a dot's colour. | |
| 72 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] | |
| 73 | #[serde(rename_all = "snake_case")] | |
| 74 | pub enum Tone { | |
| 75 | Ok, | |
| 76 | Working, | |
| 77 | Off, | |
| 78 | Warning, | |
| 79 | Problem, | |
| 80 | } | |
| 81 | ||
| 82 | /// A one-click fix. | |
| 83 | #[derive(Clone, Debug, PartialEq, Eq, Serialize)] | |
| 84 | pub struct Action { | |
| 85 | pub id: &'static str, | |
| 86 | pub label: &'static str, | |
| 87 | } | |
| 88 | ||
| 89 | #[derive(Clone, Debug, PartialEq, Eq, Serialize)] | |
| 90 | pub struct Diagnosis { | |
| 91 | pub code: Code, | |
| 92 | pub tone: Tone, | |
| 93 | /// One line: the tray's, and the heading everywhere else. | |
| 94 | pub title: String, | |
| 95 | /// Why, in a sentence. | |
| 96 | pub cause: Option<String>, | |
| 97 | /// What to do about it. | |
| 98 | pub fix: Option<String>, | |
| 99 | pub actions: Vec<Action>, | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 100 | /// With the sandbox: each kind of job it runs, on its own line. |
| 101 | #[serde(skip_serializing_if = "Vec::is_empty")] | |
| 102 | pub capabilities: Vec<CapabilityLine>, | |
| 103 | } | |
| 104 | ||
| 105 | /// One kind of job the sandbox runs, `linux` or `windows` (or `macos`), | |
| 106 | /// and how it is: Linux jobs can run while Windows jobs cannot. | |
| 107 | #[derive(Clone, Debug, PartialEq, Eq, Serialize)] | |
| 108 | pub struct CapabilityLine { | |
| 109 | pub id: String, | |
| 110 | /// `Linux jobs`. | |
| 111 | pub label: String, | |
| 112 | pub tone: Tone, | |
| 113 | /// `Waiting for jobs`, `Not available`. | |
| 114 | pub title: String, | |
| 115 | pub cause: Option<String>, | |
| 116 | pub fix: Option<String>, | |
| 117 | pub actions: Vec<Action>, | |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 118 | } |
| 119 | ||
| 120 | /// What the words are filled in with. | |
| 121 | #[derive(Clone, Debug, Default)] | |
| 122 | pub struct Facts { | |
| 123 | pub workspace: Option<String>, | |
| 124 | /// How jobs run, as `sandboxed`, `in Docker`, `on this computer`. | |
| 125 | pub backend: &'static str, | |
| 126 | pub machine: String, | |
| 127 | pub job: Option<String>, | |
| 128 | pub detail: Option<String>, | |
| 129 | pub exit_code: Option<i32>, | |
| 130 | pub wait_seconds: Option<u64>, | |
| 131 | /// The server's host: `g1t.sh`. | |
| 132 | pub server: String, | |
| 133 | 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. | 134 | /// What the sandbox says to do, for a cause in its own words. |
| 135 | pub fix: Option<String>, | |
| 136 | /// How the sandbox is, when jobs run sandboxed. | |
| 137 | pub sandbox: Option<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. | 138 | } |
| 139 | ||
| 140 | const START: Action = Action { id: "start", label: "Start" }; | |
| 141 | const TURN_ON: Action = Action { id: "start", label: "Turn on" }; | |
| 142 | const RETRY: Action = Action { id: "retry", label: "Retry" }; | |
| 143 | const RESTART: Action = Action { id: "restart", label: "Restart" }; | |
| 144 | const SIGN_IN: Action = Action { id: "sign_in", label: "Sign in" }; | |
| 145 | const CHOOSE_WORKSPACE: Action = Action { id: "choose_workspace", label: "Choose workspace" }; | |
| 146 | const NEW_WORKSPACE: Action = Action { id: "new_workspace", label: "Create a workspace" }; | |
| 147 | const CHOOSE_BACKEND: Action = Action { id: "choose_backend", label: "Choose how jobs run" }; | |
| 148 | const OPEN_DOCKER: Action = Action { id: "open_docker", label: "Open Docker" }; | |
| 149 | const GET_DOCKER: Action = Action { id: "get_docker", label: "Get Docker" }; | |
| 150 | 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. | 151 | const COPY_WSL_INSTALL: Action = Action { id: "copy_wsl_install", label: "Copy command" }; |
| 152 | const COPY_VFKIT_INSTALL: Action = Action { id: "copy_vfkit_install", label: "Copy command" }; | |
| 153 | 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. | 154 | const LEARN_MORE: Action = Action { id: "learn_more", label: "Learn more" }; |
| 155 | const VIEW_LOG: Action = Action { id: "view_log", label: "View log" }; | |
| 156 | const REPORT: Action = Action { id: "report", label: "Report" }; | |
| 157 | const VIEW_RUNNERS: Action = Action { id: "view_runners", label: "View on g1t" }; | |
| 158 | ||
| 159 | fn some(text: impl Into<String>) -> Option<String> { | |
| 160 | Some(text.into()) | |
| 161 | } | |
| 162 | ||
| 163 | /// The catalogue. | |
| 164 | pub fn diagnose(code: Code, facts: &Facts) -> Diagnosis { | |
| 165 | let workspace = facts.workspace.as_deref().unwrap_or("your workspace"); | |
| 166 | let detail = facts.detail.clone(); | |
| 167 | let (tone, title, cause, fix, actions): (Tone, String, Option<String>, Option<String>, Vec<Action>) = match code { | |
| 168 | Code::Off => ( | |
| 169 | Tone::Off, | |
| 170 | "Not running: turned off".into(), | |
| 171 | some("Running jobs on this computer is turned off in g1t's settings."), | |
| 172 | some("Turn it on to run your workspace's self-hosted jobs here while g1t is open."), | |
| 173 | vec![TURN_ON], | |
| 174 | ), | |
| 175 | Code::Stopped => ( | |
| 176 | Tone::Off, | |
| 177 | "Not running: stopped".into(), | |
| 178 | some("You stopped the runner. It starts again when you press Start, or when g1t next opens."), | |
| 179 | None, | |
| 180 | vec![START], | |
| 181 | ), | |
| 182 | Code::Starting => (Tone::Working, "Starting".into(), detail, None, vec![]), | |
| 183 | Code::Registering => ( | |
| 184 | Tone::Working, | |
| 185 | format!("Registering with {workspace}"), | |
| 186 | some(format!("g1t is registering {} as a runner for {workspace}.", facts.machine)), | |
| 187 | some("Keep g1t open and signed in; it takes a few seconds."), | |
| 188 | vec![], | |
| 189 | ), | |
| 190 | Code::SignedOut => ( | |
| 191 | Tone::Warning, | |
| 192 | "Not running: signed out".into(), | |
| 193 | some("This computer isn't registered yet, and registering it needs you signed in to g1t."), | |
| 194 | some("Sign in, as an owner of the workspace the runner is for."), | |
| 195 | vec![SIGN_IN], | |
| 196 | ), | |
| 197 | Code::NoWorkspace => ( | |
| 198 | Tone::Warning, | |
| 199 | "Not running: no workspace to run jobs for".into(), | |
| 200 | some("Only a workspace's owners add runners to it, and you don't own one."), | |
| 201 | some("Ask an owner of your workspace to run it, or create a workspace of your own."), | |
| 202 | vec![NEW_WORKSPACE], | |
| 203 | ), | |
| 204 | Code::NotAllowed => ( | |
| 205 | Tone::Problem, | |
| 206 | format!("Couldn't register with {workspace}"), | |
| 207 | some(format!("You can't add runners to {workspace}: only its owners can.")), | |
| 208 | some(format!("Ask an owner of {workspace} to make you one, or choose a workspace you own.")), | |
| 209 | vec![CHOOSE_WORKSPACE], | |
| 210 | ), | |
| 211 | Code::RegisterFailed => ( | |
| 212 | Tone::Problem, | |
| 213 | format!("Couldn't register with {workspace}"), | |
| 214 | detail.or_else(|| some("g1t didn't make a registration for this computer.")), | |
| 215 | some("It tries again by itself in a few minutes."), | |
| 216 | vec![RETRY, VIEW_LOG], | |
| 217 | ), | |
| 218 | Code::Idle => ( | |
| 219 | Tone::Ok, | |
| 220 | "Waiting for jobs".into(), | |
| 221 | some(format!("Running {} for {workspace} on {}.", facts.backend, facts.machine)), | |
| 222 | None, | |
| 223 | vec![VIEW_RUNNERS], | |
| 224 | ), | |
| 225 | Code::Busy => ( | |
| 226 | Tone::Working, | |
| 227 | format!("Running {}", facts.job.as_deref().unwrap_or("a job")), | |
| 228 | some(format!("Running {} for {workspace} on {}.", facts.backend, facts.machine)), | |
| 229 | None, | |
| 230 | vec![VIEW_RUNNERS], | |
| 231 | ), | |
| 232 | Code::NoDocker => { | |
| 233 | let app = match facts.platform.as_str() { | |
| 234 | "windows" | "macos" => "Docker Desktop", | |
| 235 | _ => "Docker", | |
| 236 | }; | |
| 237 | ( | |
| 238 | Tone::Problem, | |
| 239 | "Not running: Docker isn't running".into(), | |
| 240 | some(format!("Running jobs {} needs Docker, and Docker isn't running on this computer, or isn't installed.", facts.backend)), | |
| 241 | some(format!("Start {app}, or install it. The runner starts by itself once Docker answers.")), | |
| 242 | if app == "Docker Desktop" { vec![OPEN_DOCKER, GET_DOCKER] } else { vec![GET_DOCKER] }, | |
| 243 | ) | |
| 244 | } | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 245 | 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. | 246 | 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. | 247 | "Not running: this g1t-runner has no sandbox".into(), |
| 248 | some("The g1t-runner g1t downloaded is older than its sandbox launcher, so it can't make a sandbox."), | |
| 249 | some("g1t looks for a newer one by itself; 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."), | |
| 250 | vec![RETRY, CHOOSE_BACKEND], | |
| 251 | ), | |
| 252 | Code::NoWsl => ( | |
| 253 | Tone::Problem, | |
| 254 | "Not running: WSL isn't installed".into(), | |
| 255 | some("Linux jobs run in a WSL distribution of the runner's own, and WSL isn't installed on this computer."), | |
| 256 | some(format!("Run `{}` in an admin terminal, then restart Windows. It works on every edition of Windows 11, Home included.", launcher::WSL_INSTALL)), | |
| 257 | vec![COPY_WSL_INSTALL, LEARN_MORE], | |
| 258 | ), | |
| 259 | Code::WslMirrored => ( | |
| 260 | Tone::Problem, | |
| 261 | "Not running: WSL uses mirrored networking".into(), | |
| 262 | some("WSL is set to mirrored networking, in which Linux reaches this computer's own localhost, so the sandbox won't start."), | |
| 263 | some("Remove networkingMode=mirrored from %USERPROFILE%\\.wslconfig, run `wsl --shutdown`, then Retry."), | |
| 264 | 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. | 265 | ), |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 266 | Code::NoVirtualization => ( |
| 267 | Tone::Problem, | |
| 268 | "Not running: virtualization is off".into(), | |
| 269 | some("The sandbox is a small virtual machine, and this computer has virtualization turned off."), | |
| 270 | some(if facts.platform == "windows" { | |
| 271 | "Turn on virtualization (Intel VT-x or AMD-V) in this computer's firmware settings, and Virtual Machine Platform in Windows features; then restart." | |
| 272 | } else { | |
| 273 | "Turn on virtualization (Intel VT-x or AMD-V) in this computer's firmware settings, then restart." | |
| 274 | }), | |
| 275 | if facts.platform == "windows" { vec![WINDOWS_FEATURES, LEARN_MORE] } else { vec![RETRY, LEARN_MORE] }, | |
| 276 | ), | |
| 277 | Code::NoVfkit => ( | |
| 278 | Tone::Problem, | |
| 279 | "Not running: vfkit isn't installed".into(), | |
| 280 | some("Linux jobs run in a small VM that vfkit runs with Apple's Virtualization framework, and vfkit isn't installed."), | |
| 281 | some(format!("Run `{}` in Terminal (macOS 13 or later). The runner starts by itself once it's there.", launcher::VFKIT_INSTALL)), | |
| 282 | vec![COPY_VFKIT_INSTALL, LEARN_MORE], | |
| 283 | ), | |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 284 | Code::NoSysbox => ( |
| 285 | 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. | 286 | "Not running: no Sysbox or KVM".into(), |
| 287 | some("The sandbox on Linux is the runner's image under Sysbox, or a VM under QEMU with KVM, and this computer has neither."), | |
| 288 | 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. | 289 | vec![INSTALL_SYSBOX, CHOOSE_BACKEND], |
| 290 | ), | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 291 | Code::ImageFailed => ( |
| 292 | Tone::Problem, | |
| 293 | "Not running: the sandbox's image didn't download".into(), | |
| 294 | detail.or_else(|| some("The sandbox's image couldn't be downloaded, or wasn't the release's, so it wasn't used.")), | |
| 295 | some("It's only ever used once its signature and SHA-256 are the release's. g1t tries again by itself; check the connection, or report it if it keeps happening."), | |
| 296 | vec![RETRY, REPORT], | |
| 297 | ), | |
| 298 | Code::SandboxUnavailable => ( | |
| 299 | Tone::Problem, | |
| 300 | "Not running: the sandbox can't run here".into(), | |
| 301 | detail.or_else(|| some("The sandbox can't be made on this computer.")), | |
| 302 | 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.")), | |
| 303 | vec![RETRY, LEARN_MORE, CHOOSE_BACKEND], | |
| 304 | ), | |
| 305 | Code::SandboxFailed => ( | |
| 306 | Tone::Problem, | |
| 307 | "Not running: the sandbox failed".into(), | |
| 308 | detail.or_else(|| some("The sandbox stopped, and didn't say why.")), | |
| 309 | Some(match facts.wait_seconds { | |
| 310 | Some(wait) => format!("It starts again by itself in {wait} seconds. Retry now, or look at its log."), | |
| 311 | None => "Retry, or look at its log.".into(), | |
| 312 | }), | |
| 313 | vec![RETRY, VIEW_LOG], | |
| 314 | ), | |
| 315 | Code::SandboxTaken => ( | |
| 316 | Tone::Warning, | |
| 317 | "Not running: this computer has another sandbox".into(), | |
| 318 | 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."), | |
| 319 | 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."), | |
| 320 | vec![RETRY, LEARN_MORE], | |
| 321 | ), | |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 322 | Code::Updating => (Tone::Working, "Updating".into(), detail, some("The runner starts again as soon as it's done."), vec![]), |
| 323 | Code::UpdateFailed => ( | |
| 324 | Tone::Warning, | |
| 325 | "Update failed".into(), | |
| 326 | detail, | |
| 327 | some("The runner keeps running the version it has. Try again, or report it if it keeps happening."), | |
| 328 | vec![RETRY, REPORT], | |
| 329 | ), | |
| 330 | Code::Offline => ( | |
| 331 | Tone::Warning, | |
| 332 | format!("Offline: {} unreachable", facts.server), | |
| 333 | some(format!("This computer can't reach {}, so the runner can't take jobs.", facts.server)), | |
| 334 | some("Check the connection. The runner carries on by itself when it's back."), | |
| 335 | vec![RETRY], | |
| 336 | ), | |
| 337 | Code::Crashed => ( | |
| 338 | Tone::Problem, | |
| 339 | match facts.exit_code { | |
| 340 | Some(exit) => format!("Runner stopped (exit code {exit})"), | |
| 341 | None => "Runner stopped".into(), | |
| 342 | }, | |
| 343 | detail, | |
| 344 | Some(match facts.wait_seconds { | |
| 345 | Some(wait) => format!("It starts again by itself in {wait} seconds. Restart now, or look at its log."), | |
| 346 | None => "Restart it, or look at its log.".into(), | |
| 347 | }), | |
| 348 | vec![RESTART, VIEW_LOG], | |
| 349 | ), | |
| 350 | }; | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 351 | let capabilities = facts.sandbox.as_ref().map(|status| capabilities(status, facts)).unwrap_or_default(); |
| 352 | Diagnosis { code, tone, title, cause, fix, actions, 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. | 353 | } |
| 354 | ||
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 355 | /// What the app calls a sandbox's problem with Linux jobs, from the |
| 356 | /// launcher's sentence about it, which goes with it as its detail. | |
| 357 | pub fn sandbox_code(why: Why) -> Code { | |
| 358 | match why { | |
| 359 | Why::NoWsl => Code::NoWsl, | |
| 360 | Why::WslMirrored => Code::WslMirrored, | |
| 361 | Why::NoVirtualization => Code::NoVirtualization, | |
| 362 | Why::NoVfkit => Code::NoVfkit, | |
| 363 | Why::NoSysboxOrKvm => Code::NoSysbox, | |
| 364 | Why::Image => Code::ImageFailed, | |
| 365 | _ => Code::SandboxFailed, | |
| 366 | } | |
| 367 | } | |
| 368 | ||
| 369 | /// The code for what is wrong with Linux jobs, as a status says it, and | |
| 370 | /// the launcher's own words for it. | |
| 371 | pub fn sandbox_trouble(status: &Status) -> Option<(Code, String, Option<String>)> { | |
| 372 | let (cause, fix) = status.trouble()?; | |
| 373 | let code = match launcher::why(&cause) { | |
| 374 | Why::Other if !status.linux().available => Code::SandboxUnavailable, | |
| 375 | why => sandbox_code(why), | |
| 376 | }; | |
| 377 | Some((code, cause, fix)) | |
| 378 | } | |
| 379 | ||
| 380 | /// Each kind of job the sandbox runs, on a line of its own. | |
| 381 | fn capabilities(status: &Status, facts: &Facts) -> Vec<CapabilityLine> { | |
| 382 | let listed = if status.capabilities.is_empty() { vec![status.linux()] } else { status.capabilities.clone() }; | |
| 383 | listed.iter().map(|capability| capability_line(status, capability, facts)).collect() | |
| 384 | } | |
| 385 | ||
| 386 | fn capability_line(status: &Status, capability: &launcher::Capability, facts: &Facts) -> CapabilityLine { | |
| 387 | let label = match capability.id.as_str() { | |
| 388 | "linux" => "Linux jobs".to_owned(), | |
| 389 | "windows" => "Windows jobs".to_owned(), | |
| 390 | "macos" => "macOS jobs".to_owned(), | |
| 391 | other => format!("{other} jobs"), | |
| 392 | }; | |
| 393 | let line = |tone: Tone, title: String, cause: Option<String>, fix: Option<String>, actions: Vec<Action>| CapabilityLine { | |
| 394 | id: capability.id.clone(), | |
| 395 | label: label.clone(), | |
| 396 | tone, | |
| 397 | title, | |
| 398 | cause, | |
| 399 | fix, | |
| 400 | actions, | |
| 401 | }; | |
| 402 | let title = if capability.available { "Not running" } else { "Not available" }; | |
| 403 | if capability.id == "linux" { | |
| 404 | if let Some((code, cause, fix)) = sandbox_trouble(status) { | |
| 405 | // The same words as the whole runner's when Linux jobs are what is wrong. | |
| 406 | let said = diagnose(code, &Facts { detail: Some(cause), fix, wait_seconds: None, sandbox: None, ..facts.clone() }); | |
| 407 | return line(Tone::Problem, title.into(), said.cause, said.fix, said.actions); | |
| 408 | } | |
| 409 | } else if !capability.available || capability.state == State::Error { | |
| 410 | let cause = capability.cause.clone(); | |
| 411 | return match cause.as_deref().map(launcher::why) { | |
| 412 | Some(Why::WindowsSandboxOff) => line( | |
| 413 | Tone::Off, | |
| 414 | title.into(), | |
| 415 | some("Windows Sandbox is off, or this edition of Windows has none: Home doesn't."), | |
| 416 | some("Linux jobs still run. For Windows jobs, on Windows 11 Pro, Enterprise or Education, turn on Windows Sandbox in Windows features, then restart."), | |
| 417 | vec![WINDOWS_FEATURES], | |
| 418 | ), | |
| 419 | Some(Why::NoWsb) => line( | |
| 420 | Tone::Off, | |
| 421 | title.into(), | |
| 422 | some("This Windows has Windows Sandbox, but not its wsb command, which came with Windows 11 24H2."), | |
| 423 | some("Linux jobs still run. For Windows jobs, update Windows to 24H2 or later."), | |
| 424 | vec![], | |
| 425 | ), | |
| 426 | Some(Why::MacNotReleased) => line(Tone::Off, title.into(), some("Sandboxed macOS jobs aren't released yet."), some("Linux jobs still run."), vec![]), | |
| 427 | _ => line(if capability.available { Tone::Problem } else { Tone::Off }, title.into(), cause, capability.fix.clone(), vec![]), | |
| 428 | }; | |
| 429 | } | |
| 430 | match capability.state { | |
| 431 | State::Running => match &capability.job { | |
| 432 | Some(job) => line(Tone::Working, format!("Running {}", job.said()), None, None, vec![]), | |
| 433 | None => line(Tone::Ok, "Waiting for jobs".into(), None, None, vec![]), | |
| 434 | }, | |
| 435 | State::Starting => line(Tone::Working, "Starting".into(), None, None, vec![]), | |
| 436 | State::Updating => line(Tone::Working, "Updating".into(), None, None, vec![]), | |
| 437 | State::Stopped => line(Tone::Off, "Stopped".into(), None, None, vec![]), | |
| 438 | State::Absent => line(Tone::Off, "Not set up yet".into(), None, None, vec![]), | |
| 439 | State::Error | State::Unknown => line(Tone::Off, "Not running".into(), capability.cause.clone(), capability.fix.clone(), vec![]), | |
| 440 | } | |
| 441 | } | |
| 442 | ||
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 443 | /// What the site says of this computer while g1t is closed on it. |
| 444 | pub fn closed(facts: &Facts) -> Diagnosis { | |
| 445 | Diagnosis { | |
| 446 | code: Code::Off, | |
| 447 | tone: Tone::Off, | |
| 448 | title: format!("g1t is closed on {}", facts.machine), | |
| 449 | cause: some(format!("This runner runs while g1t is open on {}.", facts.machine)), | |
| 450 | fix: some(format!("Open g1t on {} to run jobs there again.", facts.machine)), | |
| 451 | 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. | 452 | capabilities: vec![], |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 453 | } |
| 454 | } | |
| 455 | ||
| 456 | #[cfg(test)] | |
| 457 | mod tests { | |
| 458 | use super::*; | |
| 459 | ||
| 460 | fn facts() -> Facts { | |
| 461 | Facts { workspace: Some("acme".into()), backend: "sandboxed", machine: "build-box".into(), server: "g1t.sh".into(), platform: "windows".into(), ..Facts::default() } | |
| 462 | } | |
| 463 | ||
| 464 | #[test] | |
| 465 | fn every_failure_says_why_and_how_to_fix_it() { | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 466 | for code in [Code::SignedOut, Code::NoWorkspace, Code::NotAllowed, Code::RegisterFailed, Code::NoDocker, |
| 467 | Code::NoLauncher, Code::NoWsl, Code::WslMirrored, Code::NoVirtualization, Code::NoVfkit, Code::NoSysbox, Code::ImageFailed, Code::SandboxUnavailable, Code::SandboxFailed, | |
| 468 | Code::SandboxTaken, Code::UpdateFailed, Code::Offline, Code::Crashed, | |
| 469 | ] { | |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 470 | let diagnosis = diagnose(code, &Facts { detail: Some("why".into()), ..facts() }); |
| 471 | assert!(diagnosis.cause.is_some(), "{code:?} has no cause"); | |
| 472 | assert!(diagnosis.fix.is_some(), "{code:?} has no fix"); | |
| 473 | assert!(!diagnosis.actions.is_empty(), "{code:?} has no action"); | |
| 474 | assert!(matches!(diagnosis.tone, Tone::Warning | Tone::Problem), "{code:?}"); | |
| 475 | } | |
| 476 | } | |
| 477 | ||
| 478 | #[test] | |
| 479 | fn the_words_name_the_workspace_the_machine_and_the_job() { | |
| 480 | let not_allowed = diagnose(Code::NotAllowed, &facts()); | |
| 481 | assert_eq!(not_allowed.cause.as_deref(), Some("You can't add runners to acme: only its owners can.")); | |
| 482 | assert_eq!(not_allowed.actions[0].id, "choose_workspace"); | |
| 483 | let busy = diagnose(Code::Busy, &Facts { job: Some("Build (acme/web)".into()), ..facts() }); | |
| 484 | assert_eq!(busy.title, "Running Build (acme/web)"); | |
| 485 | assert_eq!(busy.cause.as_deref(), Some("Running sandboxed for acme on build-box.")); | |
| 486 | let crashed = diagnose(Code::Crashed, &Facts { exit_code: Some(101), wait_seconds: Some(30), ..facts() }); | |
| 487 | assert_eq!(crashed.title, "Runner stopped (exit code 101)"); | |
| 488 | assert_eq!(crashed.actions.iter().map(|a| a.id).collect::<Vec<_>>(), ["restart", "view_log"]); | |
| 489 | assert_eq!(diagnose(Code::NoDocker, &facts()).actions[0].id, "open_docker"); | |
| 490 | assert_eq!(closed(&facts()).title, "g1t is closed on build-box"); | |
| 491 | } | |
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 492 | |
| 493 | fn status(json: &str) -> Status { | |
| 494 | launcher::parse(json).unwrap() | |
| 495 | } | |
| 496 | ||
| 497 | const NO_WSL_PRO: &str = r#"{"schema":1,"state":"error","backend":"wsl2","launcher_version":"0.2.1", | |
| 498 | "error":"WSL is not installed, and the sandbox for Linux jobs is a WSL distribution of its own.", | |
| 499 | "capabilities":[ | |
| 500 | {"id":"linux","available":false,"state":"error","isolation":"nested-container", | |
| 501 | "cause":"WSL is not installed, and the sandbox for Linux jobs is a WSL distribution of its own.", | |
| 502 | "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."}, | |
| 503 | {"id":"windows","available":true,"state":"running","isolation":"vm","runner_version":"0.2.1","job":null}]}"#; | |
| 504 | ||
| 505 | #[test] | |
| 506 | fn without_wsl_the_fix_is_the_command_to_copy_and_windows_jobs_still_show() { | |
| 507 | let status = status(NO_WSL_PRO); | |
| 508 | let (code, cause, _) = sandbox_trouble(&status).unwrap(); | |
| 509 | assert_eq!(code, Code::NoWsl); | |
| 510 | let diagnosis = diagnose(code, &Facts { detail: Some(cause), sandbox: Some(status), ..facts() }); | |
| 511 | assert_eq!(diagnosis.title, "Not running: WSL isn't installed"); | |
| 512 | 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.")); | |
| 513 | assert_eq!(diagnosis.actions[0], COPY_WSL_INSTALL); | |
| 514 | let [linux, windows] = &diagnosis.capabilities[..] else { panic!("{:?}", diagnosis.capabilities) }; | |
| 515 | assert_eq!((linux.label.as_str(), linux.title.as_str(), linux.tone), ("Linux jobs", "Not available", Tone::Problem)); | |
| 516 | assert_eq!(linux.fix, diagnosis.fix); | |
| 517 | assert_eq!(linux.actions[0].id, "copy_wsl_install"); | |
| 518 | assert_eq!((windows.label.as_str(), windows.title.as_str(), windows.tone), ("Windows jobs", "Waiting for jobs", Tone::Ok)); | |
| 519 | } | |
| 520 | ||
| 521 | #[test] | |
| 522 | fn on_home_windows_jobs_are_unavailable_and_linux_jobs_still_run() { | |
| 523 | let status = status( | |
| 524 | r#"{"schema":1,"state":"running","backend":"wsl2","runner_version":"0.3.0","launcher_version":"0.3.0", | |
| 525 | "job":{"kind":"workflow","id":"job_1","name":"build","repo":"acme/web"}, | |
| 526 | "capabilities":[ | |
| 527 | {"id":"linux","available":true,"state":"running","runner_version":"0.3.0","job":{"kind":"workflow","id":"job_1","name":"build","repo":"acme/web"}}, | |
| 528 | {"id":"windows","available":false,"state":"absent","cause":"Windows Sandbox is not turned on, or this edition of Windows has none (Home does not).","fix":"On Windows 11 Pro…"}]}"#, | |
| 529 | ); | |
| 530 | assert_eq!(sandbox_trouble(&status), None); | |
| 531 | let busy = diagnose(Code::Busy, &Facts { job: status.job().map(launcher::Job::said), sandbox: Some(status), ..facts() }); | |
| 532 | assert_eq!(busy.title, "Running build (acme/web)"); | |
| 533 | let [linux, windows] = &busy.capabilities[..] else { panic!() }; | |
| 534 | assert_eq!((linux.title.as_str(), linux.tone), ("Running build (acme/web)", Tone::Working)); | |
| 535 | assert_eq!((windows.title.as_str(), windows.tone), ("Not available", Tone::Off)); | |
| 536 | assert_eq!(windows.cause.as_deref(), Some("Windows Sandbox is off, or this edition of Windows has none: Home doesn't.")); | |
| 537 | assert!(windows.fix.as_deref().unwrap().starts_with("Linux jobs still run.")); | |
| 538 | assert_eq!(windows.actions[0].id, "windows_features"); | |
| 539 | } | |
| 540 | ||
| 541 | #[test] | |
| 542 | fn each_launcher_trouble_has_its_words() { | |
| 543 | let trouble = |linux_available: bool, text: &str| { | |
| 544 | let json = serde_json::json!({ | |
| 545 | "schema": 1, "state": "error", "error": text, | |
| 546 | "capabilities": [{"id": "linux", "available": linux_available, "state": "error", "cause": if linux_available { None } else { Some(text) }, "fix": "the launcher's fix"}] | |
| 547 | }); | |
| 548 | sandbox_trouble(&status(&json.to_string())).unwrap() | |
| 549 | }; | |
| 550 | 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); | |
| 551 | assert_eq!(trouble(false, "vfkit, which runs the sandbox's VM with Apple's Virtualization framework, is not installed.").0, Code::NoVfkit); | |
| 552 | assert_eq!(trouble(false, "This machine has neither Sysbox nor KVM, so there is nothing to sandbox the runner with.").0, Code::NoSysbox); | |
| 553 | assert_eq!(trouble(false, "WSL is set to mirrored networking, in which Linux reaches this computer's own localhost.").0, Code::WslMirrored); | |
| 554 | 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); | |
| 555 | // Unknown words: shown as the launcher wrote them, its fix with them. | |
| 556 | let (code, cause, fix) = trouble(false, "This user may not use /dev/kvm."); | |
| 557 | assert_eq!(code, Code::SandboxUnavailable); | |
| 558 | let unavailable = diagnose(code, &Facts { detail: Some(cause), fix, ..facts() }); | |
| 559 | assert_eq!(unavailable.cause.as_deref(), Some("This user may not use /dev/kvm.")); | |
| 560 | assert_eq!(unavailable.fix.as_deref(), Some("the launcher's fix")); | |
| 561 | let (code, cause, _) = trouble(true, "the sandbox did not start within 600 seconds"); | |
| 562 | assert_eq!(code, Code::SandboxFailed); | |
| 563 | let failed = diagnose(code, &Facts { detail: Some(cause), wait_seconds: Some(60), ..facts() }); | |
| 564 | assert_eq!(failed.cause.as_deref(), Some("the sandbox did not start within 600 seconds")); | |
| 565 | assert_eq!(failed.fix.as_deref(), Some("It starts again by itself in 60 seconds. Retry now, or look at its log.")); | |
| 566 | let image = diagnose(Code::ImageFailed, &Facts { detail: Some("could not fetch https://g1t.sh/x: dns error".into()), ..facts() }); | |
| 567 | assert_eq!(image.cause.as_deref(), Some("could not fetch https://g1t.sh/x: dns error")); | |
| 568 | assert_eq!(diagnose(Code::NoVirtualization, &Facts { platform: "macos".into(), ..facts() }).actions[0].id, "retry"); | |
| 569 | assert_eq!(diagnose(Code::NoVfkit, &facts()).actions[0].id, "copy_vfkit_install"); | |
| 570 | } | |
| 571 | ||
| 572 | #[test] | |
| 573 | fn a_diagnosis_without_the_sandbox_lists_no_capabilities() { | |
| 574 | let json = serde_json::to_value(diagnose(Code::Idle, &Facts { backend: "in Docker", ..facts() })).unwrap(); | |
| 575 | assert!(json.get("capabilities").is_none()); | |
| 576 | let sandboxed = serde_json::to_value(diagnose(Code::Idle, &Facts { sandbox: Some(status(NO_WSL_PRO)), ..facts() })).unwrap(); | |
| 577 | assert_eq!(sandboxed["capabilities"][1]["id"], "windows"); | |
| 578 | } | |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers 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 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.