Skip to content
557 linesCodeBlameRaw

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

Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.1//! The one place the app meets `g1t-runner sandbox`, the launcher that
2//! makes a sandbox of the runner's own on this computer and runs the
3//! runner inside it (crates/runner/src/selfhosted/launcher): Linux jobs in
4//! a Linux VM (a WSL 2 distribution on Windows, vfkit on macOS, Sysbox or
5//! QEMU with KVM on Linux), each a fresh container inside it
6//! (`nested-container`), and, on Windows 11 Pro and up, Windows jobs each
7//! in a Windows Sandbox of its own (`vm`).
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.8//!
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.9//! It is the `g1t-runner` the app already keeps up to date (native.rs),
10//! with its state in the app's runner folder (`--state-dir`). The app runs
11//! four commands, each with `--json`, and reads the one object each prints
12//! last (the launcher's documented status, schema 1):
13//!
14//! - `up --url … --name … --labels desktop [--token …]`, after the window
15//! opens: makes the sandbox if there is none and starts it, waiting until
16//! the runner asks for work. The token goes only to the first `up`; the
17//! launcher keeps the registration from then on, across updates too. It
18//! adds `-linux` and `-windows` to the name, and the OS labels, itself.
19//! - `status`, every few seconds, for the tray, settings and the site.
20//! - `down` as g1t quits or the person stops it; `down --drain` when they
21//! choose another way of running jobs; `down --delete` when the
22//! registration is no longer the one wanted.
23//! - `update`, while nothing runs: the newest signed image, the same
24//! registration.
25//!
26//! What a status's causes and errors mean is read here (`why`); what the
27//! app says about them is diagnosis.rs's.
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.28
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.29use std::ffi::OsString;
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.30use std::path::Path;
31use std::process::{Command, Stdio};
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.32use std::time::Duration;
33
34use serde::Deserialize;
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.35
36use super::backend::quiet;
37
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.38/// The launcher's state folder, under the app's runner folder.
39pub const FOLDER: &str = "sandbox";
40/// The status shape the app reads (`schema`).
41pub const SCHEMA: u32 = 1;
42/// How often the app asks the sandbox how it is: a call takes about a second.
43pub const STATUS_EVERY: Duration = Duration::from_secs(8);
44/// What installs WSL on Windows, for the person to run as administrator.
45pub const WSL_INSTALL: &str = "wsl --install --no-distribution";
46/// What installs vfkit on a Mac.
47pub const VFKIT_INSTALL: &str = "brew install vfkit";
48
49/// A sandbox's, or one capability's, state.
50#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
51#[serde(rename_all = "snake_case")]
52pub enum State {
53 /// Never made, or deleted.
54 #[default]
55 Absent,
56 Stopped,
57 Starting,
58 Running,
59 Updating,
60 Error,
61 /// A state a newer launcher has that this app does not know.
62 #[serde(other)]
63 Unknown,
64}
65
66/// What a runner is running now.
67#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
68#[serde(default)]
69pub struct Job {
70 /// `workflow` or `agent`.
71 pub kind: String,
72 pub id: String,
73 pub name: String,
74 pub repo: String,
75}
76
77impl Job {
78 /// How the tray says it: `build (acme/web)`.
79 pub fn said(&self) -> String {
80 if self.repo.is_empty() { self.name.clone() } else { format!("{} ({})", self.name, self.repo) }
81 }
82}
83
84/// One kind of job the computer can run sandboxed: `linux`, `windows` or `macos`.
85#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
86#[serde(default)]
87pub struct Capability {
88 pub id: String,
89 pub available: bool,
90 pub state: State,
91 pub isolation: Option<String>,
92 pub runner_version: Option<String>,
93 pub job: Option<Job>,
94 /// Why it is not available, in a sentence.
95 pub cause: Option<String>,
96 /// What to do about it, in a sentence.
97 pub fix: Option<String>,
98}
99
100/// `g1t-runner sandbox status --json`. The top-level fields are the Linux
101/// VM's. Fields the app does not use (`resources`, `state_dir`) are left
102/// out, and any a newer launcher adds are ignored.
103#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
104#[serde(default)]
105pub struct Status {
106 pub schema: u32,
107 pub state: State,
108 /// `wsl2`, `vfkit`, `qemu` or `sysbox`.
109 pub backend: Option<String>,
110 pub runner_version: Option<String>,
111 pub launcher_version: Option<String>,
112 pub image_version: Option<String>,
113 pub isolation: Option<String>,
114 pub job: Option<Job>,
115 /// A sentence for a person, whenever something is wrong.
116 pub error: Option<String>,
117 pub capabilities: Vec<Capability>,
118}
119
120impl Status {
121 pub fn capability(&self, id: &str) -> Option<&Capability> {
122 self.capabilities.iter().find(|c| c.id == id)
123 }
124
125 /// The Linux capability, as the top-level fields say it when a launcher lists none.
126 pub fn linux(&self) -> Capability {
127 self.capability("linux").cloned().unwrap_or_else(|| Capability {
128 id: "linux".into(),
129 available: self.state != State::Error || self.error.is_none(),
130 state: self.state,
131 runner_version: self.runner_version.clone(),
132 job: self.job.clone(),
133 ..Capability::default()
134 })
135 }
136
137 /// Whether any of its runners is running.
138 pub fn running(&self) -> bool {
139 self.state == State::Running || self.capabilities.iter().any(|c| c.state == State::Running)
140 }
141
142 /// Whether anything here can be sandboxed at all.
143 pub fn anything_available(&self) -> bool {
144 if self.capabilities.is_empty() { self.linux().available } else { self.capabilities.iter().any(|c| c.available) }
145 }
146
147 /// A capability that can be had here but was never set up: registering
148 /// it needs a token.
149 pub fn unregistered(&self) -> bool {
150 let wanting = |c: &Capability| c.available && c.state == State::Absent;
151 if self.capabilities.is_empty() { wanting(&self.linux()) } else { self.capabilities.iter().any(wanting) }
152 }
153
154 /// What a runner is running, the Linux one's first.
155 pub fn job(&self) -> Option<&Job> {
156 self.job.as_ref().or_else(|| self.capabilities.iter().find_map(|c| c.job.as_ref()))
157 }
158
159 /// The runner's version, as the tray and the site say it.
160 pub fn version(&self) -> Option<String> {
161 self.runner_version
162 .clone()
163 .or_else(|| self.capabilities.iter().find_map(|c| c.runner_version.clone()))
164 .or_else(|| self.launcher_version.clone())
165 }
166
167 /// What is wrong with Linux jobs, in the launcher's words: its error,
168 /// else why the capability is unavailable.
169 pub fn trouble(&self) -> Option<(String, Option<String>)> {
170 let linux = self.linux();
171 if !linux.available {
172 let cause = linux.cause.or_else(|| self.error.clone())?;
173 return Some((cause, linux.fix));
174 }
175 if linux.state == State::Error || self.state == State::Error {
176 return Some((self.error.clone().unwrap_or_else(|| "The sandbox stopped, and didn't say why.".into()), None));
177 }
178 None
179 }
180}
181
182/// Reads what a launcher command printed: log lines, then the status as
183/// one JSON object, which starts on a line of its own. A launcher whose
184/// schema is not this app's is not read, nor is a `g1t-runner` from before
185/// the launcher, which prints no status at all.
186pub fn parse(output: &str) -> Result<Status, String> {
187 let start = output
188 .match_indices('\n')
189 .map(|(at, _)| at + 1)
190 .chain(std::iter::once(0))
191 .filter(|at| output[*at..].starts_with('{'))
192 .max()
193 .ok_or_else(|| "this g1t-runner printed no sandbox status: it is older than its sandbox launcher".to_owned())?;
194 let status: Status = serde_json::from_str(output[start..].trim()).map_err(|e| format!("the sandbox's status doesn't read: {e}"))?;
195 if status.schema != SCHEMA {
196 return Err(format!("the sandbox's status is in schema {}, and this g1t reads schema {SCHEMA}: update g1t", status.schema));
197 }
198 Ok(status)
199}
200
201/// Whether a line of a launcher's output starts its status, rather than being a log line.
202pub fn starts_status(line: &str) -> bool {
203 line.starts_with('{')
204}
205
206/// What `up` is told the first time, and again on every start (the
207/// launcher keeps what it was told, and `--token` only matters once).
208#[derive(Clone, PartialEq, Eq)]
209pub struct Up<'a> {
210 pub url: &'a str,
211 pub name: &'a str,
212 pub labels: &'a str,
213 /// The registration token: only for a sandbox that has no registration yet.
214 pub token: Option<&'a str>,
215}
216
217// Never print a token, in a log or a panic.
218impl std::fmt::Debug for Up<'_> {
219 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220 f.debug_struct("Up").field("url", &self.url).field("name", &self.name).field("labels", &self.labels).field("token", &self.token.map(|_| "…")).finish()
221 }
222}
223
224fn args(action: &str, dir: &Path) -> Vec<OsString> {
225 let mut args: Vec<OsString> = vec!["sandbox".into(), action.into(), "--json".into(), "--state-dir".into()];
226 args.push(dir.as_os_str().to_owned());
227 args
228}
229
A registration token never shows in the process list. The desktop app passed it to g1t-runner sandbox up as --token, which anyone on the computer can read while up runs; it goes in up's environment now, as G1T_RUNNER_TOKEN, which the launcher reads when --token is left out, as register-and-run already did.230/// `sandbox up`'s arguments. The token is never one of them: anyone on the
231/// computer can read a process's command line while it runs, so it goes in
232/// `up`'s environment instead ([`with_token`]).
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.233pub fn up_args(dir: &Path, up: &Up) -> Vec<OsString> {
234 let mut args = args("up", dir);
235 for (name, value) in [("--url", up.url), ("--name", up.name), ("--labels", up.labels)] {
236 args.push(name.into());
237 args.push(value.into());
238 }
A registration token never shows in the process list. The desktop app passed it to g1t-runner sandbox up as --token, which anyone on the computer can read while up runs; it goes in up's environment now, as G1T_RUNNER_TOKEN, which the launcher reads when --token is left out, as register-and-run already did.239 args
240}
241
242/// Gives a launcher command the registration token, as `G1T_RUNNER_TOKEN`
243/// in its environment and nowhere else.
244pub fn with_token(command: &mut Command, token: Option<&str>) {
245 if let Some(token) = token {
246 command.env("G1T_RUNNER_TOKEN", token);
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.247 }
248}
249
250/// How `down` stops the sandbox.
251#[derive(Clone, Copy, Debug, PartialEq, Eq)]
252pub enum Down {
253 /// Now: a running job is cut short (as g1t quits, or Stop).
254 Now,
255 /// After the running job (another way of running jobs was chosen).
256 Drain,
257 /// Now, and the VM and the registration with it (another workspace or server).
258 Delete,
259}
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.260
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.261pub fn down_args(dir: &Path, how: Down) -> Vec<OsString> {
262 let mut args = args("down", dir);
263 match how {
264 Down::Now => {}
265 Down::Drain => args.push("--drain".into()),
266 Down::Delete => args.push("--delete".into()),
267 }
268 args
269}
270
271pub fn status_args(dir: &Path) -> Vec<OsString> {
272 args("status", dir)
273}
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.274
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.275pub fn update_args(dir: &Path) -> Vec<OsString> {
276 args("update", dir)
277}
278
279/// A launcher command whose output the app reads a line at a time
280/// (`up` and `update`, which take minutes the first time).
281pub fn command(binary: &Path, args: Vec<OsString>) -> Command {
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.282 let mut command = quiet(binary);
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.283 // What the app's own environment may have set for another runner is not this one's.
284 for (name, _) in std::env::vars_os() {
285 if name.to_string_lossy().starts_with("G1T_RUNNER_") {
286 command.env_remove(&name);
287 }
288 }
289 command.args(args).stdout(Stdio::piped()).stderr(Stdio::piped());
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.290 command
291}
292
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.293/// Runs a short launcher command to its end and reads its status, which
294/// it prints whether it worked or not.
295fn run(binary: &Path, args: Vec<OsString>) -> Result<Status, String> {
296 let out = command(binary, args).output().map_err(|e| format!("g1t-runner could not start: {e}"))?;
297 let printed = String::from_utf8_lossy(&out.stdout);
298 match parse(&printed) {
299 Ok(status) => Ok(status),
300 Err(_) if !out.status.success() && !out.stderr.is_empty() => Err(String::from_utf8_lossy(&out.stderr).trim().trim_start_matches("g1t-runner: ").to_owned()),
301 Err(why) => Err(why),
302 }
303}
304
305/// How the sandbox is now.
306pub fn status(binary: &Path, dir: &Path) -> Result<Status, String> {
307 run(binary, status_args(dir))
308}
309
310/// Stops the sandbox, waiting for it.
311pub fn down(binary: &Path, dir: &Path, how: Down) -> Result<(), String> {
312 if !binary.exists() {
313 return Err("g1t-runner isn't installed".into());
314 }
315 let status = run(binary, down_args(dir, how))?;
316 match status.error {
317 Some(error) => Err(error),
318 None => Ok(()),
319 }
320}
321
322/// What a launcher's sentence is about: the words the app shows for it
323/// are diagnosis.rs's. Read from the causes and errors the launcher
324/// writes (crates/runner/src/selfhosted/launcher and native.rs).
325#[derive(Clone, Copy, Debug, PartialEq, Eq)]
326pub enum Why {
327 NoWsl,
328 WslMirrored,
329 NoVirtualization,
330 NoVfkit,
331 /// Linux, with neither Sysbox in Docker nor KVM.
332 NoSysboxOrKvm,
333 /// The sandbox's image couldn't be downloaded, or isn't the release's.
334 Image,
335 /// g1t refused the registration token.
336 TokenRefused,
337 /// g1t no longer knows the runner.
338 Forgotten,
339 /// The sandbox has no registration, and no token was given.
340 Unregistered,
341 /// Windows Sandbox is off, or this edition of Windows has none.
342 WindowsSandboxOff,
343 /// Windows Sandbox without `wsb`, from before Windows 11 24H2.
344 NoWsb,
345 /// Sandboxed macOS jobs are not released.
346 MacNotReleased,
347 /// Anything else: shown in the launcher's own words.
348 Other,
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.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.351pub fn why(text: &str) -> Why {
352 let lower = text.to_ascii_lowercase();
353 let has = |words: &str| lower.contains(words);
354 if has("wsl is not installed") {
355 Why::NoWsl
356 } else if has("mirrored") {
357 Why::WslMirrored
358 } else if has("virtualization is not enabled")
359 || has("enable virtualization")
360 || has("virtual machine platform")
361 || has("hcs_e_hyperv_not_installed")
362 || has("vt-x")
363 || has("hypervisor is not present")
364 {
365 Why::NoVirtualization
366 } else if has("vfkit") && has("not installed") {
367 Why::NoVfkit
368 } else if has("neither sysbox nor kvm") || has("docker has no sysbox") {
369 Why::NoSysboxOrKvm
370 } else if has("token") && (has("not valid") || has("expired") || has("invalid")) {
371 Why::TokenRefused
372 } else if has("no longer knows this runner") || has("was removed from g1t") {
373 Why::Forgotten
374 } else if has("is not registered") {
375 Why::Unregistered
376 } else if has("sandbox image") || has("sandbox's images") || has("no sandbox images") || has("sha-256") || has("signature") || has("release key") || has("could not fetch") || has("broke off") {
377 Why::Image
378 } else if has("windows sandbox is not turned on") {
379 Why::WindowsSandboxOff
380 } else if has("24h2") {
381 Why::NoWsb
382 } else if has("macos jobs are not released") {
383 Why::MacNotReleased
384 } else {
385 Why::Other
386 }
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.387}
388
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.389#[cfg(test)]
390mod tests {
391 use super::*;
392 use std::path::PathBuf;
393
394 /// What `status --json` printed on a Windows 11 Pro machine with WSL
395 /// and Windows Sandbox, before `up` (g1t-runner 0.2.1).
396 const ABSENT: &str = r#"{
397 "schema": 1,
398 "state": "absent",
399 "backend": "wsl2",
400 "runner_version": null,
401 "launcher_version": "0.2.1",
402 "image_version": null,
403 "isolation": "nested-container",
404 "job": null,
405 "error": null,
406 "resources": {
407 "cpus": 0,
408 "memory_mib": 0,
409 "disk_gib": 0
410 },
411 "capabilities": [
412 {
413 "id": "linux",
414 "available": true,
415 "state": "absent",
416 "isolation": "nested-container",
417 "runner_version": null,
418 "job": null,
419 "cause": null,
420 "fix": null
421 },
422 {
423 "id": "windows",
424 "available": true,
425 "state": "absent",
426 "isolation": "vm",
427 "runner_version": null,
428 "job": null,
429 "cause": null,
430 "fix": null
431 }
432 ],
433 "state_dir": "C:\\Users\\syntaqx\\.g1t-runner\\sandbox"
434}"#;
435
436 /// The launcher's documented example: running a job, Windows Sandbox off.
437 const RUNNING: &str = r#"{
438 "schema": 1, "state": "running", "backend": "wsl2",
439 "runner_version": "0.3.0", "launcher_version": "0.3.0", "image_version": "0.3.0",
440 "isolation": "nested-container",
441 "job": { "kind": "workflow", "id": "job_1", "name": "build", "repo": "acme/web" },
442 "error": null,
443 "resources": { "cpus": 4, "memory_mib": 8192, "disk_gib": 64 },
444 "capabilities": [
445 { "id": "linux", "available": true, "state": "running", "isolation": "nested-container",
446 "runner_version": "0.3.0", "job": { "kind": "workflow", "id": "job_1", "name": "build", "repo": "acme/web" }, "cause": null, "fix": null },
447 { "id": "windows", "available": false, "state": "absent", "isolation": "vm",
448 "runner_version": null, "job": null,
449 "cause": "Windows Sandbox is not turned on, or this edition of Windows has none (Home does not).",
450 "fix": "On Windows 11 Pro, Enterprise or Education, turn on Windows Sandbox under Settings, System, Optional features, More Windows features, then restart." }
451 ],
452 "state_dir": "C:\\Users\\a\\.g1t-runner\\sandbox",
453 "something_new": { "a": 1 }
454}"#;
455
456 #[test]
457 fn a_status_is_read_from_after_the_log() {
458 let printed = format!("2026-10-12T08:00:00Z Downloading the sandbox image wsl-x64.\n2026-10-12T08:03:00Z {{not json}}\n{RUNNING}\n");
459 let status = parse(&printed).unwrap();
460 assert_eq!(status.state, State::Running);
461 assert_eq!(status.backend.as_deref(), Some("wsl2"));
462 assert_eq!(status.job().map(Job::said).as_deref(), Some("build (acme/web)"));
463 assert_eq!(status.version().as_deref(), Some("0.3.0"));
464 assert!(status.running());
465 assert!(!status.unregistered());
466 let windows = status.capability("windows").unwrap();
467 assert!(!windows.available);
468 assert_eq!(why(windows.cause.as_deref().unwrap()), Why::WindowsSandboxOff);
469 assert_eq!(status.trouble(), None);
470 }
471
472 #[test]
473 fn a_sandbox_never_made_needs_registering() {
474 let status = parse(ABSENT).unwrap();
475 assert_eq!(status.state, State::Absent);
476 assert!(status.anything_available());
477 assert!(status.unregistered());
478 assert!(!status.running());
479 assert_eq!(status.version().as_deref(), Some("0.2.1"));
480 }
481
482 #[test]
483 fn what_is_not_a_status_is_said_so() {
484 assert!(parse("g1t-runner: unknown command sandbox\n").unwrap_err().contains("older than its sandbox launcher"));
485 assert!(parse(&ABSENT.replace("\"schema\": 1", "\"schema\": 2")).unwrap_err().contains("schema 2"));
486 let unknown = parse(&ABSENT.replace("\"state\": \"absent\",\n \"backend\"", "\"state\": \"hibernating\",\n \"backend\"")).unwrap();
487 assert_eq!(unknown.state, State::Unknown);
488 assert!(starts_status("{") && !starts_status(" \"schema\": 1,"));
489 }
490
491 #[test]
492 fn without_wsl_linux_says_why_and_nothing_else_can_run_on_home() {
493 let status = parse(
494 r#"{"schema":1,"state":"error","backend":"wsl2","launcher_version":"0.2.1",
495 "error":"WSL is not installed, and the sandbox for Linux jobs is a WSL distribution of its own.",
496 "capabilities":[
497 {"id":"linux","available":false,"state":"error",
498 "cause":"WSL is not installed, and the sandbox for Linux jobs is a WSL distribution of its own.",
499 "fix":"Open a terminal as administrator and run `wsl --install --no-distribution`, then restart Windows."},
500 {"id":"windows","available":false,"state":"absent","cause":"Windows Sandbox is not turned on, or this edition of Windows has none (Home does not).","fix":"…"}]}"#,
501 )
502 .unwrap();
503 assert!(!status.anything_available());
504 let (cause, fix) = status.trouble().unwrap();
505 assert_eq!(why(&cause), Why::NoWsl);
506 assert!(fix.unwrap().contains(WSL_INSTALL));
507 }
508
509 #[test]
510 fn the_launchers_sentences_are_told_apart() {
511 for (text, expected) in [
512 ("WSL is set to mirrored networking, in which Linux reaches this computer's own localhost.", Why::WslMirrored),
513 ("WSL could not import the sandbox: Please enable the Virtual Machine Platform Windows feature and ensure virtualization is enabled in the BIOS.", Why::NoVirtualization),
514 ("vfkit, which runs the sandbox's VM with Apple's Virtualization framework, is not installed.", Why::NoVfkit),
515 ("This machine has neither Sysbox nor KVM, so there is nothing to sandbox the runner with.", Why::NoSysboxOrKvm),
516 ("the sandbox image wsl-x64.tar.gz's SHA-256 is not the release's; not using it", Why::Image),
517 ("could not fetch https://g1t.sh/downloads/runner/0.3.0/wsl-x64.tar.gz: dns error", Why::Image),
518 ("release 0.3.0 has no sandbox images", Why::Image),
519 ("The registration token is not valid, or has expired.", Why::TokenRefused),
520 ("g1t no longer knows this runner (401 unauthorized). Register it again.", Why::Forgotten),
521 ("the sandbox's runner is not registered, and no --url and --token were given", Why::Unregistered),
522 ("This Windows has Windows Sandbox but not its command line, wsb, which came in Windows 11 24H2.", Why::NoWsb),
523 ("Sandboxed macOS jobs are not released yet.", Why::MacNotReleased),
524 ("This user may not use /dev/kvm.", Why::Other),
525 ] {
526 assert_eq!(why(text), expected, "{text}");
527 }
528 }
529
530 #[test]
531 fn the_commands_are_the_launchers() {
532 let dir = PathBuf::from("data").join("runner").join(FOLDER);
533 let text = |args: Vec<OsString>| args.iter().map(|a| a.to_string_lossy().into_owned()).collect::<Vec<_>>();
534 let first = Up { url: "https://g1t.sh", name: "build-box", labels: "desktop", token: Some("g1trt_secret") };
535 let up = text(up_args(&dir, &first));
536 assert_eq!(&up[..4], ["sandbox", "up", "--json", "--state-dir"]);
537 assert_eq!(up[4], dir.to_string_lossy());
A registration token never shows in the process list. The desktop app passed it to g1t-runner sandbox up as --token, which anyone on the computer can read while up runs; it goes in up's environment now, as G1T_RUNNER_TOKEN, which the launcher reads when --token is left out, as register-and-run already did.538 // Never the token: it goes in the environment, out of the process list.
539 assert_eq!(&up[5..], ["--url", "https://g1t.sh", "--name", "build-box", "--labels", "desktop"]);
540 assert!(!up.iter().any(|a| a == "--token" || a.starts_with("g1trt_")));
541 let mut command = command(Path::new("g1t-runner"), up_args(&dir, &first));
542 with_token(&mut command, first.token);
543 assert!(command.get_envs().any(|(name, value)| name == "G1T_RUNNER_TOKEN" && value == Some(std::ffi::OsStr::new("g1trt_secret"))));
544 assert!(!command.get_args().any(|a| a.to_string_lossy().contains("g1trt_")));
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.545 // Its name and labels as the app has them: the launcher adds `-linux` and the OS labels.
546 assert!(!up.iter().any(|a| a.ends_with("-linux") || a.contains("windows")));
547 let again = text(up_args(&dir, &Up { token: None, ..first.clone() }));
548 assert!(!again.iter().any(|a| a == "--token" || a.starts_with("g1trt_")));
549 assert!(!format!("{first:?}").contains("g1trt_secret"));
550 assert_eq!(text(down_args(&dir, Down::Now))[..3], ["sandbox", "down", "--json"]);
551 assert_eq!(text(down_args(&dir, Down::Now)).len(), 5);
552 assert_eq!(text(down_args(&dir, Down::Drain)).last().unwrap(), "--drain");
553 assert_eq!(text(down_args(&dir, Down::Delete)).last().unwrap(), "--delete");
554 assert_eq!(text(status_args(&dir))[..3], ["sandbox", "status", "--json"]);
555 assert_eq!(text(update_args(&dir))[..3], ["sandbox", "update", "--json"]);
556 }
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.557}

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