Skip to content
169 linesCodeBlameRaw
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}