| 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 | |
| 14 | use std::process::{Command, Stdio}; |
| 15 | |
| 16 | use serde::{Deserialize, Serialize}; |
| 17 | |
| 18 | use super::launcher; |
| 19 | |
| 20 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] |
| 21 | #[serde(rename_all = "snake_case")] |
| 22 | pub enum Backend { |
| 23 | #[default] |
| 24 | Sandboxed, |
| 25 | Docker, |
| 26 | Host, |
| 27 | } |
| 28 | |
| 29 | impl 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)] |
| 42 | pub 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)] |
| 54 | pub 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. |
| 64 | pub 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. |
| 77 | pub 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. |
| 87 | fn 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. |
| 96 | pub 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. |
| 127 | pub 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)] |
| 148 | mod 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 | } |