Skip to content
140 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) | `g1t-runner sandbox` (launcher.rs): Linux jobs in a Linux VM of the runner's own (WSL 2 on Windows, vfkit on macOS, Sysbox or QEMU with KVM on Linux), each a container inside it, `nested-container`; on Windows 11 Pro and up, Windows jobs each in a Windows Sandbox, `vm`. What the computer lacks for it, the launcher says, and the app shows. | Its own container, or its own Windows Sandbox. 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
18#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "snake_case")]
20pub enum Backend {
21 #[default]
22 Sandboxed,
23 Docker,
24 Host,
25}
26
27impl Backend {
28 /// How it is said in the tray and the log.
29 pub fn label(self) -> &'static str {
30 match self {
31 Backend::Sandboxed => "sandboxed",
32 Backend::Docker => "in Docker",
33 Backend::Host => "on this computer",
34 }
35 }
36}
37
38/// What starts the runner for a backend, here.
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub enum Engine {
41 /// `g1t-runner sandbox`, which makes and owns the sandbox, and says
42 /// itself what this computer lacks for one.
43 Launcher,
44 /// `g1t-runner` itself, as a child of the app; `docker` is whether its
45 /// jobs run in this computer's Docker or directly on it.
46 Native { docker: bool },
47}
48
49/// Why a backend cannot start here (diagnosis.rs says it).
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub enum Missing {
52 /// Docker is not installed, or not running.
53 Docker,
54}
55
56/// A program run without a console window flashing up on Windows.
57pub fn quiet(program: impl AsRef<std::ffi::OsStr>) -> Command {
58 let mut command = Command::new(program);
59 #[cfg(windows)]
60 {
61 use std::os::windows::process::CommandExt;
62 const CREATE_NO_WINDOW: u32 = 0x0800_0000;
63 command.creation_flags(CREATE_NO_WINDOW);
64 }
65 command.stdin(Stdio::null());
66 command
67}
68
69/// Whether this computer's Docker answers.
70pub fn docker_ready() -> bool {
71 quiet("docker")
72 .args(["version", "--format", "{{.Server.Version}}"])
73 .stdout(Stdio::null())
74 .stderr(Stdio::null())
75 .status()
76 .is_ok_and(|status| status.success())
77}
78
79/// What a backend needs, checked now: how to start it, or why it cannot.
80pub fn engine(backend: Backend) -> Result<Engine, Missing> {
81 match backend {
82 Backend::Sandboxed => Ok(Engine::Launcher),
83 Backend::Docker => {
84 if docker_ready() {
85 Ok(Engine::Native { docker: true })
86 } else {
87 Err(Missing::Docker)
88 }
89 }
90 Backend::Host => Ok(Engine::Native { docker: false }),
91 }
92}
93
94/// This computer's name, as the runner is called on g1t: what `g1t-runner`
95/// would choose itself, given here so a runner in a container, whose own
96/// host name is the container's, has the computer's.
97pub fn machine_name() -> String {
98 let given = ["COMPUTERNAME", "HOSTNAME"]
99 .iter()
100 .find_map(|name| std::env::var(name).ok().map(|v| v.trim().to_owned()).filter(|v| !v.is_empty()))
101 .or_else(|| {
102 quiet("hostname")
103 .output()
104 .ok()
105 .map(|out| String::from_utf8_lossy(&out.stdout).trim().to_owned())
106 .filter(|name| !name.is_empty())
107 })
108 .unwrap_or_else(|| "desktop".into());
109 let name: String = given
110 .chars()
111 .map(|c| if c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.') { c } else { '-' })
112 .take(64)
113 .collect();
114 if name.is_empty() { "desktop".into() } else { name }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn the_sandbox_is_the_default_and_names_read() {
123 assert_eq!(Backend::default(), Backend::Sandboxed);
124 assert_eq!(serde_json::to_string(&Backend::Host).unwrap(), r#""host""#);
125 assert_eq!(serde_json::from_str::<Backend>(r#""docker""#).unwrap(), Backend::Docker);
126 }
127
128 #[test]
129 fn on_this_computer_needs_nothing_more_and_the_sandbox_is_the_launchers() {
130 assert_eq!(engine(Backend::Host), Ok(Engine::Native { docker: false }));
131 assert_eq!(engine(Backend::Sandboxed), Ok(Engine::Launcher));
132 }
133
134 #[test]
135 fn a_machine_name_is_one_g1t_takes() {
136 let name = machine_name();
137 assert!(!name.is_empty() && name.len() <= 64);
138 assert!(name.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.')), "{name}");
139 }
140}