| 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 | |
| 14 | use std::process::{Command, Stdio}; |
| 15 | |
| 16 | use serde::{Deserialize, Serialize}; |
| 17 | |
| 18 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] |
| 19 | #[serde(rename_all = "snake_case")] |
| 20 | pub enum Backend { |
| 21 | #[default] |
| 22 | Sandboxed, |
| 23 | Docker, |
| 24 | Host, |
| 25 | } |
| 26 | |
| 27 | impl 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)] |
| 40 | pub 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)] |
| 51 | pub 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. |
| 57 | pub 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. |
| 70 | pub 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. |
| 80 | pub 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. |
| 97 | pub 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)] |
| 118 | mod 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 | } |