Skip to content

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

1,741 linesCodeBlameRaw
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.
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, 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.
22
23use serde::Serialize;
24
25use super::launcher::{self, State, Status, Why};
26
27/// Every state the runner can be in.
28#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
29#[serde(rename_all = "snake_case")]
30pub enum Code {
31 /// Running jobs is turned off in settings.
32 #[default]
33 Off,
34 /// The person pressed Stop.
35 Stopped,
36 /// Sandboxed, the person stopped every kind of job there is here.
37 KindsStopped,
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,
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.
62 NoSysbox,
63 /// This computer lacks something the sandbox needs, by name (the
64 /// launcher's `needs`): each kind of job's card says what, and Set up
65 /// does what Windows can do itself.
66 NeedsSetup,
67 /// The newest release is published without the sandbox's images yet.
68 WaitingForImage,
69 /// The sandbox's image couldn't be downloaded, or isn't the release's.
70 ImageFailed,
71 /// The sandbox can't be made here, for a reason in the launcher's words.
72 SandboxUnavailable,
73 /// The sandbox failed, in the launcher's words.
74 SandboxFailed,
75 /// The computer has a sandbox the app didn't make, and has room for one.
76 SandboxTaken,
77 Updating,
78 UpdateFailed,
79 /// The newest release's manifest isn't signed with g1t's release key.
80 UpdateRefused,
81 Offline,
82 Crashed,
83}
84
85/// How a state should look: a dot's colour.
86#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
87#[serde(rename_all = "snake_case")]
88pub enum Tone {
89 Ok,
90 Working,
91 Off,
92 Warning,
93 Problem,
94}
95
96/// A one-click fix.
97#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
98pub struct Action {
99 pub id: &'static str,
100 pub label: &'static str,
101 /// Its button stays busy, after a click, until the runner's status
102 /// that follows changes (or an error, or a while with no change): the
103 /// runner starting, stopping, retrying or looking for updates. One
104 /// that opens a page, a window or copies is done once the app has it.
105 pub waits: bool,
106 /// What the person is asked before it goes ahead, for a fix that can't be undone.
107 #[serde(skip_serializing_if = "Option::is_none")]
108 pub confirm: Option<&'static str>,
109}
110
111impl Action {
112 /// A fix that is done the moment the app has it.
113 const fn now(id: &'static str, label: &'static str) -> Self {
114 Self { id, label, waits: false, confirm: None }
115 }
116
117 /// A fix whose effect the runner's status shows when it comes.
118 const fn waits(id: &'static str, label: &'static str) -> Self {
119 Self { id, label, waits: true, confirm: None }
120 }
121
122 const fn confirming(self, question: &'static str) -> Self {
123 Self { confirm: Some(question), ..self }
124 }
125}
126
127#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
128pub struct Diagnosis {
129 pub code: Code,
130 pub tone: Tone,
131 /// One line: the tray's, and the heading everywhere else.
132 pub title: String,
133 /// Why, in a sentence.
134 pub cause: Option<String>,
135 /// What to do about it.
136 pub fix: Option<String>,
137 pub actions: Vec<Action>,
138 /// With the sandbox: each kind of job it runs, as a card of its own.
139 #[serde(skip_serializing_if = "Vec::is_empty")]
140 pub capabilities: Vec<CapabilityLine>,
141 /// The cards say all of this, each for its kind of job: a page that
142 /// shows them shows no line of the whole runner's as well.
143 #[serde(skip_serializing_if = "is_false")]
144 pub in_capabilities: bool,
145 /// How the runner's updates stand: when g1t last looked, and a newer
146 /// one waiting. None for what is said once g1t is closed.
147 #[serde(skip_serializing_if = "Option::is_none")]
148 pub update: Option<UpdateLine>,
149}
150
151fn is_false(value: &bool) -> bool {
152 !*value
153}
154
155/// The runner's updates, as settings and the site's Desktops show them.
156#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
157pub struct UpdateLine {
158 /// When g1t last heard from the runner's releases, RFC 3339: the site,
159 /// which shows a report for minutes, says how long ago itself.
160 pub checked_at: Option<String>,
161 /// `Checked 12 min ago`, `Checking for updates…`.
162 pub checked: String,
163 pub checking: bool,
164 /// A newer version, downloaded and checked, waiting to be installed.
165 pub ready: Option<String>,
166 /// What becomes of it, for a runner with no kinds of job of its own
167 /// (Docker, or on this computer): `0.3.2 ready, installs after this
168 /// job`. A sandbox's cards each say it for their own kind.
169 pub note: Option<String>,
170 /// Looks now: "Check for runner updates".
171 pub check: Action,
172}
173
174/// How one kind of job is, as its card's label says it.
175#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
176#[serde(rename_all = "snake_case")]
177pub enum KindState {
178 /// Running a job.
179 Busy,
180 /// Waiting for jobs.
181 Idle,
182 Starting,
183 /// Downloading the sandbox's image, which `progress` says how far.
184 Downloading,
185 Updating,
186 /// Waiting for something g1t sees to by itself, such as the release's images.
187 Waiting,
188 /// Stopped: by the person (`enabled` is false), or with the whole runner.
189 Stopped,
190 /// This computer can't run it, and `cause` says why.
191 Unavailable,
192 /// It should run and doesn't: `cause` and `fix`.
193 Problem,
194}
195
196/// One kind of job the sandbox runs, `linux` or `windows` (or `macos`),
197/// and how it is: Linux jobs can run while Windows jobs cannot.
198#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
199pub struct CapabilityLine {
200 pub id: String,
201 /// `Linux jobs`.
202 pub label: String,
203 pub tone: Tone,
204 pub state: KindState,
205 /// Its state in a few words, for its label: `Waiting for jobs`, `Not available`.
206 pub title: String,
207 /// What it is, in a line: `A Linux VM of its own, each job in a fresh container`.
208 pub about: String,
209 /// What is wrong, in a few words, over its cause: `WSL isn't installed`.
210 pub headline: Option<String>,
211 pub cause: Option<String>,
212 pub fix: Option<String>,
213 pub actions: Vec<Action>,
214 /// On in settings: the person hasn't stopped it.
215 pub enabled: bool,
216 /// This computer can run it.
217 pub available: bool,
218 /// It can be started and stopped on its own here (the launcher can).
219 pub can_toggle: bool,
220 /// What it is running now.
221 pub job: Option<JobLine>,
222 /// How far its image has downloaded, in percent.
223 pub progress: Option<u8>,
224 /// How it keeps jobs from the computer: `nested-container`, `vm`.
225 pub isolation: Option<String>,
226 /// Its runner's name on g1t: `build-box-linux`.
227 pub runner: Option<String>,
228 /// Its runner's version.
229 pub version: Option<String>,
230 /// A newer version waiting for it: `0.3.2 ready, installs after this job`.
231 pub update: Option<String>,
232}
233
234/// The job a kind of job is running, with where to see it on g1t.
235#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
236pub struct JobLine {
237 /// `workflow` or `agent`.
238 pub kind: String,
239 pub name: String,
240 pub repo: String,
241 /// The repository's Actions page, where its run is.
242 pub link: Option<String>,
243}
244
245/// What the words are filled in with.
246#[derive(Clone, Debug, Default)]
247pub struct Facts {
248 pub workspace: Option<String>,
249 /// How jobs run, as `sandboxed`, `in Docker`, `on this computer`.
250 pub backend: &'static str,
251 pub machine: String,
252 pub job: Option<String>,
253 pub detail: Option<String>,
254 pub exit_code: Option<i32>,
255 pub wait_seconds: Option<u64>,
256 /// The server's host: `g1t.sh`.
257 pub server: String,
258 /// The server's address, for links: `https://g1t.sh`.
259 pub site: String,
260 pub platform: String,
261 /// What the sandbox says to do, for a cause in its own words.
262 pub fix: Option<String>,
263 /// How the sandbox is, when jobs run sandboxed.
264 pub sandbox: Option<Status>,
265 /// The kinds of job the person stopped in settings.
266 pub off: Vec<String>,
267 /// The sandbox is up, or on its way: a kind of job that should run
268 /// and doesn't is failing, rather than stopped with everything else.
269 pub live: bool,
270 /// The kinds of job a launcher command is starting now.
271 pub starting: Vec<String>,
272 /// How far the sandbox's image has downloaded, while it does.
273 pub download: Option<launcher::Download>,
274 /// The person stopped the whole runner, or turned it off.
275 pub held: bool,
276 pub update: Update,
277 /// How Set up stands (setup.rs).
278 pub setup: SetUp,
279 /// What installs QEMU here: `sudo apt install qemu-system-x86`.
280 pub qemu_install: Option<String>,
281}
282
283/// How Set up stands: what Windows can do itself for the sandbox, done
284/// behind one administrator prompt (setup.rs).
285#[derive(Clone, Debug, Default, PartialEq, Eq)]
286pub struct SetUp {
287 /// Under way, and what it does: `turns on Virtual Machine Platform and installs WSL`.
288 pub running: Option<String>,
289 /// Why the last one didn't finish, until it is tried again.
290 pub failed: Option<String>,
291 /// The person chose Later over restarting now.
292 pub later: bool,
293 /// Every kind of job's needs, of those that are on: one press of Set
294 /// up meets them all, so each card says all it does.
295 pub needs: Vec<String>,
296}
297
298/// How the runner's updates stand.
299#[derive(Clone, Debug, Default, PartialEq, Eq)]
300pub struct Update {
301 /// The g1t-runner here.
302 pub installed: Option<String>,
303 /// When g1t last heard from the releases, RFC 3339, and how long ago.
304 pub checked_at: Option<String>,
305 pub checked_ago: Option<u64>,
306 pub checking: bool,
307 /// A newer release, downloaded and checked, waiting to be put in place.
308 pub ready: Option<String>,
309 /// What is still to take it: `linux` (the sandbox's image), `windows`
310 /// (the program its runner is), or `runner` for a runner with no kinds
311 /// of job of its own.
312 pub waiting: Vec<String>,
313 /// Those taking it now.
314 pub installing: Vec<String>,
315}
316
317/// When g1t last looked for a newer runner: `Checked 12 min ago`.
318pub fn checked(update: &Update) -> String {
319 if update.checking {
320 return "Checking for updates…".into();
321 }
322 let Some(ago) = update.checked_ago else { return "Not checked yet".into() };
323 let plural = |n: u64, one: &str| if n == 1 { format!("Checked 1 {one} ago") } else { format!("Checked {n} {one}s ago") };
324 match ago {
325 0..=59 => "Checked just now".into(),
326 60..=3599 => format!("Checked {} min ago", ago / 60),
327 3600..=86_399 => plural(ago / 3600, "hour"),
328 _ => plural(ago / 86_400, "day"),
329 }
330}
331
332/// What becomes of a newer version waiting for `what` (a kind of job, or
333/// `runner`), whose job is running when `busy`. None when nothing waits.
334fn waiting(update: &Update, what: &str, busy: bool) -> Option<String> {
335 let version = update.ready.as_deref()?;
336 if update.installing.iter().any(|kind| kind == what) {
337 return some(format!("Installing {version}"));
338 }
339 if !update.waiting.iter().any(|kind| kind == what) {
340 return None;
341 }
342 some(if busy { format!("{version} ready, installs after this job") } else { format!("{version} ready, installs in a moment") })
343}
344
345fn update_line(facts: &Facts, code: Code) -> UpdateLine {
346 let update = &facts.update;
347 UpdateLine {
348 checked_at: update.checked_at.clone(),
349 checked: checked(update),
350 checking: update.checking,
351 ready: update.ready.clone(),
352 note: waiting(update, "runner", code == Code::Busy),
353 check: CHECK_UPDATES,
354 }
355}
356
357const START: Action = Action::waits("start", "Start");
358const TURN_ON: Action = Action::waits("start", "Turn on");
359const RETRY: Action = Action::waits("retry", "Retry");
360const CHECK_NOW: Action = Action::waits("retry", "Check now");
361/// Looks for a newer runner now, and installs it as soon as its kinds of job are idle.
362const CHECK_UPDATES: Action = Action::waits("check_updates", "Check for runner updates");
363const CHECK_AGAIN: Action = Action::waits("check_updates", "Check again");
364/// Removes the sandbox this computer has (`sandbox down --delete`, in the
365/// app's own folder for it) and starts the app's own: what the command a
366/// person would type does, without needing to know where that folder is.
367const REMOVE_SANDBOX: Action = Action::waits("remove_sandbox", "Remove it and start again").confirming(
368 "Remove this computer's sandbox? Its VM and registration go, and g1t makes its own in their place, registered with your workspace. Its old runners stay listed on g1t until you remove them there.",
369);
370const RESTART: Action = Action::waits("restart", "Restart");
371const SIGN_IN: Action = Action::now("sign_in", "Sign in");
372const CHOOSE_WORKSPACE: Action = Action::now("choose_workspace", "Choose workspace");
373const NEW_WORKSPACE: Action = Action::now("new_workspace", "Create a workspace");
374const CHOOSE_BACKEND: Action = Action::now("choose_backend", "Choose how jobs run");
375/// Docker takes a while to start; the runner's status says when it has.
376const OPEN_DOCKER: Action = Action::waits("open_docker", "Open Docker");
377const GET_DOCKER: Action = Action::now("get_docker", "Get Docker");
378const INSTALL_SYSBOX: Action = Action::now("install_sysbox", "Install Sysbox");
379const COPY_WSL_INSTALL: Action = Action::now("copy_wsl_install", "Copy command");
380const COPY_VFKIT_INSTALL: Action = Action::now("copy_vfkit_install", "Copy command");
381const WINDOWS_FEATURES: Action = Action::now("windows_features", "Open Windows features");
382const LEARN_MORE: Action = Action::now("learn_more", "Learn more");
383const VIEW_LOG: Action = Action::now("view_log", "View log");
384const REPORT: Action = Action::now("report", "Report");
385const VIEW_RUNNERS: Action = Action::now("view_runners", "View on g1t");
386/// What Windows can do itself for the sandbox, at once, behind its own
387/// administrator prompt (setup.rs): busy until the card says it is setting up.
388const SET_UP: Action = Action::waits("set_up", "Set up");
389/// Asked with Windows' own dialog (setup.rs), which no page can answer for
390/// the person, rather than a page's: done once the app has it.
391const RESTART_WINDOWS: Action = Action::now("restart_windows", "Restart now");
392const RESTART_LATER: Action = Action::waits("restart_later", "Later");
393const WINDOWS_UPDATE: Action = Action::now("windows_update", "Open Windows Update");
394const WINDOWS_ACTIVATION: Action = Action::now("windows_activation", "Open Activation");
395const FIRMWARE_HELP: Action = Action::now("firmware_help", "How to turn it on");
396const NESTED_HELP: Action = Action::now("nested_help", "How to turn it on");
397const SETUP_HELP: Action = Action::now("setup_help", "Learn more");
398const COPY_KVM_LOAD: Action = Action::now("copy_kvm_load", "Copy command");
399const COPY_KVM_GROUP: Action = Action::now("copy_kvm_group", "Copy command");
400const COPY_QEMU_INSTALL: Action = Action::now("copy_qemu_install", "Copy command");
401const GET_HOMEBREW: Action = Action::now("get_homebrew", "Get Homebrew");
402const SOFTWARE_UPDATE: Action = Action::now("software_update", "Open Software Update");
403const OPEN_RUNNER: Action = Action::now("open_runner", "See what's missing");
404
405fn some(text: impl Into<String>) -> Option<String> {
406 Some(text.into())
407}
408
409/// A wait, said: `30 seconds`, `5 minutes`.
410fn said_wait(seconds: u64) -> String {
411 match seconds {
412 0..=1 => "a moment".into(),
413 2..=89 => format!("{seconds} seconds"),
414 _ => format!("{} minutes", seconds.div_ceil(60)),
415 }
416}
417
418/// The catalogue.
419pub fn diagnose(code: Code, facts: &Facts) -> Diagnosis {
420 let workspace = facts.workspace.as_deref().unwrap_or("your workspace");
421 let detail = facts.detail.clone();
422 let capabilities = facts.sandbox.as_ref().map(|status| capabilities(status, facts)).unwrap_or_default();
423 let (tone, title, cause, fix, actions): (Tone, String, Option<String>, Option<String>, Vec<Action>) = match code {
424 Code::Off => (
425 Tone::Off,
426 "Not running: turned off".into(),
427 some("Running jobs on this computer is turned off in g1t's settings."),
428 some("Turn it on to run your workspace's self-hosted jobs here while g1t is open."),
429 vec![TURN_ON],
430 ),
431 Code::Stopped => (
432 Tone::Off,
433 "Not running: stopped".into(),
434 some("You stopped the runner. It starts again when you press Start, or when g1t next opens."),
435 None,
436 vec![START],
437 ),
438 Code::KindsStopped => (
439 Tone::Off,
440 "Not running: every kind of job is stopped".into(),
441 some("You stopped each kind of job this computer can run."),
442 some("Start one to run jobs here again."),
443 vec![],
444 ),
445 Code::Starting => (Tone::Working, "Starting".into(), detail, None, vec![]),
446 Code::Registering => (
447 Tone::Working,
448 format!("Registering with {workspace}"),
449 some(format!("g1t is registering {} as a runner for {workspace}.", facts.machine)),
450 some("Keep g1t open and signed in; it takes a few seconds."),
451 vec![],
452 ),
453 Code::SignedOut => (
454 Tone::Warning,
455 "Not running: signed out".into(),
456 some("This computer isn't registered yet, and registering it needs you signed in to g1t."),
457 some("Sign in, as an owner of the workspace the runner is for."),
458 vec![SIGN_IN],
459 ),
460 Code::NoWorkspace => (
461 Tone::Warning,
462 "Not running: no workspace to run jobs for".into(),
463 some("Only a workspace's owners add runners to it, and you don't own one."),
464 some("Ask an owner of your workspace to run it, or create a workspace of your own."),
465 vec![NEW_WORKSPACE],
466 ),
467 Code::NotAllowed => (
468 Tone::Problem,
469 format!("Couldn't register with {workspace}"),
470 some(format!("You can't add runners to {workspace}: only its owners can.")),
471 some(format!("Ask an owner of {workspace} to make you one, or choose a workspace you own.")),
472 vec![CHOOSE_WORKSPACE],
473 ),
474 Code::RegisterFailed => (
475 Tone::Problem,
476 format!("Couldn't register with {workspace}"),
477 detail.or_else(|| some("g1t didn't make a registration for this computer.")),
478 some("g1t asks again by itself in a few minutes; Retry asks now. Its log says more."),
479 vec![RETRY, VIEW_LOG],
480 ),
481 Code::Idle => (
482 Tone::Ok,
483 "Waiting for jobs".into(),
484 some(format!("Running {} for {workspace} on {}.", facts.backend, facts.machine)),
485 None,
486 vec![VIEW_RUNNERS],
487 ),
488 Code::Busy => (
489 Tone::Working,
490 format!("Running {}", facts.job.as_deref().unwrap_or("a job")),
491 some(format!("Running {} for {workspace} on {}.", facts.backend, facts.machine)),
492 None,
493 vec![VIEW_RUNNERS],
494 ),
495 Code::NoDocker => {
496 let app = match facts.platform.as_str() {
497 "windows" | "macos" => "Docker Desktop",
498 _ => "Docker",
499 };
500 (
501 Tone::Problem,
502 "Not running: Docker isn't running".into(),
503 some(format!("Running jobs {} needs Docker, and Docker isn't running on this computer, or isn't installed.", facts.backend)),
504 some(format!("Start {app}, or install it. The runner starts by itself once Docker answers.")),
505 if app == "Docker Desktop" { vec![OPEN_DOCKER, GET_DOCKER] } else { vec![GET_DOCKER] },
506 )
507 }
508 Code::NoLauncher => (
509 Tone::Warning,
510 "Not running: this g1t-runner has no sandbox".into(),
511 some("The g1t-runner g1t downloaded is older than its sandbox launcher, so it can't make a sandbox."),
512 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."),
513 vec![RETRY, CHOOSE_BACKEND],
514 ),
515 Code::NoWsl => (
516 Tone::Problem,
517 "Not running: WSL isn't installed".into(),
518 some("Linux jobs run in a WSL distribution of the runner's own, and WSL isn't installed on this computer."),
519 some(format!("Run `{}` in an admin terminal, then restart Windows. It works on every edition of Windows 11, Home included.", launcher::WSL_INSTALL)),
520 vec![COPY_WSL_INSTALL, LEARN_MORE],
521 ),
522 Code::WslMirrored => (
523 Tone::Problem,
524 "Not running: WSL uses mirrored networking".into(),
525 some("WSL is set to mirrored networking, in which Linux reaches this computer's own localhost, so the sandbox won't start."),
526 some("Remove networkingMode=mirrored from %USERPROFILE%\\.wslconfig, run `wsl --shutdown`, then Retry."),
527 vec![RETRY, LEARN_MORE],
528 ),
529 Code::NoVirtualization => (
530 Tone::Problem,
531 "Not running: virtualization is off".into(),
532 some("The sandbox is a small virtual machine, and this computer has virtualization turned off."),
533 some(if facts.platform == "windows" {
534 "Turn on virtualization (Intel VT-x or AMD-V) in this computer's firmware settings, and Virtual Machine Platform in Windows features; then restart."
535 } else {
536 "Turn on virtualization (Intel VT-x or AMD-V) in this computer's firmware settings, then restart."
537 }),
538 if facts.platform == "windows" { vec![WINDOWS_FEATURES, LEARN_MORE] } else { vec![RETRY, LEARN_MORE] },
539 ),
540 Code::NoVfkit => (
541 Tone::Problem,
542 "Not running: vfkit isn't installed".into(),
543 some("Linux jobs run in a small VM that vfkit runs with Apple's Virtualization framework, and vfkit isn't installed."),
544 some(format!("Run `{}` in Terminal (macOS 13 or later). The runner starts by itself once it's there.", launcher::VFKIT_INSTALL)),
545 vec![COPY_VFKIT_INSTALL, LEARN_MORE],
546 ),
547 Code::NoSysbox => (
548 Tone::Problem,
549 "Not running: no Sysbox or KVM".into(),
550 some("The sandbox on Linux is the runner's image under Sysbox, or a VM under QEMU with KVM, and this computer has neither."),
551 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."),
552 vec![INSTALL_SYSBOX, CHOOSE_BACKEND],
553 ),
554 Code::NeedsSetup => {
555 // The first kind of job that waits on something, in its card's words.
556 let first = capabilities.iter().find(|kind| kind.enabled && kind.headline.is_some() && matches!(kind.tone, Tone::Warning | Tone::Problem | Tone::Working));
557 match first {
558 Some(kind) => (
559 if kind.state == KindState::Starting { Tone::Working } else { Tone::Warning },
560 format!("Not running: {}", lowered(kind.headline.as_deref().unwrap_or_default())),
561 kind.cause.clone(),
562 kind.fix.clone(),
563 if kind.actions.is_empty() { vec![OPEN_RUNNER] } else { kind.actions.clone() },
564 ),
565 None => (
566 Tone::Warning,
567 "Not running: this computer needs setting up".into(),
568 some("The sandbox needs something this computer doesn't have yet."),
569 some("Each kind of job's card says what, and sets up what it can."),
570 vec![OPEN_RUNNER],
571 ),
572 }
573 }
574 Code::WaitingForImage => {
575 let (release, image) = detail.as_deref().and_then(launcher::missing_images).map_or((None, None), |(release, image)| (Some(release), image));
576 let release = release.map_or_else(|| "The newest g1t-runner".to_owned(), |release| format!("g1t-runner {release}"));
577 (
578 Tone::Warning,
579 "Waiting for the sandbox's image".into(),
580 some(match image {
581 Some(image) => format!("{release} is published without the sandbox's image for this kind of computer ({image}) yet, so Linux jobs can't start."),
582 None => format!("{release} is published without the sandbox's images yet, so Linux jobs can't start."),
583 }),
584 some("g1t checks again every few minutes and starts Linux jobs once they're out. There's nothing for you to do."),
585 vec![CHECK_NOW],
586 )
587 }
588 Code::ImageFailed => {
589 let said = detail.clone().unwrap_or_default().to_ascii_lowercase();
590 if said.contains("sha-256") || said.contains("signature") {
591 (
592 Tone::Problem,
593 "Not running: the sandbox's image didn't check out".into(),
594 some("The sandbox's image that downloaded isn't the one g1t's release signed, so it wasn't used."),
595 some(format!(
596 "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.",
597 facts.wait_seconds.map(|wait| format!(" in {}", said_wait(wait))).unwrap_or_default(),
598 facts.server
599 )),
600 vec![RETRY, REPORT],
601 )
602 } else {
603 (
604 Tone::Problem,
605 "Not running: the sandbox's image didn't download".into(),
606 some(match &detail {
607 Some(detail) => format!("The sandbox's image couldn't be downloaded: {detail}"),
608 None => "The sandbox's image couldn't be downloaded.".into(),
609 }),
610 some(format!(
611 "g1t tries again by itself{}. Check that this computer can reach {}; Retry tries now.",
612 facts.wait_seconds.map(|wait| format!(" in {}", said_wait(wait))).unwrap_or_default(),
613 facts.server
614 )),
615 vec![RETRY, VIEW_LOG],
616 )
617 }
618 }
619 Code::SandboxUnavailable => (
620 Tone::Problem,
621 "Not running: the sandbox can't run here".into(),
622 detail.or_else(|| some("The sandbox can't be made on this computer.")),
623 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.")),
624 vec![RETRY, LEARN_MORE, CHOOSE_BACKEND],
625 ),
626 Code::SandboxFailed => (
627 Tone::Problem,
628 "Not running: the sandbox failed".into(),
629 detail.or_else(|| some("The sandbox stopped, and didn't say why.")),
630 some(match facts.wait_seconds {
631 Some(wait) => format!("It starts again by itself in {}. If it keeps stopping, its log says why; Retry starts it now.", said_wait(wait)),
632 None => "Retry starts it again; its log says why it stopped.".into(),
633 }),
634 vec![RETRY, VIEW_LOG],
635 ),
636 Code::SandboxTaken => (
637 Tone::Warning,
638 "Not running: this computer has another sandbox".into(),
639 some("This computer already has a g1t-runner sandbox that this copy of g1t has no record of making, such as one from before g1t was installed again, or one started with `g1t-runner sandbox up`. A computer has room for one."),
640 some("Remove it and start again: g1t removes it, with the jobs' VM and its registration, and makes its own, registered with your workspace. Its old runners stay listed on g1t, offline, until you remove them there. Or keep it, and turn off running jobs in g1t."),
641 vec![REMOVE_SANDBOX, LEARN_MORE],
642 ),
643 Code::Updating => (Tone::Working, "Updating".into(), detail, some("The runner starts again as soon as it's done."), vec![]),
644 Code::UpdateFailed => (
645 Tone::Warning,
646 "Update failed".into(),
647 detail,
648 some("The runner keeps running the version it has, and g1t tries the update again within the hour. Check again tries now; if it keeps failing, report it."),
649 vec![CHECK_AGAIN, REPORT],
650 ),
651 Code::UpdateRefused => {
652 let why = detail.unwrap_or_else(|| "its signature didn't check out".into());
653 let tampered = format!(
654 "If this keeps happening, something between this computer and {} is changing downloads, such as a proxy or antivirus: report it.",
655 facts.server
656 );
657 match facts.update.installed.as_deref() {
658 Some(version) => (
659 Tone::Warning,
660 "Update refused: not signed by g1t".into(),
661 some(format!("The newest g1t-runner release from {} isn't signed with g1t's release key ({why}), so g1t didn't install it.", facts.server)),
662 some(format!("The runner keeps running g1t-runner {version}, and g1t checks again within the hour. {tampered}")),
663 vec![CHECK_AGAIN, REPORT],
664 ),
665 None => (
666 Tone::Problem,
667 "Not running: the runner's release isn't signed by g1t".into(),
668 some(format!("The g1t-runner release from {} isn't signed with g1t's release key ({why}), so g1t didn't install it, and has no runner to start.", facts.server)),
669 some(format!("g1t tries again by itself in a few minutes; Retry tries now. {tampered}")),
670 vec![RETRY, REPORT],
671 ),
672 }
673 }
674 Code::Offline => (
675 Tone::Warning,
676 format!("Offline: {} unreachable", facts.server),
677 some(format!("This computer can't reach {}, so the runner can't take jobs.", facts.server)),
678 some("Check the connection. The runner carries on by itself when it's back."),
679 vec![RETRY],
680 ),
681 Code::Crashed => (
682 Tone::Problem,
683 match facts.exit_code {
684 Some(exit) => format!("Runner stopped (exit code {exit})"),
685 None => "Runner stopped".into(),
686 },
687 detail,
688 Some(match facts.wait_seconds {
689 Some(wait) => format!("It starts again by itself in {}. If it keeps stopping, its log says why; Restart starts it now.", said_wait(wait)),
690 None => "Restart starts it again; its log says why it stopped.".into(),
691 }),
692 vec![RESTART, VIEW_LOG],
693 ),
694 };
695 let in_capabilities = !capabilities.is_empty()
696 && match code {
697 Code::Idle | Code::Busy | Code::KindsStopped | Code::NeedsSetup => true,
698 Code::Starting | Code::Updating => facts.live,
699 // Said by a card, for its kind of job, word for word.
700 Code::NoWsl
701 | Code::WslMirrored
702 | Code::NoVirtualization
703 | Code::NoVfkit
704 | Code::NoSysbox
705 | Code::WaitingForImage
706 | Code::ImageFailed
707 | Code::SandboxUnavailable
708 | Code::SandboxFailed => cause.is_some() && capabilities.iter().any(|c| c.cause == cause),
709 _ => false,
710 };
711 let update = Some(update_line(facts, code));
712 Diagnosis { code, tone, title, cause, fix, actions, capabilities, in_capabilities, update }
713}
714
715/// What the app calls a sandbox's problem with Linux jobs, from the
716/// launcher's sentence about it, which goes with it as its detail.
717pub fn sandbox_code(why: Why) -> Code {
718 match why {
719 Why::NoWsl => Code::NoWsl,
720 Why::WslMirrored => Code::WslMirrored,
721 Why::NoVirtualization => Code::NoVirtualization,
722 Why::NoVfkit => Code::NoVfkit,
723 Why::NoSysboxOrKvm => Code::NoSysbox,
724 Why::NoImages => Code::WaitingForImage,
725 Why::Image => Code::ImageFailed,
726 _ => Code::SandboxFailed,
727 }
728}
729
730/// The code for what is wrong with Linux jobs, as a status says it, and
731/// the launcher's own words for it.
732pub fn sandbox_trouble(status: &Status) -> Option<(Code, String, Option<String>)> {
733 let (cause, fix) = status.trouble()?;
734 let code = match launcher::why(&cause) {
735 Why::Other if !status.linux().available => Code::SandboxUnavailable,
736 why => sandbox_code(why),
737 };
738 Some((code, cause, fix))
739}
740
741/// Each kind of job the sandbox runs, as a card of its own.
742fn capabilities(status: &Status, facts: &Facts) -> Vec<CapabilityLine> {
743 let listed = if status.capabilities.is_empty() { vec![status.linux()] } else { status.capabilities.clone() };
744 listed.iter().map(|capability| capability_line(status, capability, facts)).collect()
745}
746
747/// `Linux jobs`.
748pub fn kind_label(id: &str) -> String {
749 match id {
750 "linux" => "Linux jobs".to_owned(),
751 "windows" => "Windows jobs".to_owned(),
752 "macos" => "macOS jobs".to_owned(),
753 other => format!("{other} jobs"),
754 }
755}
756
757/// What a kind of job is, in a line, by how the sandbox makes it.
758fn about(id: &str, backend: Option<&str>) -> String {
759 match (id, backend) {
760 ("linux", Some("sysbox")) => "The runner under Sysbox, each job in a fresh container".into(),
761 ("linux", _) => "A Linux VM of its own, each job in a fresh container".into(),
762 ("windows", _) => "Each job in a Windows Sandbox, discarded after".into(),
763 ("macos", _) => "Each job in a macOS VM of its own, discarded after".into(),
764 _ => "Each job sandboxed".into(),
765 }
766}
767
768/// A headline inside a sentence: `WSL isn't installed` stays, `Virtualization is off` is `virtualization is off`.
769fn lowered(headline: &str) -> String {
770 let mut letters = headline.chars();
771 let Some(first) = letters.next() else { return String::new() };
772 // An initialism (WSL, KVM, QEMU) keeps its capitals.
773 if headline.chars().nth(1).is_some_and(char::is_uppercase) {
774 return headline.to_owned();
775 }
776 first.to_lowercase().chain(letters).collect()
777}
778
779/// What a kind of job's card says of what this computer lacks for it.
780struct NeedWords {
781 state: KindState,
782 tone: Tone,
783 title: &'static str,
784 headline: String,
785 cause: String,
786 fix: Option<String>,
787 actions: Vec<Action>,
788}
789
790/// The card's words for what this computer lacks (the launcher's `needs`,
791/// first what must come first): what Set up does when Windows can do all
792/// of it, else the first, with exact steps. None for a need this app
793/// doesn't know, from a newer launcher: its own words say it then.
794fn need_words(needs: &[String], id: &str, facts: &Facts) -> Option<NeedWords> {
795 let label = kind_label(id);
796 let first = needs.first()?.as_str();
797 let words = |headline: &str, cause: String, fix: String, actions: Vec<Action>| NeedWords {
798 state: KindState::Unavailable,
799 tone: Tone::Warning,
800 title: "Needs setting up",
801 headline: headline.into(),
802 cause,
803 fix: Some(fix),
804 actions,
805 };
806 if super::setup::fixable(needs) {
807 // What one press does: for every kind of job that is on, this one's included.
808 let all = if facts.setup.needs.is_empty() { needs } else { &facts.setup.needs[..] };
809 let plan = super::setup::plan_of(all);
810 if let Some(doing) = &facts.setup.running {
811 return Some(NeedWords {
812 state: KindState::Starting,
813 tone: Tone::Working,
814 title: "Setting up",
815 headline: "Setting up".into(),
816 cause: format!("g1t {doing}. Windows asks you to allow it first, and it takes a few minutes."),
817 fix: None,
818 actions: vec![],
819 });
820 }
821 let has = |need: &str| needs.iter().any(|n| n == need);
822 let headline = if has("wsl") {
823 "WSL isn't installed"
824 } else if has("vm_platform") {
825 "Virtual Machine Platform is off"
826 } else if has("wsl_update") {
827 "WSL needs an update"
828 } else {
829 "Windows Sandbox is off"
830 };
831 let cause = if id == "linux" {
832 let mut lacks = Vec::new();
833 if has("vm_platform") {
834 lacks.push("Virtual Machine Platform turned on");
835 }
836 if has("wsl") {
837 lacks.push("WSL installed");
838 } else if has("wsl_update") {
839 lacks.push("a newer WSL");
840 }
841 format!("Linux jobs run in a small Linux VM of their own under WSL 2, which needs {}.", lacks.join(" and "))
842 } else {
843 "Windows jobs each run in a Windows Sandbox of their own, which is turned off.".to_owned()
844 };
845 let restart = if plan.features.is_empty() { "" } else { ", and to restart after" };
846 let fix = format!("Set up {} in one go. Windows asks you to allow it{restart}.", plan.said());
847 return Some(match &facts.setup.failed {
848 Some(why) => NeedWords {
849 cause: format!("Set up didn't finish: {why}"),
850 fix: Some(format!("{fix} Its log says what happened.")),
851 actions: vec![SET_UP, VIEW_LOG],
852 ..words(headline, String::new(), String::new(), vec![])
853 },
854 None => words(headline, cause, fix, vec![SET_UP, SETUP_HELP]),
855 });
856 }
857 let linux_host = facts.platform == "linux";
858 Some(match first {
859 "restart" => NeedWords {
860 title: "Restart needed",
861 ..words(
862 "Restart to finish setting up",
863 format!("Windows has turned on what {label} need, and it takes effect once Windows restarts."),
864 if facts.setup.later {
865 "Restart when it suits you: g1t opens again after, and carries on by itself.".into()
866 } else {
867 "Restart now, or later: g1t opens again after the restart and carries on by itself.".into()
868 },
869 if facts.setup.later { vec![RESTART_WINDOWS] } else { vec![RESTART_WINDOWS, RESTART_LATER] },
870 )
871 },
872 "windows_update" if id == "windows" => words(
873 "Windows needs an update",
874 "Windows jobs use Windows Sandbox's command line, which came with Windows 11 24H2, and this Windows is older.".into(),
875 "Update Windows to 24H2 or later in Windows Update, and restart. g1t carries on after.".into(),
876 vec![WINDOWS_UPDATE, CHECK_NOW],
877 ),
878 "windows_update" => words(
879 "Windows needs an update",
880 "Linux jobs run in a small Linux VM under WSL 2, and this Windows is older than the first with it, Windows 10 version 2004.".into(),
881 "Update Windows in Windows Update, and restart. g1t sets up the rest after.".into(),
882 vec![WINDOWS_UPDATE, CHECK_NOW],
883 ),
884 "virtualization" => words(
885 "Virtualization is off",
886 if linux_host {
887 "The sandbox is a small virtual machine, and this computer's processor has virtualization (Intel VT-x or AMD-V) turned off in its firmware, or has none.".into()
888 } else {
889 "The sandbox is a small virtual machine, and this computer has virtualization (Intel VT-x or AMD-V) turned off in its firmware, where only you can turn it on.".into()
890 },
891 "Restart into the firmware settings (BIOS or UEFI: a key such as F2, F10, Del or Esc as the computer starts), turn on Intel Virtualization Technology or SVM Mode, then save and restart. g1t carries on after.".into(),
892 vec![FIRMWARE_HELP, CHECK_NOW],
893 ),
894 "nested_virtualization" => words(
895 "This VM can't run the sandbox",
896 "g1t is running in a virtual machine whose host doesn't pass virtualization through to it, and the sandbox is a virtual machine inside it.".into(),
897 "On the host, turn on nested virtualization for this VM, then start it again. On Hyper-V, with the VM off: Set-VMProcessor -VMName <name> -ExposeVirtualizationExtensions $true.".into(),
898 vec![NESTED_HELP, CHECK_NOW],
899 ),
900 "windows_edition" => NeedWords {
901 tone: Tone::Off,
902 title: "Not available",
903 ..words(
904 "Not in Windows Home",
905 "Windows jobs each run in a Windows Sandbox, and Windows Home doesn't have it.".into(),
906 "Linux jobs run here all the same. Windows jobs need Windows 11 Pro, Enterprise or Education.".into(),
907 vec![WINDOWS_ACTIVATION],
908 )
909 },
910 "kvm_module" => words(
911 "KVM isn't loaded",
912 "Linux jobs run in a small VM under QEMU with KVM, and /dev/kvm isn't there, though this processor can run virtual machines.".into(),
913 format!("Load it with `{}` (Copy command), then Check now. If it says the firmware disabled it, turn on VT-x or AMD-V there.", super::launcher::KVM_LOAD),
914 vec![COPY_KVM_LOAD, CHECK_NOW],
915 ),
916 "kvm_group" => words(
917 "No access to KVM",
918 "/dev/kvm is here, but your account isn't allowed to use it.".into(),
919 format!("Run `{}` (Copy command), then log out and back in.", super::launcher::KVM_GROUP),
920 vec![COPY_KVM_GROUP, CHECK_NOW],
921 ),
922 "qemu" => words(
923 "QEMU isn't installed",
924 "Linux jobs run in a small VM under QEMU, which isn't installed.".into(),
925 match &facts.qemu_install {
926 Some(command) => format!("Install it with `{command}` (Copy command), then Check now. Or install Sysbox 0.7 or later instead."),
927 None => "Install QEMU with your package manager, then Check now. Or install Sysbox 0.7 or later instead.".into(),
928 },
929 if facts.qemu_install.is_some() { vec![COPY_QEMU_INSTALL, CHECK_NOW] } else { vec![CHECK_NOW, INSTALL_SYSBOX] },
930 ),
931 "macos_update" => words(
932 "macOS needs an update",
933 "Linux jobs run in a small VM with Apple's Virtualization framework, through vfkit, which needs macOS 13 or later.".into(),
934 "Update macOS in Software Update, then open g1t again.".into(),
935 vec![SOFTWARE_UPDATE],
936 ),
937 "homebrew" => words(
938 "vfkit isn't installed",
939 "Linux jobs run in a small VM under vfkit, which installs with Homebrew, and neither is on this Mac.".into(),
940 format!("Install Homebrew from brew.sh (Get Homebrew), then run `{}` in Terminal (Copy command). The runner starts by itself once vfkit is there.", launcher::VFKIT_INSTALL),
941 vec![GET_HOMEBREW, COPY_VFKIT_INSTALL],
942 ),
943 "vfkit" => words(
944 "vfkit isn't installed",
945 "Linux jobs run in a small VM that vfkit runs with Apple's Virtualization framework, and vfkit isn't installed.".into(),
946 format!("Run `{}` in Terminal (Copy command). The runner starts by itself once it's there.", launcher::VFKIT_INSTALL),
947 vec![COPY_VFKIT_INSTALL, CHECK_NOW],
948 ),
949 _ => return None,
950 })
951}
952
953/// A catalogue title as a card's headline: `Not running: virtualization is off` is `Virtualization is off`.
954fn headline(title: &str) -> String {
955 let said = title.strip_prefix("Not running: ").unwrap_or(title);
956 let mut letters = said.chars();
957 letters.next().map(|first| first.to_uppercase().chain(letters).collect()).unwrap_or_default()
958}
959
960fn capability_line(status: &Status, capability: &launcher::Capability, facts: &Facts) -> CapabilityLine {
961 let id = capability.id.as_str();
962 let label = kind_label(id);
963 let enabled = !facts.off.iter().any(|off| off == id);
964 let per_kind = status.per_kind();
965 let mut line = CapabilityLine {
966 id: id.to_owned(),
967 label: label.clone(),
968 tone: Tone::Off,
969 state: KindState::Stopped,
970 title: "Stopped".into(),
971 about: about(id, status.backend.as_deref()),
972 headline: None,
973 cause: None,
974 fix: None,
975 actions: vec![],
976 enabled,
977 available: capability.available,
978 // Nothing to start or stop where it can't run, nor while the whole runner is
979 // stopped, whose own Start starts it (one the person stopped can be started).
980 can_toggle: per_kind && (capability.available || !enabled) && !(facts.held && enabled),
981 job: None,
982 progress: None,
983 isolation: capability.isolation.clone().filter(|_| capability.available),
984 runner: capability.available.then(|| format!("{}-{id}", facts.machine)),
985 version: capability.runner_version.clone(),
986 update: waiting(&facts.update, id, capability.job.is_some()),
987 };
988 let said = |line: &mut CapabilityLine, state: KindState, tone: Tone, cause: Option<String>, fix: Option<String>, actions: Vec<Action>| {
989 line.state = state;
990 line.tone = tone;
991 line.title = match state {
992 KindState::Busy => "Running a job",
993 KindState::Idle => "Waiting for jobs",
994 KindState::Starting => "Starting",
995 KindState::Downloading => "Downloading image",
996 KindState::Updating => "Updating",
997 KindState::Waiting => "Waiting for image",
998 KindState::Stopped => "Stopped",
999 KindState::Unavailable => "Not available",
1000 KindState::Problem => "Problem",
1001 }
1002 .into();
1003 line.cause = cause;
1004 line.fix = fix;
1005 line.actions = actions;
1006 };
1007 let again = |what: &str| format!("g1t {what} again by itself in a few minutes; Retry {what} now.");
1008 let starting = facts.starting.iter().any(|kind| kind == id) || matches!(capability.state, State::Starting);
1009
1010 // Stopped by the person: nothing is wrong with it.
1011 if capability.off() || (!enabled && per_kind) {
1012 said(&mut line, KindState::Stopped, Tone::Off, some(format!("You stopped {label} on this computer. They stay stopped until you start them.")), None, vec![]);
1013 return line;
1014 }
1015 // What this computer lacks for it, by name: said plainly, with Set up
1016 // for what Windows can do itself, and exact steps for the rest.
1017 if let Some(words) = need_words(&capability.needs, id, facts) {
1018 said(&mut line, words.state, words.tone, Some(words.cause), words.fix, words.actions);
1019 line.title = words.title.into();
1020 line.headline = Some(words.headline);
1021 return line;
1022 }
1023 // This computer can't run it, and why.
1024 if !capability.available {
1025 if id == "linux"
1026 && let Some((code, cause, fix)) = sandbox_trouble(status)
1027 {
1028 let whole = diagnose(code, &Facts { detail: Some(cause), fix, sandbox: None, ..facts.clone() });
1029 said(&mut line, KindState::Unavailable, Tone::Warning, whole.cause, whole.fix, whole.actions);
1030 line.headline = Some(headline(&whole.title));
1031 return line;
1032 }
1033 let cause = capability.cause.clone();
1034 let (headline, cause, fix, actions) = match cause.as_deref().map(launcher::why) {
1035 Some(Why::WindowsSandboxOff) => (
1036 "Windows Sandbox is off",
1037 some("Windows Sandbox is turned off, or this edition of Windows has none: Home doesn't."),
1038 some("On Windows 11 Pro, Enterprise or Education, turn on Windows Sandbox in Windows features, then restart."),
1039 vec![WINDOWS_FEATURES],
1040 ),
1041 Some(Why::NoWsb) => (
1042 "Windows needs an update",
1043 some("This Windows has Windows Sandbox, but not its wsb command, which came with Windows 11 24H2."),
1044 some("Update Windows to 24H2 or later, then restart g1t."),
1045 vec![],
1046 ),
1047 Some(Why::MacNotReleased) => (
1048 "Not released yet",
1049 some("Sandboxed macOS jobs aren't released yet."),
1050 some("They come with a g1t-runner update, which g1t downloads by itself. There's nothing for you to do."),
1051 vec![],
1052 ),
1053 _ => ("Can't run here", cause, capability.fix.clone(), vec![LEARN_MORE]),
1054 };
1055 said(&mut line, KindState::Unavailable, Tone::Off, cause, fix, actions);
1056 line.headline = Some(headline.into());
1057 return line;
1058 }
1059 // Linux jobs that should run and don't, in the catalogue's words.
1060 if id == "linux"
1061 && !starting
1062 && !matches!(capability.state, State::Updating)
1063 && let Some((code, cause, fix)) = sandbox_trouble(status)
1064 {
1065 let wait_seconds = if facts.live { None } else { facts.wait_seconds };
1066 let whole = diagnose(code, &Facts { detail: Some(cause), fix, wait_seconds, sandbox: None, ..facts.clone() });
1067 let state = if code == Code::WaitingForImage { KindState::Waiting } else { KindState::Problem };
1068 said(&mut line, state, whole.tone, whole.cause, whole.fix, whole.actions);
1069 line.headline = Some(headline(&whole.title));
1070 return line;
1071 }
1072 // A native kind of job whose runner didn't start, in the launcher's words.
1073 if id != "linux" && capability.state == State::Error && !starting {
1074 let cause = capability.cause.clone().or_else(|| some("Its runner didn't start, and didn't say why."));
1075 said(&mut line, KindState::Problem, Tone::Problem, cause, some(again("tries")), vec![RETRY, VIEW_LOG]);
1076 line.headline = Some("Its runner didn't start".into());
1077 return line;
1078 }
1079 let downloading = facts.download.as_ref().filter(|_| id == "linux");
1080 match capability.state {
1081 _ if starting || capability.state == State::Updating => match downloading {
1082 Some(download) => {
1083 said(&mut line, KindState::Downloading, Tone::Working, some("Downloading and checking the sandbox's image, once: it takes a few minutes."), None, vec![]);
1084 line.progress = Some(download.percent);
1085 }
1086 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![]),
1087 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![]),
1088 None => said(&mut line, KindState::Starting, Tone::Working, None, None, vec![]),
1089 },
1090 State::Running => match &capability.job {
1091 Some(job) => {
1092 said(&mut line, KindState::Busy, Tone::Working, None, None, vec![]);
1093 // Every job a runner takes is on the repository's Actions page:
1094 // workflow jobs, and the queue, merge check and checks tasks
1095 // the runner's wire still calls `agent` work.
1096 let link = (!job.repo.is_empty() && !facts.site.is_empty()).then(|| format!("{}/{}/actions", facts.site.trim_end_matches('/'), job.repo));
1097 line.job = Some(JobLine { kind: job.kind.clone(), name: job.name.clone(), repo: job.repo.clone(), link });
1098 }
1099 None => said(&mut line, KindState::Idle, Tone::Ok, None, None, vec![]),
1100 },
1101 // Up while the others run, yet not running: it stopped by itself,
1102 // or is to be registered.
1103 State::Stopped | State::Absent if facts.live && status.running() => {
1104 if capability.state == State::Absent {
1105 said(&mut line, KindState::Starting, Tone::Working, some(format!("Registering it with {}.", facts.workspace.as_deref().unwrap_or("your workspace"))), None, vec![]);
1106 } else {
1107 said(&mut line, KindState::Problem, Tone::Problem, some("Its runner stopped by itself."), some(again("starts it")), vec![RETRY, VIEW_LOG]);
1108 line.headline = Some("Its runner stopped".into());
1109 }
1110 }
1111 State::Stopped | State::Absent => said(&mut line, KindState::Stopped, Tone::Off, None, None, vec![]),
1112 State::Error | State::Unknown | State::Starting | State::Updating => {
1113 said(&mut line, KindState::Problem, Tone::Problem, capability.cause.clone().or_else(|| status.error.clone()), capability.fix.clone(), vec![RETRY, VIEW_LOG]);
1114 }
1115 }
1116 // The person stopped it, and this launcher can't stop one kind alone yet.
1117 if !enabled && line.fix.is_none() {
1118 line.fix = some(format!("This g1t-runner can't stop {label} on their own; the next one can, and g1t downloads it by itself."));
1119 }
1120 line
1121}
1122
1123/// The runner in a few words, for the chip beside the title bar's settings
1124/// button, which opens the Runner window: `Runner running`, `Running 2 jobs`.
1125#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
1126pub struct Chip {
1127 pub tone: Tone,
1128 pub label: String,
1129}
1130
1131/// The chip, from the whole runner's diagnosis and its kinds of job: what
1132/// it is doing, and whether anything there wants the person.
1133pub fn chip(diagnosis: &Diagnosis) -> Chip {
1134 let jobs = diagnosis.capabilities.iter().filter(|kind| kind.job.is_some()).count().max(usize::from(diagnosis.code == Code::Busy));
1135 let wanting = diagnosis.capabilities.iter().any(|kind| kind.enabled && matches!(kind.tone, Tone::Problem | Tone::Warning));
1136 let (tone, label) = match diagnosis.code {
1137 _ if jobs > 1 => (Tone::Working, format!("Running {jobs} jobs")),
1138 _ if jobs == 1 => (Tone::Working, "Running a job".to_owned()),
1139 Code::Idle => (if wanting { Tone::Warning } else { Tone::Ok }, "Runner running".to_owned()),
1140 Code::Starting | Code::Registering => (Tone::Working, "Runner starting".to_owned()),
1141 Code::Updating => (Tone::Working, "Runner updating".to_owned()),
1142 Code::UpdateFailed | Code::UpdateRefused => (Tone::Warning, "Runner running".to_owned()),
1143 Code::Off => (Tone::Off, "Runner off".to_owned()),
1144 Code::Stopped | Code::KindsStopped => (Tone::Off, "Runner stopped".to_owned()),
1145 Code::Offline => (Tone::Warning, "Runner offline".to_owned()),
1146 Code::NeedsSetup if diagnosis.tone == Tone::Working => (Tone::Working, "Runner setting up".to_owned()),
1147 _ => (diagnosis.tone, "Runner needs you".to_owned()),
1148 };
1149 Chip { tone, label }
1150}
1151
1152/// What the site says of this computer while g1t is closed on it.
1153pub fn closed(facts: &Facts) -> Diagnosis {
1154 Diagnosis {
1155 code: Code::Off,
1156 tone: Tone::Off,
1157 title: format!("g1t is closed on {}", facts.machine),
1158 cause: some(format!("This runner runs while g1t is open on {}.", facts.machine)),
1159 fix: some(format!("Open g1t on {} to run jobs there again.", facts.machine)),
1160 actions: vec![],
1161 capabilities: vec![],
1162 in_capabilities: false,
1163 update: None,
1164 }
1165}
1166
1167#[cfg(test)]
1168mod tests {
1169 use super::*;
1170
1171 fn facts() -> Facts {
1172 Facts {
1173 workspace: Some("acme".into()),
1174 backend: "sandboxed",
1175 machine: "build-box".into(),
1176 server: "g1t.sh".into(),
1177 site: "https://g1t.sh".into(),
1178 platform: "windows".into(),
1179 ..Facts::default()
1180 }
1181 }
1182
1183 #[test]
1184 fn a_fix_that_changes_the_runner_waits_for_its_status() {
1185 for action in [START, TURN_ON, RETRY, CHECK_NOW, CHECK_UPDATES, CHECK_AGAIN, REMOVE_SANDBOX, RESTART, OPEN_DOCKER] {
1186 assert!(action.waits, "{} waits for the status that follows", action.id);
1187 }
1188 for action in [SIGN_IN, CHOOSE_WORKSPACE, CHOOSE_BACKEND, GET_DOCKER, COPY_WSL_INSTALL, LEARN_MORE, VIEW_LOG, VIEW_RUNNERS] {
1189 assert!(!action.waits, "{} is done once the app has it", action.id);
1190 }
1191 // Only what can't be undone asks first.
1192 assert!(REMOVE_SANDBOX.confirm.is_some_and(|question| question.ends_with('.') && question.contains('?')));
1193 assert!(RETRY.confirm.is_none());
1194 let said = serde_json::to_value(RETRY).unwrap();
1195 assert_eq!(said, serde_json::json!({ "id": "retry", "label": "Retry", "waits": true }));
1196 }
1197
1198 #[test]
1199 fn every_failure_says_why_and_how_to_fix_it() {
1200 for code in [
1201 Code::SignedOut,
1202 Code::NoWorkspace,
1203 Code::NotAllowed,
1204 Code::RegisterFailed,
1205 Code::NoDocker,
1206 Code::NoLauncher,
1207 Code::NoWsl,
1208 Code::WslMirrored,
1209 Code::NoVirtualization,
1210 Code::NoVfkit,
1211 Code::NoSysbox,
1212 Code::WaitingForImage,
1213 Code::ImageFailed,
1214 Code::SandboxUnavailable,
1215 Code::SandboxFailed,
1216 Code::SandboxTaken,
1217 Code::UpdateFailed,
1218 Code::UpdateRefused,
1219 Code::Offline,
1220 Code::Crashed,
1221 ] {
1222 let diagnosis = diagnose(code, &Facts { detail: Some("why".into()), ..facts() });
1223 assert!(diagnosis.cause.is_some(), "{code:?} has no cause");
1224 assert!(diagnosis.fix.is_some(), "{code:?} has no fix");
1225 assert!(!diagnosis.actions.is_empty(), "{code:?} has no action");
1226 assert!(matches!(diagnosis.tone, Tone::Warning | Tone::Problem), "{code:?}");
1227 // No fix sends the person off to check their connection or report it
1228 // when that isn't what is wrong.
1229 let fix = diagnosis.fix.unwrap();
1230 if !matches!(code, Code::Offline | Code::ImageFailed) {
1231 assert!(!fix.contains("check the connection") && !fix.contains("Check the connection"), "{code:?}: {fix}");
1232 }
1233 }
1234 }
1235
1236 #[test]
1237 fn the_words_name_the_workspace_the_machine_and_the_job() {
1238 let not_allowed = diagnose(Code::NotAllowed, &facts());
1239 assert_eq!(not_allowed.cause.as_deref(), Some("You can't add runners to acme: only its owners can."));
1240 assert_eq!(not_allowed.actions[0].id, "choose_workspace");
1241 let busy = diagnose(Code::Busy, &Facts { job: Some("Build (acme/web)".into()), ..facts() });
1242 assert_eq!(busy.title, "Running Build (acme/web)");
1243 assert_eq!(busy.cause.as_deref(), Some("Running sandboxed for acme on build-box."));
1244 let crashed = diagnose(Code::Crashed, &Facts { exit_code: Some(101), wait_seconds: Some(30), ..facts() });
1245 assert_eq!(crashed.title, "Runner stopped (exit code 101)");
1246 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."));
1247 assert_eq!(crashed.actions.iter().map(|a| a.id).collect::<Vec<_>>(), ["restart", "view_log"]);
1248 assert_eq!(diagnose(Code::NoDocker, &facts()).actions[0].id, "open_docker");
1249 assert_eq!(closed(&facts()).title, "g1t is closed on build-box");
1250 assert_eq!(said_wait(300), "5 minutes");
1251 assert_eq!(said_wait(45), "45 seconds");
1252 }
1253
1254 fn status(json: &str) -> Status {
1255 launcher::parse(json).unwrap()
1256 }
1257
1258 const NO_WSL_PRO: &str = r#"{"schema":1,"state":"error","backend":"wsl2","launcher_version":"0.2.1",
1259 "error":"WSL is not installed, and the sandbox for Linux jobs is a WSL distribution of its own.",
1260 "capabilities":[
1261 {"id":"linux","available":false,"state":"error","isolation":"nested-container",
1262 "cause":"WSL is not installed, and the sandbox for Linux jobs is a WSL distribution of its own.",
1263 "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."},
1264 {"id":"windows","available":true,"state":"running","isolation":"vm","runner_version":"0.2.1","job":null}]}"#;
1265
1266 #[test]
1267 fn without_wsl_the_fix_is_the_command_to_copy_and_windows_jobs_still_show() {
1268 let status = status(NO_WSL_PRO);
1269 let (code, cause, _) = sandbox_trouble(&status).unwrap();
1270 assert_eq!(code, Code::NoWsl);
1271 let diagnosis = diagnose(code, &Facts { detail: Some(cause), sandbox: Some(status), live: true, ..facts() });
1272 assert_eq!(diagnosis.title, "Not running: WSL isn't installed");
1273 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."));
1274 assert_eq!(diagnosis.actions[0], COPY_WSL_INSTALL);
1275 let [linux, windows] = &diagnosis.capabilities[..] else { panic!("{:?}", diagnosis.capabilities) };
1276 assert_eq!((linux.label.as_str(), linux.title.as_str(), linux.state), ("Linux jobs", "Not available", KindState::Unavailable));
1277 assert_eq!(linux.headline.as_deref(), Some("WSL isn't installed"));
1278 assert_eq!(linux.fix, diagnosis.fix);
1279 assert_eq!(linux.actions[0].id, "copy_wsl_install");
1280 assert_eq!((windows.label.as_str(), windows.title.as_str(), windows.tone), ("Windows jobs", "Waiting for jobs", Tone::Ok));
1281 assert_eq!(windows.runner.as_deref(), Some("build-box-windows"));
1282 assert_eq!(windows.about, "Each job in a Windows Sandbox, discarded after");
1283 // The Linux card says it all: the whole runner's line isn't shown as well.
1284 assert!(diagnosis.in_capabilities);
1285 // An older launcher can't stop one kind of job alone.
1286 assert!(!windows.can_toggle);
1287 }
1288
1289 #[test]
1290 fn on_home_windows_jobs_are_unavailable_and_linux_jobs_still_run() {
1291 let status = status(
1292 r#"{"schema":1,"state":"running","backend":"wsl2","runner_version":"0.3.0","launcher_version":"0.3.0",
1293 "job":{"kind":"workflow","id":"job_1","name":"build","repo":"acme/web"},
1294 "capabilities":[
1295 {"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"}},
1296 {"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…"}]}"#,
1297 );
1298 assert_eq!(sandbox_trouble(&status), None);
1299 let busy = diagnose(Code::Busy, &Facts { job: status.job().map(launcher::Job::said), sandbox: Some(status), live: true, ..facts() });
1300 assert_eq!(busy.title, "Running build (acme/web)");
1301 assert!(busy.in_capabilities);
1302 let [linux, windows] = &busy.capabilities[..] else { panic!() };
1303 assert_eq!((linux.title.as_str(), linux.state, linux.tone), ("Running a job", KindState::Busy, Tone::Working));
1304 let job = linux.job.as_ref().unwrap();
1305 assert_eq!((job.name.as_str(), job.repo.as_str(), job.link.as_deref()), ("build", "acme/web", Some("https://g1t.sh/acme/web/actions")));
1306 assert!(linux.can_toggle && linux.enabled);
1307 assert_eq!((windows.title.as_str(), windows.tone, windows.state), ("Not available", Tone::Off, KindState::Unavailable));
1308 assert_eq!(windows.headline.as_deref(), Some("Windows Sandbox is off"));
1309 assert_eq!(windows.cause.as_deref(), Some("Windows Sandbox is turned off, or this edition of Windows has none: Home doesn't."));
1310 assert_eq!(windows.actions[0].id, "windows_features");
1311 // Nothing to start or stop where it can't run.
1312 assert!(!windows.can_toggle);
1313 assert_eq!(windows.runner, None);
1314 }
1315
1316 /// 0.3.0 was published without the sandbox's images: Linux jobs wait for
1317 /// them, plainly, while Windows jobs run.
1318 const NO_IMAGES: &str = r#"{"schema":1,"state":"error","backend":"wsl2","launcher_version":"0.3.0",
1319 "error":"Windows jobs are sandboxed and running, but Linux jobs are not: release 0.3.0 has no sandbox images",
1320 "capabilities":[
1321 {"id":"linux","available":true,"state":"error","isolation":"nested-container","turned_off":false},
1322 {"id":"windows","available":true,"state":"running","isolation":"vm","runner_version":"0.3.0","turned_off":false}]}"#;
1323
1324 #[test]
1325 fn a_release_without_images_is_waited_for_not_reported() {
1326 let status = status(NO_IMAGES);
1327 let (code, cause, _) = sandbox_trouble(&status).unwrap();
1328 assert_eq!(code, Code::WaitingForImage);
1329 let whole = diagnose(code, &Facts { detail: Some(cause), ..facts() });
1330 assert_eq!(whole.title, "Waiting for the sandbox's image");
1331 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."));
1332 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."));
1333 assert_eq!(whole.actions, [CHECK_NOW]);
1334 assert!(!whole.fix.unwrap().contains("report"));
1335 // Windows jobs run: the runner waits for jobs, and the Linux card says what it waits for.
1336 let idle = diagnose(Code::Idle, &Facts { sandbox: Some(status), live: true, ..facts() });
1337 assert!(idle.in_capabilities);
1338 let [linux, windows] = &idle.capabilities[..] else { panic!() };
1339 assert_eq!((linux.state, linux.title.as_str(), linux.tone), (KindState::Waiting, "Waiting for image", Tone::Warning));
1340 assert_eq!(linux.headline.as_deref(), Some("Waiting for the sandbox's image"));
1341 assert_eq!(windows.state, KindState::Idle);
1342 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() });
1343 assert_eq!(image.title, "Not running: the sandbox's image didn't check out");
1344 let fetch = diagnose(Code::ImageFailed, &Facts { detail: Some("could not fetch https://g1t.sh/x: dns error".into()), wait_seconds: Some(60), ..facts() });
1345 assert_eq!(fetch.cause.as_deref(), Some("The sandbox's image couldn't be downloaded: could not fetch https://g1t.sh/x: dns error"));
1346 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."));
1347 }
1348
1349 #[test]
1350 fn a_kind_the_person_stopped_says_so_and_is_not_failing() {
1351 let status = status(
1352 r#"{"schema":1,"state":"running","backend":"wsl2","capabilities":[
1353 {"id":"linux","available":true,"state":"running","turned_off":false},
1354 {"id":"windows","available":true,"state":"stopped","turned_off":true}]}"#,
1355 );
1356 let idle = diagnose(Code::Idle, &Facts { sandbox: Some(status.clone()), live: true, off: vec!["windows".into()], ..facts() });
1357 let [linux, windows] = &idle.capabilities[..] else { panic!() };
1358 assert_eq!(linux.state, KindState::Idle);
1359 assert_eq!((windows.state, windows.tone, windows.enabled), (KindState::Stopped, Tone::Off, false));
1360 assert_eq!(windows.cause.as_deref(), Some("You stopped Windows jobs on this computer. They stay stopped until you start them."));
1361 assert!(windows.can_toggle && windows.actions.is_empty());
1362 // Stopped by itself while Linux jobs run is a problem, with Retry.
1363 let died = launcher::parse(&serde_json::to_string(&serde_json::json!({"schema":1,"state":"running","capabilities":[
1364 {"id":"linux","available":true,"state":"running","turned_off":false},
1365 {"id":"windows","available":true,"state":"stopped","turned_off":false}]})).unwrap()).unwrap();
1366 let idle = diagnose(Code::Idle, &Facts { sandbox: Some(died.clone()), live: true, ..facts() });
1367 assert_eq!((idle.capabilities[1].state, idle.capabilities[1].actions[0].id), (KindState::Problem, "retry"));
1368 // With the whole runner stopped, it is only stopped.
1369 let stopped = diagnose(Code::Stopped, &Facts { sandbox: Some(died), live: false, held: true, ..facts() });
1370 // Its own Start or Stop waits for the runner's: the line above the cards starts it.
1371 assert!(!stopped.capabilities[0].can_toggle);
1372 assert_eq!(stopped.capabilities[1].state, KindState::Stopped);
1373 assert!(!stopped.in_capabilities);
1374 }
1375
1376 #[test]
1377 fn a_download_shows_how_far_it_is() {
1378 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}]}"#);
1379 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() };
1380 let starting = diagnose(Code::Starting, &facts);
1381 assert!(starting.in_capabilities);
1382 let [linux, windows] = &starting.capabilities[..] else { panic!() };
1383 assert_eq!((linux.state, linux.title.as_str(), linux.progress), (KindState::Downloading, "Downloading image", Some(40)));
1384 assert_eq!((windows.state, windows.progress), (KindState::Starting, None));
1385 }
1386
1387 #[test]
1388 fn each_launcher_trouble_has_its_words() {
1389 let trouble = |linux_available: bool, text: &str| {
1390 let json = serde_json::json!({
1391 "schema": 1, "state": "error", "error": text,
1392 "capabilities": [{"id": "linux", "available": linux_available, "state": "error", "cause": if linux_available { None } else { Some(text) }, "fix": "the launcher's fix"}]
1393 });
1394 sandbox_trouble(&status(&json.to_string())).unwrap()
1395 };
1396 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);
1397 assert_eq!(trouble(false, "vfkit, which runs the sandbox's VM with Apple's Virtualization framework, is not installed.").0, Code::NoVfkit);
1398 assert_eq!(trouble(false, "This machine has neither Sysbox nor KVM, so there is nothing to sandbox the runner with.").0, Code::NoSysbox);
1399 assert_eq!(trouble(false, "WSL is set to mirrored networking, in which Linux reaches this computer's own localhost.").0, Code::WslMirrored);
1400 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);
1401 assert_eq!(trouble(true, "release 0.3.0 has no sandbox images").0, Code::WaitingForImage);
1402 // Unknown words: shown as the launcher wrote them, its fix with them.
1403 let (code, cause, fix) = trouble(false, "This user may not use /dev/kvm.");
1404 assert_eq!(code, Code::SandboxUnavailable);
1405 let unavailable = diagnose(code, &Facts { detail: Some(cause), fix, ..facts() });
1406 assert_eq!(unavailable.cause.as_deref(), Some("This user may not use /dev/kvm."));
1407 assert_eq!(unavailable.fix.as_deref(), Some("the launcher's fix"));
1408 let (code, cause, _) = trouble(true, "the sandbox did not start within 600 seconds");
1409 assert_eq!(code, Code::SandboxFailed);
1410 let failed = diagnose(code, &Facts { detail: Some(cause), wait_seconds: Some(60), ..facts() });
1411 assert_eq!(failed.cause.as_deref(), Some("the sandbox did not start within 600 seconds"));
1412 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."));
1413 assert_eq!(diagnose(Code::NoVirtualization, &Facts { platform: "macos".into(), ..facts() }).actions[0].id, "retry");
1414 assert_eq!(diagnose(Code::NoVfkit, &facts()).actions[0].id, "copy_vfkit_install");
1415 }
1416
1417 #[test]
1418 fn when_g1t_last_looked_for_a_runner_is_said_plainly() {
1419 let ago = |seconds: u64| checked(&Update { checked_ago: Some(seconds), ..Update::default() });
1420 assert_eq!(checked(&Update::default()), "Not checked yet");
1421 assert_eq!(checked(&Update { checking: true, checked_ago: Some(30), ..Update::default() }), "Checking for updates…");
1422 assert_eq!(ago(20), "Checked just now");
1423 assert_eq!(ago(12 * 60 + 5), "Checked 12 min ago");
1424 assert_eq!(ago(3600), "Checked 1 hour ago");
1425 assert_eq!(ago(5 * 3600), "Checked 5 hours ago");
1426 assert_eq!(ago(2 * 86_400), "Checked 2 days ago");
1427 }
1428
1429 /// 0.3.2 is downloaded: Linux jobs take its image after the job they
1430 /// are running, Windows jobs, idle, take its program now.
1431 #[test]
1432 fn a_waiting_update_is_said_on_each_kind_of_jobs_card() {
1433 let status = status(
1434 r#"{"schema":1,"state":"running","backend":"wsl2","image_version":"0.3.1","capabilities":[
1435 {"id":"linux","available":true,"state":"running","runner_version":"0.3.1","turned_off":false,"job":{"kind":"workflow","id":"j","name":"build","repo":"acme/web"}},
1436 {"id":"windows","available":true,"state":"running","runner_version":"0.3.1","turned_off":false}]}"#,
1437 );
1438 let update = Update {
1439 installed: Some("0.3.1".into()),
1440 checked_at: Some("2026-10-12T12:00:00Z".into()),
1441 checked_ago: Some(12 * 60),
1442 ready: Some("0.3.2".into()),
1443 waiting: vec!["linux".into(), "windows".into()],
1444 ..Update::default()
1445 };
1446 let busy = diagnose(Code::Busy, &Facts { sandbox: Some(status.clone()), live: true, update: update.clone(), ..facts() });
1447 let [linux, windows] = &busy.capabilities[..] else { panic!() };
1448 assert_eq!(linux.version.as_deref(), Some("0.3.1"));
1449 assert_eq!(linux.update.as_deref(), Some("0.3.2 ready, installs after this job"));
1450 assert_eq!(windows.update.as_deref(), Some("0.3.2 ready, installs in a moment"));
1451 let line = busy.update.unwrap();
1452 assert_eq!((line.checked.as_str(), line.ready.as_deref(), line.checked_at.as_deref()), ("Checked 12 min ago", Some("0.3.2"), Some("2026-10-12T12:00:00Z")));
1453 // The cards say it, each for its own kind: nothing is said for the whole runner.
1454 assert_eq!(line.note, None);
1455 assert_eq!((line.check.id, line.check.label), ("check_updates", "Check for runner updates"));
1456 // Windows jobs stop while the program is replaced: installing, then nothing waits.
1457 let installing = Update { installing: vec!["windows".into()], ..update.clone() };
1458 let busy = diagnose(Code::Busy, &Facts { sandbox: Some(status.clone()), live: true, update: installing, ..facts() });
1459 assert_eq!(busy.capabilities[1].update.as_deref(), Some("Installing 0.3.2"));
1460 let done = Update { waiting: vec!["linux".into()], ..update.clone() };
1461 let busy = diagnose(Code::Busy, &Facts { sandbox: Some(status), live: true, update: done, ..facts() });
1462 assert_eq!(busy.capabilities[1].update, None);
1463 // In Docker or on this computer there are no cards: the whole runner says it.
1464 let docker = Update { waiting: vec!["runner".into()], ..update };
1465 let busy = diagnose(Code::Busy, &Facts { backend: "in Docker", job: Some("build (acme/web)".into()), update: docker.clone(), ..facts() });
1466 assert_eq!(busy.update.unwrap().note.as_deref(), Some("0.3.2 ready, installs after this job"));
1467 let idle = diagnose(Code::Idle, &Facts { backend: "in Docker", update: docker, ..facts() });
1468 assert_eq!(idle.update.unwrap().note.as_deref(), Some("0.3.2 ready, installs in a moment"));
1469 // What is said once g1t is closed knows nothing of updates.
1470 assert_eq!(closed(&facts()).update, None);
1471 }
1472
1473 #[test]
1474 fn a_release_not_signed_by_g1t_is_refused_and_said_so() {
1475 let detail = Some("the release's signature isn't g1t's".to_owned());
1476 let running = diagnose(Code::UpdateRefused, &Facts { detail: detail.clone(), update: Update { installed: Some("0.3.1".into()), ..Update::default() }, ..facts() });
1477 assert_eq!(running.title, "Update refused: not signed by g1t");
1478 assert_eq!(
1479 running.cause.as_deref(),
1480 Some("The newest g1t-runner release from g1t.sh isn't signed with g1t's release key (the release's signature isn't g1t's), so g1t didn't install it.")
1481 );
1482 assert!(running.fix.as_deref().unwrap().starts_with("The runner keeps running g1t-runner 0.3.1, and g1t checks again within the hour."));
1483 assert_eq!(running.actions, [CHECK_AGAIN, REPORT]);
1484 assert_eq!(running.tone, Tone::Warning);
1485 // With no runner yet, nothing runs until a signed one downloads.
1486 let nothing = diagnose(Code::UpdateRefused, &Facts { detail, ..facts() });
1487 assert_eq!(nothing.title, "Not running: the runner's release isn't signed by g1t");
1488 assert_eq!((nothing.tone, nothing.actions[0].id), (Tone::Problem, "retry"));
1489 assert!(!nothing.in_capabilities);
1490 }
1491
1492 #[test]
1493 fn the_title_bars_chip_says_the_runner_in_a_few_words() {
1494 let said = |code: Code, facts: &Facts| {
1495 let chip = chip(&diagnose(code, facts));
1496 (chip.tone, chip.label)
1497 };
1498 assert_eq!(said(Code::Idle, &facts()), (Tone::Ok, "Runner running".into()));
1499 assert_eq!(said(Code::Busy, &Facts { job: Some("build (acme/web)".into()), ..facts() }), (Tone::Working, "Running a job".into()));
1500 assert_eq!(said(Code::Starting, &facts()), (Tone::Working, "Runner starting".into()));
1501 assert_eq!(said(Code::Stopped, &facts()), (Tone::Off, "Runner stopped".into()));
1502 assert_eq!(said(Code::Off, &facts()), (Tone::Off, "Runner off".into()));
1503 assert_eq!(said(Code::NoWsl, &facts()), (Tone::Problem, "Runner needs you".into()));
1504 assert_eq!(said(Code::UpdateFailed, &Facts { detail: Some("x".into()), ..facts() }).1, "Runner running");
1505 // Both kinds of job busy at once.
1506 let both = status(
1507 r#"{"schema":1,"state":"running","capabilities":[
1508 {"id":"linux","available":true,"state":"running","turned_off":false,"job":{"kind":"workflow","id":"a","name":"build","repo":"acme/web"}},
1509 {"id":"windows","available":true,"state":"running","turned_off":false,"job":{"kind":"workflow","id":"b","name":"installer","repo":"acme/app"}}]}"#,
1510 );
1511 assert_eq!(said(Code::Busy, &Facts { sandbox: Some(both), live: true, ..facts() }), (Tone::Working, "Running 2 jobs".into()));
1512 // Linux jobs run while Windows jobs can't start: running, and wanting a look.
1513 let one_failing = status(NO_WSL_PRO);
1514 assert_eq!(said(Code::Idle, &Facts { sandbox: Some(one_failing), live: true, ..facts() }), (Tone::Warning, "Runner running".into()));
1515 }
1516
1517 #[test]
1518 fn a_diagnosis_without_the_sandbox_lists_no_capabilities() {
1519 let json = serde_json::to_value(diagnose(Code::Idle, &Facts { backend: "in Docker", ..facts() })).unwrap();
1520 assert!(json.get("capabilities").is_none());
1521 assert!(json.get("in_capabilities").is_none());
1522 let sandboxed = serde_json::to_value(diagnose(Code::Idle, &Facts { sandbox: Some(status(NO_WSL_PRO)), ..facts() })).unwrap();
1523 assert_eq!(sandboxed["capabilities"][1]["id"], "windows");
1524 assert_eq!(sandboxed["capabilities"][1]["state"], "idle");
1525 assert_eq!(sandboxed["in_capabilities"], true);
1526 // What affects everything is said once, above the cards.
1527 let signed_out = diagnose(Code::SignedOut, &Facts { sandbox: Some(status(NO_WSL_PRO)), ..facts() });
1528 assert!(!signed_out.in_capabilities);
1529 }
1530
1531 // --- What this computer lacks, by name (the launcher's `needs`) ------------------
1532
1533 /// A launcher's status with each kind of job's needs: `None` for a kind
1534 /// that is running, waiting for jobs.
1535 fn needing(linux: Option<&[&str]>, native: Option<(&str, Option<&[&str]>)>) -> Status {
1536 let kind = |id: &str, needs: Option<&[&str]>| match needs {
1537 None => format!(r#"{{"id":"{id}","available":true,"state":"running","runner_version":"0.3.5","turned_off":false,"needs":[]}}"#),
1538 // What the launcher says of macOS jobs, which need nothing: they aren't released.
1539 Some([]) => format!(r#"{{"id":"{id}","available":false,"state":"absent","turned_off":false,"cause":"Sandboxed macOS jobs are not released yet.","fix":"Run macOS jobs on a runner registered with --no-docker for now.","needs":[]}}"#),
1540 Some(needs) => format!(
1541 r#"{{"id":"{id}","available":false,"state":"{}","turned_off":false,"cause":"In the launcher's words.","fix":"Its fix.","needs":{}}}"#,
1542 if id == "linux" { "error" } else { "absent" },
1543 serde_json::to_string(needs).unwrap()
1544 ),
1545 };
1546 let mut kinds = vec![kind("linux", linux)];
1547 if let Some((id, needs)) = native {
1548 kinds.push(kind(id, needs));
1549 }
1550 let state = if linux.is_none() { "running" } else { "error" };
1551 status(&format!(r#"{{"schema":1,"state":"{state}","backend":"wsl2","launcher_version":"0.3.5","capabilities":[{}]}}"#, kinds.join(",")))
1552 }
1553
1554 /// What the whole runner and each card say, as the app would show it: the code `shown` picks.
1555 fn shown_for(status: Status, facts: Facts) -> Diagnosis {
1556 let code = if status.running() { Code::Idle } else if status.linux_needs().is_empty() { Code::SandboxFailed } else { Code::NeedsSetup };
1557 // As the app fills it in: the needs of each kind that is on.
1558 let setup = SetUp { needs: super::super::setup::needs(&status, |kind| !facts.off.iter().any(|off| off == kind)), ..facts.setup.clone() };
1559 diagnose(code, &Facts { sandbox: Some(status), live: true, setup, ..facts })
1560 }
1561
1562 fn ids(actions: &[Action]) -> Vec<&'static str> {
1563 actions.iter().map(|a| a.id).collect()
1564 }
1565
1566 /// Writes each scenario's diagnosis to `G1T_SCREENS_OUT`, when it is set, for the screens.
1567 fn screen(name: &str, diagnosis: &Diagnosis) {
1568 if let Some(dir) = std::env::var_os("G1T_SCREENS_OUT") {
1569 let dir = std::path::PathBuf::from(dir);
1570 std::fs::create_dir_all(&dir).unwrap();
1571 let shown = serde_json::json!({ "diagnosis": diagnosis, "chip": chip(diagnosis) });
1572 std::fs::write(dir.join(format!("{name}.json")), serde_json::to_vec_pretty(&shown).unwrap()).unwrap();
1573 }
1574 }
1575
1576 #[test]
1577 fn a_clean_windows_home_is_set_up_with_one_button_and_says_windows_jobs_plainly() {
1578 let home = shown_for(needing(Some(&["vm_platform", "wsl"]), Some(("windows", Some(&["windows_edition"])))), facts());
1579 screen("windows-home-clean", &home);
1580 assert_eq!(home.code, Code::NeedsSetup);
1581 assert_eq!(home.title, "Not running: WSL isn't installed");
1582 assert!(home.in_capabilities);
1583 let [linux, windows] = &home.capabilities[..] else { panic!() };
1584 assert_eq!((linux.title.as_str(), linux.tone, linux.state), ("Needs setting up", Tone::Warning, KindState::Unavailable));
1585 assert_eq!(linux.headline.as_deref(), Some("WSL isn't installed"));
1586 assert_eq!(linux.cause.as_deref(), Some("Linux jobs run in a small Linux VM of their own under WSL 2, which needs Virtual Machine Platform turned on and WSL installed."));
1587 assert_eq!(linux.fix.as_deref(), Some("Set up turns on Virtual Machine Platform and installs WSL in one go. Windows asks you to allow it, and to restart after."));
1588 assert_eq!(ids(&linux.actions), ["set_up", "setup_help"]);
1589 assert!(SET_UP.waits && SET_UP.confirm.is_none(), "Windows' own prompt asks");
1590 // Home: no dead end, and no button that can't work.
1591 assert_eq!((windows.title.as_str(), windows.tone), ("Not available", Tone::Off));
1592 assert_eq!(windows.headline.as_deref(), Some("Not in Windows Home"));
1593 assert_eq!(windows.fix.as_deref(), Some("Linux jobs run here all the same. Windows jobs need Windows 11 Pro, Enterprise or Education."));
1594 assert_eq!(ids(&windows.actions), ["windows_activation"]);
1595 // The chip asks for the person.
1596 assert_eq!(chip(&home).label, "Runner needs you");
1597 }
1598
1599 #[test]
1600 fn pro_sets_up_both_kinds_and_each_says_its_own() {
1601 let pro = shown_for(needing(Some(&["vm_platform", "wsl"]), Some(("windows", Some(&["windows_sandbox"])))), facts());
1602 screen("windows-pro-clean", &pro);
1603 let [linux, windows] = &pro.capabilities[..] else { panic!() };
1604 assert_eq!(ids(&linux.actions), ["set_up", "setup_help"]);
1605 assert_eq!(windows.headline.as_deref(), Some("Windows Sandbox is off"));
1606 // One press sets up both kinds, and each card says all it does.
1607 let both = "Set up turns on Virtual Machine Platform and Windows Sandbox and installs WSL in one go. Windows asks you to allow it, and to restart after.";
1608 assert_eq!((linux.fix.as_deref(), windows.fix.as_deref()), (Some(both), Some(both)));
1609 // Windows jobs stopped by the person: Set up leaves their feature alone, and says so.
1610 let linux_only = shown_for(
1611 needing(Some(&["vm_platform", "wsl"]), Some(("windows", Some(&["windows_sandbox"])))),
1612 Facts { setup: SetUp { needs: vec!["vm_platform".into(), "wsl".into()], ..SetUp::default() }, off: vec!["windows".into()], ..facts() },
1613 );
1614 assert_eq!(linux_only.capabilities[0].fix.as_deref(), Some("Set up turns on Virtual Machine Platform and installs WSL in one go. Windows asks you to allow it, and to restart after."));
1615 // Linux jobs run while Windows Sandbox is off: waiting for jobs, and one card wants a look.
1616 let linux_runs = shown_for(needing(None, Some(("windows", Some(&["windows_sandbox"])))), facts());
1617 screen("windows-pro-sandbox-off", &linux_runs);
1618 assert_eq!(linux_runs.title, "Waiting for jobs");
1619 assert_eq!(linux_runs.capabilities[0].title, "Waiting for jobs");
1620 assert_eq!(ids(&linux_runs.capabilities[1].actions), ["set_up", "setup_help"]);
1621 assert_eq!(chip(&linux_runs), Chip { tone: Tone::Warning, label: "Runner running".into() });
1622 // Windows jobs run while Linux jobs need WSL: each kind on its own.
1623 let windows_runs = shown_for(needing(Some(&["wsl"]), Some(("windows", None))), facts());
1624 screen("windows-pro-wsl-missing", &windows_runs);
1625 assert_eq!(windows_runs.code, Code::Idle);
1626 assert_eq!(chip(&windows_runs), Chip { tone: Tone::Warning, label: "Runner running".into() });
1627 assert_eq!(windows_runs.capabilities[1].title, "Waiting for jobs");
1628 assert_eq!(windows_runs.capabilities[0].fix.as_deref(), Some("Set up installs WSL in one go. Windows asks you to allow it."));
1629 // An older WSL is updated, not installed again.
1630 let old = shown_for(needing(Some(&["wsl_update"]), None), facts());
1631 assert_eq!(old.capabilities[0].headline.as_deref(), Some("WSL needs an update"));
1632 assert_eq!(old.capabilities[0].fix.as_deref(), Some("Set up updates WSL in one go. Windows asks you to allow it."));
1633 }
1634
1635 #[test]
1636 fn set_up_shows_while_it_works_then_a_restart_now_or_later() {
1637 let clean = needing(Some(&["vm_platform", "wsl"]), Some(("windows", Some(&["windows_sandbox"]))));
1638 let working = SetUp { running: Some("turns on Virtual Machine Platform and Windows Sandbox and installs WSL".into()), ..SetUp::default() };
1639 let setting_up = shown_for(clean.clone(), Facts { setup: working, ..facts() });
1640 screen("windows-setting-up", &setting_up);
1641 assert_eq!(setting_up.title, "Not running: setting up");
1642 assert_eq!(setting_up.tone, Tone::Working);
1643 assert_eq!(chip(&setting_up), Chip { tone: Tone::Working, label: "Runner setting up".into() });
1644 for kind in &setting_up.capabilities {
1645 assert_eq!((kind.title.as_str(), kind.state, kind.tone), ("Setting up", KindState::Starting, Tone::Working));
1646 assert!(kind.actions.is_empty());
1647 assert_eq!(kind.cause.as_deref(), Some("g1t turns on Virtual Machine Platform and Windows Sandbox and installs WSL. Windows asks you to allow it first, and it takes a few minutes."));
1648 }
1649 let restart = shown_for(needing(Some(&["restart"]), Some(("windows", Some(&["restart"])))), facts());
1650 screen("windows-restart", &restart);
1651 assert_eq!(restart.title, "Not running: restart to finish setting up");
1652 let linux = &restart.capabilities[0];
1653 assert_eq!((linux.title.as_str(), linux.tone), ("Restart needed", Tone::Warning));
1654 assert_eq!(ids(&linux.actions), ["restart_windows", "restart_later"]);
1655 // Windows' own dialog asks, not a page's, which a page could skip.
1656 assert!(RESTART_WINDOWS.confirm.is_none() && !RESTART_WINDOWS.waits);
1657 assert!(super::super::setup::RESTART_QUESTION.starts_with("Restart Windows now"));
1658 assert_eq!(linux.fix.as_deref(), Some("Restart now, or later: g1t opens again after the restart and carries on by itself."));
1659 let later = shown_for(needing(Some(&["restart"]), None), Facts { setup: SetUp { later: true, ..SetUp::default() }, ..facts() });
1660 screen("windows-restart-later", &later);
1661 assert_eq!(ids(&later.capabilities[0].actions), ["restart_windows"]);
1662 // Said No to Windows' prompt: nothing changed, and Set up again.
1663 let declined = SetUp { failed: Some("you said No to Windows' prompt, so nothing changed.".into()), ..SetUp::default() };
1664 let again = shown_for(clean, Facts { setup: declined, ..facts() });
1665 screen("windows-set-up-declined", &again);
1666 assert_eq!(again.capabilities[0].cause.as_deref(), Some("Set up didn't finish: you said No to Windows' prompt, so nothing changed."));
1667 assert_eq!(ids(&again.capabilities[0].actions), ["set_up", "view_log"]);
1668 }
1669
1670 #[test]
1671 fn what_only_the_person_can_do_is_said_step_by_step() {
1672 let firmware = shown_for(needing(Some(&["virtualization"]), Some(("windows", Some(&["virtualization"])))), facts());
1673 screen("windows-virtualization-off", &firmware);
1674 assert_eq!(firmware.title, "Not running: virtualization is off");
1675 for kind in &firmware.capabilities {
1676 assert_eq!(kind.headline.as_deref(), Some("Virtualization is off"));
1677 assert!(kind.fix.as_deref().is_some_and(|fix| fix.contains("BIOS or UEFI") && fix.contains("SVM Mode")));
1678 assert_eq!(ids(&kind.actions), ["firmware_help", "retry"]);
1679 }
1680 let nested = shown_for(needing(Some(&["nested_virtualization"]), Some(("windows", None))), facts());
1681 screen("windows-in-a-vm", &nested);
1682 assert_eq!(nested.capabilities[0].headline.as_deref(), Some("This VM can't run the sandbox"));
1683 assert!(nested.capabilities[0].fix.as_deref().is_some_and(|fix| fix.contains("ExposeVirtualizationExtensions")));
1684 let old = shown_for(needing(Some(&["windows_update"]), Some(("windows", Some(&["windows_update"])))), facts());
1685 screen("windows-too-old", &old);
1686 assert_eq!(ids(&old.capabilities[0].actions), ["windows_update", "retry"]);
1687 assert!(old.capabilities[0].cause.as_deref().is_some_and(|c| c.contains("version 2004")));
1688 assert!(old.capabilities[1].cause.as_deref().is_some_and(|c| c.contains("24H2")));
1689 }
1690
1691 #[test]
1692 fn linux_and_macos_say_the_command_that_fixes_it() {
1693 let linux = Facts { platform: "linux".into(), qemu_install: Some("sudo apt install qemu-system-x86".into()), ..facts() };
1694 let group = shown_for(needing(Some(&["kvm_group"]), None), linux.clone());
1695 screen("linux-kvm-group", &group);
1696 assert_eq!(group.title, "Not running: no access to KVM");
1697 assert_eq!(group.capabilities[0].fix.as_deref(), Some("Run `sudo usermod -aG kvm $USER` (Copy command), then log out and back in."));
1698 assert_eq!(ids(&group.capabilities[0].actions), ["copy_kvm_group", "retry"]);
1699 let bare = shown_for(needing(Some(&["kvm_module", "qemu"]), None), linux.clone());
1700 screen("linux-no-kvm", &bare);
1701 assert_eq!(bare.title, "Not running: KVM isn't loaded");
1702 assert_eq!(ids(&bare.capabilities[0].actions), ["copy_kvm_load", "retry"]);
1703 let qemu = shown_for(needing(Some(&["qemu"]), None), linux.clone());
1704 assert_eq!(qemu.capabilities[0].fix.as_deref(), Some("Install it with `sudo apt install qemu-system-x86` (Copy command), then Check now. Or install Sysbox 0.7 or later instead."));
1705 let unknown_distro = shown_for(needing(Some(&["qemu"]), None), Facts { qemu_install: None, ..linux.clone() });
1706 assert_eq!(ids(&unknown_distro.capabilities[0].actions), ["retry", "install_sysbox"]);
1707 let firmware = shown_for(needing(Some(&["virtualization"]), None), linux);
1708 assert!(firmware.capabilities[0].cause.as_deref().is_some_and(|c| c.contains("or has none")));
1709 let mac = Facts { platform: "macos".into(), ..facts() };
1710 let no_brew = shown_for(needing(Some(&["homebrew"]), Some(("macos", Some(&[])))), mac.clone());
1711 screen("macos-no-homebrew", &no_brew);
1712 assert_eq!(no_brew.title, "Not running: vfkit isn't installed");
1713 assert_eq!(ids(&no_brew.capabilities[0].actions), ["get_homebrew", "copy_vfkit_install"]);
1714 let old = shown_for(needing(Some(&["macos_update"]), None), mac);
1715 assert_eq!(ids(&old.capabilities[0].actions), ["software_update"]);
1716 }
1717
1718 #[test]
1719 fn every_need_has_a_way_forward() {
1720 let needs = [
1721 "restart", "windows_update", "virtualization", "nested_virtualization", "vm_platform", "wsl", "wsl_update", "windows_edition", "windows_sandbox", "kvm_module", "kvm_group", "qemu", "macos_update", "homebrew",
1722 "vfkit",
1723 ];
1724 for need in needs {
1725 for kind in ["linux", "windows"] {
1726 let words = need_words(&[need.to_owned()], kind, &facts()).unwrap_or_else(|| panic!("{need} has no words"));
1727 assert!(!words.actions.is_empty(), "{need} on {kind} is a dead end");
1728 assert!(words.fix.is_some_and(|fix| fix.ends_with('.')), "{need}");
1729 assert!(words.cause.ends_with('.'), "{need}");
1730 }
1731 }
1732 // A need from a newer launcher this app doesn't know: the launcher's own words.
1733 let newer = shown_for(needing(Some(&["something_new"]), None), facts());
1734 assert_eq!(newer.capabilities[0].cause.as_deref(), Some("In the launcher's words."));
1735 // From a launcher before needs: as before, by its words.
1736 let before = diagnose(Code::NoWsl, &Facts { sandbox: Some(status(NO_WSL_PRO)), ..facts() });
1737 assert_eq!(ids(&before.capabilities[0].actions), ["copy_wsl_install", "learn_more"]);
1738 assert_eq!(lowered("WSL isn't installed"), "WSL isn't installed");
1739 assert_eq!(lowered("No access to KVM"), "no access to KVM");
1740 }
1741}