Skip to content
169 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 runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.1//! How the runner runs on this computer, which is the person's choice in
2//! settings, and what each choice needs before it can start.
3//!
4//! | Backend | How | What a job can reach |
5//! | --- | --- | --- |
6//! | Sandboxed (the default) | The runner in a sandbox of its own: its image under Sysbox on Linux today (container.rs), a VM that `g1t-runner sandbox` makes on Windows and macOS once it is built (launcher.rs). Jobs are containers inside it: `nested-container` isolation. | Its own container. Not this computer. |
7//! | Docker | `g1t-runner` on this computer, driving this computer's Docker (native.rs): `container` isolation. | Its own container; but the runner controls this computer's Docker, which on Docker Desktop is a VM with the person's files shared into it. Not a sandbox. |
8//! | On this computer | `g1t-runner --no-docker` (native.rs): `host` isolation. | Everything the person's account can. |
9//!
10//! The two that are not sandboxes are only ever chosen; nothing falls back
11//! to them. When the chosen one cannot start, the app says why and runs
12//! nothing.
13
14use std::process::{Command, Stdio};
15
16use serde::{Deserialize, Serialize};
17
18use super::launcher;
19
20#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum Backend {
23 #[default]
24 Sandboxed,
25 Docker,
26 Host,
27}
28
29impl Backend {
30 /// How it is said in the tray and the log.
31 pub fn label(self) -> &'static str {
32 match self {
33 Backend::Sandboxed => "sandboxed",
34 Backend::Docker => "in Docker",
35 Backend::Host => "on this computer",
36 }
37 }
38}
39
40/// What starts the runner for a backend, here.
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub enum Engine {
43 /// The runner's image under Sysbox, through this computer's Docker.
44 Sysbox,
45 /// `g1t-runner sandbox`, which makes and owns the sandbox.
46 Launcher,
47 /// `g1t-runner` itself, as a child of the app; `docker` is whether its
48 /// jobs run in this computer's Docker or directly on it.
49 Native { docker: bool },
50}
51
52/// Why a backend cannot start here (diagnosis.rs says it).
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54pub enum Missing {
55 /// Docker is not installed, or not running.
56 Docker,
57 /// No sandbox can be made on this computer yet.
58 Sandbox,
59 /// Linux, whose sandbox needs Sysbox in its Docker, which has none.
60 Sysbox,
61}
62
63/// A program run without a console window flashing up on Windows.
64pub fn quiet(program: impl AsRef<std::ffi::OsStr>) -> Command {
65 let mut command = Command::new(program);
66 #[cfg(windows)]
67 {
68 use std::os::windows::process::CommandExt;
69 const CREATE_NO_WINDOW: u32 = 0x0800_0000;
70 command.creation_flags(CREATE_NO_WINDOW);
71 }
72 command.stdin(Stdio::null());
73 command
74}
75
76/// Whether this computer's Docker answers.
77pub fn docker_ready() -> bool {
78 quiet("docker")
79 .args(["version", "--format", "{{.Server.Version}}"])
80 .stdout(Stdio::null())
81 .stderr(Stdio::null())
82 .status()
83 .is_ok_and(|status| status.success())
84}
85
86/// Whether this computer's Docker has Sysbox's runtime.
87fn sysbox_ready() -> bool {
88 quiet("docker")
89 .args(["info", "--format", "{{json .Runtimes}}"])
90 .stderr(Stdio::null())
91 .output()
92 .is_ok_and(|out| out.status.success() && String::from_utf8_lossy(&out.stdout).contains("sysbox-runc"))
93}
94
95/// What a backend needs, checked now: how to start it, or why it cannot.
96pub fn engine(backend: Backend) -> Result<Engine, Missing> {
97 match backend {
98 Backend::Sandboxed => {
99 if launcher::READY {
100 return Ok(Engine::Launcher);
101 }
102 if !cfg!(target_os = "linux") {
103 return Err(Missing::Sandbox);
104 }
105 if !docker_ready() {
106 return Err(Missing::Docker);
107 }
108 if !sysbox_ready() {
109 return Err(Missing::Sysbox);
110 }
111 Ok(Engine::Sysbox)
112 }
113 Backend::Docker => {
114 if docker_ready() {
115 Ok(Engine::Native { docker: true })
116 } else {
117 Err(Missing::Docker)
118 }
119 }
120 Backend::Host => Ok(Engine::Native { docker: false }),
121 }
122}
123
124/// This computer's name, as the runner is called on g1t: what `g1t-runner`
125/// would choose itself, given here so a runner in a container, whose own
126/// host name is the container's, has the computer's.
127pub fn machine_name() -> String {
128 let given = ["COMPUTERNAME", "HOSTNAME"]
129 .iter()
130 .find_map(|name| std::env::var(name).ok().map(|v| v.trim().to_owned()).filter(|v| !v.is_empty()))
131 .or_else(|| {
132 quiet("hostname")
133 .output()
134 .ok()
135 .map(|out| String::from_utf8_lossy(&out.stdout).trim().to_owned())
136 .filter(|name| !name.is_empty())
137 })
138 .unwrap_or_else(|| "desktop".into());
139 let name: String = given
140 .chars()
141 .map(|c| if c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.') { c } else { '-' })
142 .take(64)
143 .collect();
144 if name.is_empty() { "desktop".into() } else { name }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn the_sandbox_is_the_default_and_names_read() {
153 assert_eq!(Backend::default(), Backend::Sandboxed);
154 assert_eq!(serde_json::to_string(&Backend::Host).unwrap(), r#""host""#);
155 assert_eq!(serde_json::from_str::<Backend>(r#""docker""#).unwrap(), Backend::Docker);
156 }
157
158 #[test]
159 fn on_this_computer_needs_nothing_more() {
160 assert_eq!(engine(Backend::Host), Ok(Engine::Native { docker: false }));
161 }
162
163 #[test]
164 fn a_machine_name_is_one_g1t_takes() {
165 let name = machine_name();
166 assert!(!name.is_empty() && name.len() <= 64);
167 assert!(name.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.')), "{name}");
168 }
169}

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