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 runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 1 | //! `g1t-runner sandbox`: self-hosted runners on a person's own computer |
| 2 | //! that nothing a job runs can get out of, to the computer's files, its | |
| 3 | //! processes, its other programs or its Docker. | |
| 4 | //! | |
| 5 | //! ```text | |
| 6 | //! g1t-runner sandbox up [--url https://g1t.sh --token g1trt_…] [--name NAME] | |
| 7 | //! [--labels a,b] [--group GROUP] [--cpus N] [--memory 8g] [--disk 64g] | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 8 | //! [--only linux,windows] [--no-native | --native] [--state-dir DIR] [--json] |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 9 | //! [--image-file PATH] [--runner-image REF] [--runner-image-file TAR] |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 10 | //! g1t-runner sandbox down [--only linux,windows] [--drain] [--delete] [--json] |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 11 | //! g1t-runner sandbox status [--json] |
| 12 | //! g1t-runner sandbox update [--image-file PATH] [--json] | |
| 13 | //! ``` | |
| 14 | //! | |
| 15 | //! It gives a computer up to two capabilities, each a runner of its own: | |
| 16 | //! | |
| 17 | //! - **`linux`: Linux jobs, in a VM of the runner's own.** The launcher | |
| 18 | //! makes and owns one small Linux VM per computer (a WSL 2 distribution | |
| 19 | //! on Windows, wsl.rs; a vfkit VM on macOS and a QEMU one on Linux | |
| 20 | //! without Sysbox, vm.rs; on Linux with Sysbox no VM is needed, | |
| 21 | //! sysbox.rs). Inside it the runner's image runs under Sysbox, and each | |
| 22 | //! job is a fresh container inside that, removed after (guest.rs): | |
| 23 | //! `nested-container` isolation. The VM's image is published with each | |
| 24 | //! runner release, in its signed manifest, and checked before use | |
| 25 | //! (image.rs). Its registration is kept apart from its system, so | |
| 26 | //! `update` drains the runner, swaps the image and starts again without | |
| 27 | //! registering again. | |
| 28 | //! - **`windows` (and `macos`): jobs native to the computer's OS**, each in | |
| 29 | //! a throwaway VM of its own (native.rs): Windows Sandbox on Windows 11 | |
| 30 | //! Pro, Enterprise and Education. The runner for these runs on the | |
| 31 | //! computer itself, with `--native`, and only starts and stops those VMs. | |
| 32 | //! `vm` isolation. On a Mac this is designed, not released. | |
| 33 | //! | |
| 34 | //! A runner registers labels only for what it sandboxes: the VM's runner | |
| 35 | //! as `linux`, the native one as `windows` only when Windows Sandbox is | |
| 36 | //! there. Nothing here advertises a native label for host mode. | |
| 37 | //! | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 38 | //! Each capability starts and stops on its own. `up --only windows` turns |
| 39 | //! Windows jobs on and starts them, leaving Linux jobs as they are; `down | |
| 40 | //! --only windows` stops them and keeps them off, so a later `up` (with no | |
| 41 | //! `--only`) starts only what is on. `down` with no `--only` stops | |
| 42 | //! everything and turns nothing off. `--no-native` and `--native` turn the | |
| 43 | //! OS's own capability off and on, as `--only` does. | |
| 44 | //! | |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 45 | //! ## `status --json` |
| 46 | //! | |
| 47 | //! The desktop app runs `g1t-runner sandbox status --json` and shows what | |
| 48 | //! it says. One JSON object on standard output, exit code 0 whenever the | |
| 49 | //! status could be read (a sandbox in `error` included): | |
| 50 | //! | |
| 51 | //! ```json | |
| 52 | //! { | |
| 53 | //! "schema": 1, | |
| 54 | //! "state": "running", | |
| 55 | //! "backend": "wsl2", | |
| 56 | //! "runner_version": "0.3.0", | |
| 57 | //! "launcher_version": "0.3.0", | |
| 58 | //! "image_version": "0.3.0", | |
| 59 | //! "isolation": "nested-container", | |
| 60 | //! "job": { "kind": "workflow", "id": "job_…", "name": "build", "repo": "acme/web" }, | |
| 61 | //! "error": null, | |
| 62 | //! "resources": { "cpus": 4, "memory_mib": 8192, "disk_gib": 64 }, | |
| 63 | //! "capabilities": [ | |
| 64 | //! { "id": "linux", "available": true, "state": "running", "isolation": "nested-container", | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 65 | //! "runner_version": "0.3.0", "job": null, "cause": null, "fix": null, "turned_off": false }, |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 66 | //! { "id": "windows", "available": false, "state": "absent", "isolation": "vm", |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 67 | //! "runner_version": null, "job": null, "turned_off": false, |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 68 | //! "cause": "Windows Sandbox is not turned on, or this edition of Windows has none (Home does not).", |
| 69 | //! "fix": "On Windows 11 Pro, Enterprise or Education, turn on Windows Sandbox under Settings, …" } | |
| 70 | //! ], | |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 71 | //! "state_dir": "C:\\Users\\a\\.g1t-runner\\sandbox", |
| 72 | //! "warnings": [] | |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 73 | //! } |
| 74 | //! ``` | |
| 75 | //! | |
| 76 | //! The top-level fields are the Linux VM's. `state` is one of `absent` | |
| 77 | //! (never made, or deleted), `stopped`, `starting`, `running`, `updating` | |
| 78 | //! or `error`; `error` is a sentence for a person, set whenever something | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 79 | //! is wrong, and `cause`/`fix` say why a capability is unavailable, or for |
| 80 | //! a native one in `error` why it didn't start, and what to do. | |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 81 | //! `warnings` holds sentences about what on the computer weakens the |
| 82 | //! sandbox: a runner of the same account registered for host mode, whose | |
| 83 | //! jobs could look inside it, or a state folder others can read. It is | |
| 84 | //! empty when nothing does, and left out by launchers before it. | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 85 | //! `turned_off` is true for a capability someone stopped (`down --only`, |
| 86 | //! `--no-native`), whose state is then `stopped`: stopped by a person, not | |
| 87 | //! failing. `job` is what the runner is running now, or null. Fields | |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 88 | //! are only ever added; `schema` changes if one changes meaning. `up`, |
| 89 | //! `down` and `update` print the same object with `--json` when they are | |
| 90 | //! done, and exit 1 with `error` set when they fail. | |
| 91 | ||
| 92 | // The supervisor runs inside the Linux VM; elsewhere only its builders | |
| 93 | // (tested everywhere) and its commands' parsing are used. | |
| 94 | #[cfg_attr(not(target_os = "linux"), allow(dead_code))] | |
| 95 | pub mod guest; | |
| 96 | mod image; | |
| 97 | mod sysbox; | |
| 98 | #[cfg_attr(not(unix), allow(dead_code))] | |
| 99 | mod vm; | |
| 100 | mod wsl; | |
| 101 | ||
| 102 | use std::path::{Path, PathBuf}; | |
| 103 | use std::process::{Command, Stdio}; | |
| 104 | use std::time::{Duration, Instant}; | |
| 105 | ||
| 106 | use anyhow::{Context, Result, anyhow, bail}; | |
| 107 | use serde::{Deserialize, Serialize}; | |
| 108 | ||
| 109 | use super::native::{Availability, MacVm, WsbFound}; | |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 110 | use super::private::{self, write_private}; |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 111 | use super::{JobStatus, Options, RunnerStatus, VERSION, config, log}; |
| 112 | use guest::{GuestStatus, Settings}; | |
| 113 | ||
| 114 | /// What `status --json`'s shape is. | |
| 115 | pub const SCHEMA: u32 = 1; | |
| 116 | /// The site whose releases the sandbox's images come from when none was | |
| 117 | /// given. | |
| 118 | pub const DEFAULT_SITE: &str = "https://g1t.sh"; | |
| 119 | ||
| 120 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| 121 | pub enum StopMode { | |
| 122 | /// After the running job, if any. | |
| 123 | Drain, | |
| 124 | /// Now: the running job is cut short, and g1t told so. | |
| 125 | Now, | |
| 126 | } | |
| 127 | ||
| 128 | /// The VM's (or the runner container's) share of the computer. | |
| 129 | #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] | |
| 130 | pub struct Resources { | |
| 131 | pub cpus: u32, | |
| 132 | pub memory_mib: u64, | |
| 133 | pub disk_gib: u64, | |
| 134 | } | |
| 135 | ||
| 136 | /// One kind of sandbox that holds the Linux runner. | |
| 137 | pub trait Backend { | |
| 138 | /// What `status` calls it: `wsl2`, `vfkit`, `qemu` or `sysbox`. | |
| 139 | fn name(&self) -> &'static str; | |
| 140 | /// The release files it is made from. | |
| 141 | fn image_keys(&self) -> Vec<String>; | |
| 142 | fn availability(&self) -> Availability; | |
| 143 | fn exists(&self) -> bool; | |
| 144 | fn running(&self) -> bool; | |
| 145 | fn create(&self, local: &image::Local, resources: &Resources) -> Result<()>; | |
| 146 | fn configure(&self, settings: &Settings) -> Result<()>; | |
| 147 | /// A runner image from a file, for a build that is not released. | |
| 148 | fn load_image(&self, tar: &Path) -> Result<()>; | |
| 149 | fn start(&self) -> Result<()>; | |
| 150 | fn guest_status(&self) -> Option<GuestStatus>; | |
| 151 | fn stop(&self, mode: StopMode) -> Result<()>; | |
| 152 | /// The runner's registration, when the sandbox's system and its state | |
| 153 | /// are one (WSL), to carry across an update. | |
| 154 | fn export_state(&self) -> Result<Option<Vec<u8>>>; | |
| 155 | fn import_state(&self, bytes: &[u8]) -> Result<()>; | |
| 156 | /// Removes the sandbox's system, keeping its state where that is apart. | |
| 157 | fn remove_system(&self) -> Result<()> { | |
| 158 | self.remove() | |
| 159 | } | |
| 160 | /// Removes the sandbox entirely. | |
| 161 | fn remove(&self) -> Result<()>; | |
| 162 | } | |
| 163 | ||
| 164 | /// What the launcher keeps in `<state>/sandbox.json`. | |
| 165 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] | |
| 166 | pub struct Saved { | |
| 167 | #[serde(default)] | |
| 168 | pub url: Option<String>, | |
| 169 | /// The runners' name before `-linux` / `-windows`. | |
| 170 | #[serde(default)] | |
| 171 | pub name: Option<String>, | |
| 172 | #[serde(default)] | |
| 173 | pub labels: Vec<String>, | |
| 174 | #[serde(default)] | |
| 175 | pub group: Option<String>, | |
| 176 | #[serde(default)] | |
| 177 | pub resources: Resources, | |
| 178 | #[serde(default)] | |
| 179 | pub image_version: Option<String>, | |
| 180 | #[serde(default)] | |
| 181 | pub runner_image: Option<String>, | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 182 | /// The capabilities turned off (`down --only`, `--no-native`), kept |
| 183 | /// until `up --only` (or `--native`) turns them on again. | |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 184 | #[serde(default)] |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 185 | pub off: Vec<String>, |
| 186 | /// From a launcher before `off`: the native capability turned off. | |
| 187 | #[serde(default, skip_serializing)] | |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 188 | pub no_native: bool, |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 189 | /// Why the native capability's runner didn't start, until it does. |
| 190 | #[serde(default)] | |
| 191 | pub native_error: Option<String>, | |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 192 | /// `starting` or `updating`, while a launcher (`phase_pid`) does it. |
| 193 | #[serde(default)] | |
| 194 | pub phase: Option<String>, | |
| 195 | #[serde(default)] | |
| 196 | pub phase_pid: Option<u32>, | |
| 197 | /// What went wrong last, for `status`. | |
| 198 | #[serde(default)] | |
| 199 | pub error: Option<String>, | |
| 200 | } | |
| 201 | ||
| 202 | impl Saved { | |
| 203 | fn path(dir: &Path) -> PathBuf { | |
| 204 | dir.join("sandbox.json") | |
| 205 | } | |
| 206 | ||
| 207 | pub fn load(dir: &Path) -> Saved { | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 208 | let mut saved: Saved = std::fs::read(Saved::path(dir)).ok().and_then(|b| serde_json::from_slice(&b).ok()).unwrap_or_default(); |
| 209 | // An earlier launcher's `--no-native`, said the way this one says it. | |
| 210 | if std::mem::take(&mut saved.no_native) | |
| 211 | && let Some(native) = native_id() | |
| 212 | { | |
| 213 | saved.turn(native, false); | |
| 214 | } | |
| 215 | saved | |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 216 | } |
| 217 | ||
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 218 | /// Whether a capability was turned off. |
| 219 | pub fn is_off(&self, id: &str) -> bool { | |
| 220 | self.off.iter().any(|o| o == id) | |
| 221 | } | |
| 222 | ||
| 223 | /// Turns a capability on or off, until said otherwise. | |
| 224 | pub fn turn(&mut self, id: &str, on: bool) { | |
| 225 | self.off.retain(|o| o != id); | |
| 226 | if !on { | |
| 227 | self.off.push(id.to_owned()); | |
| 228 | self.off.sort(); | |
| 229 | } | |
| 230 | } | |
| 231 | ||
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 232 | pub fn save(&self, dir: &Path) -> Result<()> { |
| 233 | std::fs::create_dir_all(dir).with_context(|| format!("could not make {}", dir.display()))?; | |
| 234 | write_private(&Saved::path(dir), &serde_json::to_vec_pretty(self)?) | |
| 235 | } | |
| 236 | ||
| 237 | fn site(&self) -> &str { | |
| 238 | self.url.as_deref().unwrap_or(DEFAULT_SITE) | |
| 239 | } | |
| 240 | } | |
| 241 | ||
| 242 | /// What `status --json` prints (see the module's documentation). | |
| 243 | #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] | |
| 244 | pub struct Status { | |
| 245 | pub schema: u32, | |
| 246 | pub state: String, | |
| 247 | pub backend: Option<String>, | |
| 248 | pub runner_version: Option<String>, | |
| 249 | pub launcher_version: String, | |
| 250 | pub image_version: Option<String>, | |
| 251 | pub isolation: Option<String>, | |
| 252 | pub job: Option<JobStatus>, | |
| 253 | pub error: Option<String>, | |
| 254 | pub resources: Resources, | |
| 255 | pub capabilities: Vec<Capability>, | |
| 256 | pub state_dir: String, | |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 257 | /// Sentences about what on the computer weakens the sandbox (a |
| 258 | /// host-mode runner of the same account, a state folder others can | |
| 259 | /// read); empty when nothing does. Older launchers leave it out. | |
| 260 | #[serde(default)] | |
| 261 | pub warnings: Vec<String>, | |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 262 | } |
| 263 | ||
| 264 | /// One kind of job this computer can run sandboxed. | |
| 265 | #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] | |
| 266 | pub struct Capability { | |
| 267 | /// `linux`, `windows` or `macos`. | |
| 268 | pub id: String, | |
| 269 | pub available: bool, | |
| 270 | pub state: String, | |
| 271 | pub isolation: Option<String>, | |
| 272 | pub runner_version: Option<String>, | |
| 273 | pub job: Option<JobStatus>, | |
| 274 | pub cause: Option<String>, | |
| 275 | pub fix: Option<String>, | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 276 | /// Someone stopped it (`down --only`, `--no-native`): `stopped` until |
| 277 | /// `up --only` turns it on again. Older launchers leave it out. | |
| 278 | pub turned_off: bool, | |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 279 | } |
| 280 | ||
| 281 | // --- The computer ------------------------------------------------------------- | |
| 282 | ||
| 283 | /// Whether a process is still running. | |
| 284 | pub fn process_alive(pid: u32) -> bool { | |
| 285 | #[cfg(unix)] | |
| 286 | { | |
| 287 | let alive = unsafe { libc::kill(pid as libc::pid_t, 0) } == 0; | |
| 288 | alive || std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM) | |
| 289 | } | |
| 290 | #[cfg(windows)] | |
| 291 | { | |
| 292 | let mut command = Command::new("tasklist"); | |
| 293 | command.args(["/FI", &format!("PID eq {pid}"), "/NH", "/FO", "CSV"]); | |
| 294 | super::native::hide_window(&mut command); | |
| 295 | command.output().is_ok_and(|o| String::from_utf8_lossy(&o.stdout).contains(&format!("\"{pid}\""))) | |
| 296 | } | |
| 297 | } | |
| 298 | ||
| 299 | /// Starts a process that outlives the launcher (and the desktop app that | |
| 300 | /// ran it), with no window. Its pid. | |
| 301 | pub fn spawn_detached(mut command: Command) -> Result<u32> { | |
| 302 | #[cfg(windows)] | |
| 303 | { | |
| 304 | use std::os::windows::process::CommandExt; | |
| 305 | const CREATE_NO_WINDOW: u32 = 0x0800_0000; | |
| 306 | const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200; | |
| 307 | const CREATE_BREAKAWAY_FROM_JOB: u32 = 0x0100_0000; | |
| 308 | // The caller's own output (the desktop app's pipe) must not be | |
| 309 | // inherited by a process that outlives this one, or the caller | |
| 310 | // waits for its end until the sandbox stops. | |
| 311 | let _quiet = StdHandlesPrivate::new(); | |
| 312 | command.creation_flags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP | CREATE_BREAKAWAY_FROM_JOB); | |
| 313 | if let Ok(child) = command.spawn() { | |
| 314 | return Ok(child.id()); | |
| 315 | } | |
| 316 | // A job object that allows no breakaway: started inside it. | |
| 317 | command.creation_flags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP); | |
| 318 | Ok(command.spawn().context("could not start the sandbox")?.id()) | |
| 319 | } | |
| 320 | #[cfg(unix)] | |
| 321 | { | |
| 322 | std::os::unix::process::CommandExt::process_group(&mut command, 0); | |
| 323 | Ok(command.spawn().context("could not start the sandbox")?.id()) | |
| 324 | } | |
| 325 | } | |
| 326 | ||
| 327 | /// While it lives, this process's standard handles are not inheritable. | |
| 328 | #[cfg(windows)] | |
| 329 | struct StdHandlesPrivate { | |
| 330 | handles: Vec<isize>, | |
| 331 | } | |
| 332 | ||
| 333 | #[cfg(windows)] | |
| 334 | unsafe extern "system" { | |
| 335 | fn GetStdHandle(which: u32) -> isize; | |
| 336 | fn SetHandleInformation(handle: isize, mask: u32, flags: u32) -> i32; | |
| 337 | fn GetHandleInformation(handle: isize, flags: *mut u32) -> i32; | |
| 338 | } | |
| 339 | ||
| 340 | #[cfg(windows)] | |
| 341 | impl StdHandlesPrivate { | |
| 342 | const INHERIT: u32 = 1; | |
| 343 | ||
| 344 | fn new() -> StdHandlesPrivate { | |
| 345 | let mut handles = Vec::new(); | |
| 346 | // STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE. | |
| 347 | for which in [-10i32 as u32, -11i32 as u32, -12i32 as u32] { | |
| 348 | let handle = unsafe { GetStdHandle(which) }; | |
| 349 | let mut flags = 0u32; | |
| 350 | if handle != 0 && handle != -1 && unsafe { GetHandleInformation(handle, &mut flags) } != 0 && flags & Self::INHERIT != 0 { | |
| 351 | unsafe { SetHandleInformation(handle, Self::INHERIT, 0) }; | |
| 352 | handles.push(handle); | |
| 353 | } | |
| 354 | } | |
| 355 | StdHandlesPrivate { handles } | |
| 356 | } | |
| 357 | } | |
| 358 | ||
| 359 | #[cfg(windows)] | |
| 360 | impl Drop for StdHandlesPrivate { | |
| 361 | fn drop(&mut self) { | |
| 362 | for handle in &self.handles { | |
| 363 | unsafe { SetHandleInformation(*handle, Self::INHERIT, Self::INHERIT) }; | |
| 364 | } | |
| 365 | } | |
| 366 | } | |
| 367 | ||
| 368 | /// The computer's memory, in MiB. | |
| 369 | pub fn total_memory_mib() -> Option<u64> { | |
| 370 | #[cfg(windows)] | |
| 371 | { | |
| 372 | #[repr(C)] | |
| 373 | struct MemoryStatusEx { | |
| 374 | length: u32, | |
| 375 | load: u32, | |
| 376 | total_phys: u64, | |
| 377 | avail_phys: u64, | |
| 378 | total_page: u64, | |
| 379 | avail_page: u64, | |
| 380 | total_virtual: u64, | |
| 381 | avail_virtual: u64, | |
| 382 | avail_extended: u64, | |
| 383 | } | |
| 384 | unsafe extern "system" { | |
| 385 | fn GlobalMemoryStatusEx(buffer: *mut MemoryStatusEx) -> i32; | |
| 386 | } | |
| 387 | let mut status = MemoryStatusEx { length: std::mem::size_of::<MemoryStatusEx>() as u32, load: 0, total_phys: 0, avail_phys: 0, total_page: 0, avail_page: 0, total_virtual: 0, avail_virtual: 0, avail_extended: 0 }; | |
| 388 | let ok = unsafe { GlobalMemoryStatusEx(&mut status) } != 0; | |
| 389 | ok.then_some(status.total_phys >> 20) | |
| 390 | } | |
| 391 | #[cfg(target_os = "macos")] | |
| 392 | { | |
| 393 | let out = Command::new("sysctl").args(["-n", "hw.memsize"]).output().ok()?; | |
| 394 | String::from_utf8_lossy(&out.stdout).trim().parse::<u64>().ok().map(|b| b >> 20) | |
| 395 | } | |
| 396 | #[cfg(all(unix, not(target_os = "macos")))] | |
| 397 | { | |
| 398 | let meminfo = std::fs::read_to_string("/proc/meminfo").ok()?; | |
| 399 | meminfo.lines().find_map(|l| l.strip_prefix("MemTotal:"))?.trim().trim_end_matches("kB").trim().parse::<u64>().ok().map(|kb| kb >> 10) | |
| 400 | } | |
| 401 | } | |
| 402 | ||
| 403 | /// The sandbox's share when none is given: half the CPUs (at most 8) and | |
| 404 | /// a quarter of the memory (2 to 8 GiB, never more than half), and a disk | |
| 405 | /// that may grow to 64 GiB. | |
| 406 | pub fn default_resources(cpus: usize, memory_mib: Option<u64>) -> Resources { | |
| 407 | let cpus = (cpus / 2).clamp(1, 8) as u32; | |
| 408 | let total = memory_mib.unwrap_or(8192); | |
| 409 | let memory_mib = (total / 4).clamp(2048, 8192).min(total / 2).max(1024); | |
| 410 | Resources { cpus, memory_mib, disk_gib: 64 } | |
| 411 | } | |
| 412 | ||
| 413 | fn parse_gib(text: &str) -> Option<u64> { | |
| 414 | super::exec::mib(text).map(|m| m.div_ceil(1024)) | |
| 415 | } | |
| 416 | ||
| 417 | /// The backend for this computer's Linux runner. | |
| 418 | pub fn backend(dir: &Path) -> Box<dyn Backend> { | |
| 419 | let dir = dir.to_path_buf(); | |
| 420 | match std::env::consts::OS { | |
| 421 | "windows" => Box::new(wsl::Wsl { distro: wsl::DISTRO.to_owned(), dir }), | |
| 422 | "macos" => Box::new(vm::DiskVm { hypervisor: vm::Hypervisor::Vfkit, dir }), | |
| 423 | _ => { | |
| 424 | let sysbox = sysbox::SysboxHost { dir: dir.clone() }; | |
| 425 | if sysbox.exists() || sysbox.availability().available { | |
| 426 | Box::new(sysbox) | |
| 427 | } else { | |
| 428 | Box::new(vm::DiskVm { hypervisor: vm::Hypervisor::Qemu, dir }) | |
| 429 | } | |
| 430 | } | |
| 431 | } | |
| 432 | } | |
| 433 | ||
| 434 | // --- The native capability ------------------------------------------------------ | |
| 435 | ||
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 436 | /// What this OS's native capability is called: `windows`, `macos`, or none on Linux. |
| 437 | fn native_id() -> Option<&'static str> { | |
| 438 | match std::env::consts::OS { | |
| 439 | "windows" => Some("windows"), | |
| 440 | "macos" => Some("macos"), | |
| 441 | _ => None, | |
| 442 | } | |
| 443 | } | |
| 444 | ||
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 445 | /// What this OS's native sandbox is called, and whether it is here. |
| 446 | fn native_capability() -> Option<(&'static str, Availability)> { | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 447 | match native_id()? { |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 448 | "windows" => Some(("windows", WsbFound::look().availability())), |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 449 | os => Some((os, MacVm::availability())), |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 450 | } |
| 451 | } | |
| 452 | ||
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 453 | /// The capabilities this computer has, by id: `linux`, and its OS's own. |
| 454 | fn kinds() -> Vec<&'static str> { | |
| 455 | let mut kinds = vec!["linux"]; | |
| 456 | kinds.extend(native_id()); | |
| 457 | kinds | |
| 458 | } | |
| 459 | ||
| 460 | /// How a capability is said to a person: `Windows`. | |
| 461 | fn kind_name(id: &str) -> &str { | |
| 462 | match id { | |
| 463 | "linux" => "Linux", | |
| 464 | "windows" => "Windows", | |
| 465 | "macos" => "macOS", | |
| 466 | other => other, | |
| 467 | } | |
| 468 | } | |
| 469 | ||
| 470 | /// `--only linux,windows`: the capabilities a command is for, of those | |
| 471 | /// this computer has (`native` is its OS's own), or None for all of them. | |
| 472 | fn only(options: &Options) -> Result<Option<Vec<&'static str>>> { | |
| 473 | let Some(given) = options.value("only") else { return Ok(None) }; | |
| 474 | only_of(given, &kinds()).map(Some) | |
| 475 | } | |
| 476 | ||
| 477 | fn only_of(given: &str, have: &[&'static str]) -> Result<Vec<&'static str>> { | |
| 478 | let mut chosen: Vec<&'static str> = Vec::new(); | |
| 479 | for word in given.split(',').map(str::trim).filter(|w| !w.is_empty()) { | |
| 480 | let word = if word == "native" { have.iter().copied().find(|k| *k != "linux").unwrap_or(word) } else { word }; | |
| 481 | let Some(kind) = have.iter().find(|k| **k == word) else { | |
| 482 | bail!("--only takes {} on this computer, not {word}", have.join(" or ")); | |
| 483 | }; | |
| 484 | if !chosen.contains(kind) { | |
| 485 | chosen.push(kind); | |
| 486 | } | |
| 487 | } | |
| 488 | if chosen.is_empty() { | |
| 489 | bail!("--only takes {} on this computer", have.join(" or ")); | |
| 490 | } | |
| 491 | Ok(chosen) | |
| 492 | } | |
| 493 | ||
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 494 | fn native_dir(dir: &Path) -> PathBuf { |
| 495 | dir.join("native") | |
| 496 | } | |
| 497 | ||
| 498 | fn native_pid(dir: &Path) -> Option<u32> { | |
| 499 | std::fs::read_to_string(native_dir(dir).join("runner.pid")).ok().and_then(|p| p.trim().parse().ok()) | |
| 500 | } | |
| 501 | ||
| 502 | fn native_running(dir: &Path) -> bool { | |
| 503 | native_pid(dir).is_some_and(process_alive) | |
| 504 | } | |
| 505 | ||
| 506 | /// Registers (once) and starts the runner for native jobs on the computer. | |
| 507 | fn native_up(dir: &Path, saved: &Saved, token: Option<&str>, os: &str) -> Result<()> { | |
| 508 | let folder = native_dir(dir); | |
| 509 | if config::load(&folder).is_err() { | |
| 510 | let (Some(url), Some(token)) = (saved.url.as_deref(), token) else { | |
| 511 | bail!("the {os} runner is not registered: pass --url and --token"); | |
| 512 | }; | |
| 513 | let mut args = vec!["--url".to_owned(), url.to_owned(), "--token".into(), token.to_owned(), "--native".into()]; | |
| 514 | args.extend(["--name".into(), format!("{}-{os}", base_name(saved))]); | |
| 515 | if !saved.labels.is_empty() { | |
| 516 | args.extend(["--labels".into(), saved.labels.join(",")]); | |
| 517 | } | |
| 518 | if let Some(group) = &saved.group { | |
| 519 | args.extend(["--group".into(), group.clone()]); | |
| 520 | } | |
| 521 | let options = Options::parse(&args, &super::VALUE_OPTIONS)?; | |
| 522 | super::register(&options, &folder)?; | |
| 523 | } | |
| 524 | if native_running(dir) { | |
| 525 | return Ok(()); | |
| 526 | } | |
| 527 | let me = std::env::current_exe()?; | |
| 528 | let log_file = std::fs::File::create(folder.join("runner.log"))?; | |
| 529 | let mut command = Command::new(me); | |
| 530 | command.args(["run", "--dir"]).arg(&folder).stdin(Stdio::null()).stdout(log_file.try_clone()?).stderr(log_file); | |
| 531 | let pid = spawn_detached(command)?; | |
| 532 | std::fs::write(folder.join("runner.pid"), pid.to_string())?; | |
| 533 | Ok(()) | |
| 534 | } | |
| 535 | ||
| 536 | fn native_down(dir: &Path, mode: StopMode) { | |
| 537 | let folder = native_dir(dir); | |
| 538 | let Some(pid) = native_pid(dir).filter(|p| process_alive(*p)) else { return }; | |
| 539 | let _ = std::fs::write(folder.join(if mode == StopMode::Drain { "drain" } else { "stop" }), ""); | |
| 540 | let limit = (mode == StopMode::Now).then(|| Instant::now() + Duration::from_secs(90)); | |
| 541 | while process_alive(pid) { | |
| 542 | if limit.is_some_and(|l| Instant::now() > l) { | |
| 543 | #[cfg(windows)] | |
| 544 | let _ = Command::new("taskkill").args(["/F", "/PID", &pid.to_string()]).stdout(Stdio::null()).stderr(Stdio::null()).status(); | |
| 545 | #[cfg(unix)] | |
| 546 | unsafe { | |
| 547 | libc::kill(pid as libc::pid_t, libc::SIGKILL); | |
| 548 | } | |
| 549 | break; | |
| 550 | } | |
| 551 | std::thread::sleep(Duration::from_millis(500)); | |
| 552 | } | |
| 553 | let _ = std::fs::remove_file(folder.join("runner.pid")); | |
| 554 | } | |
| 555 | ||
| 556 | fn base_name(saved: &Saved) -> String { | |
| 557 | saved.name.clone().unwrap_or_else(|| super::runner_name(&super::hostname())) | |
| 558 | } | |
| 559 | ||
| 560 | // --- Status ----------------------------------------------------------------------- | |
| 561 | ||
| 562 | /// The Linux capability's state from what is known. | |
| 563 | pub fn linux_state(available: bool, exists: bool, running: bool, phase: Option<&str>, guest: Option<&GuestStatus>, error: Option<&str>) -> &'static str { | |
| 564 | if let Some(phase) = phase { | |
| 565 | return if phase == "updating" { "updating" } else { "starting" }; | |
| 566 | } | |
| 567 | if !exists { | |
| 568 | return if available && error.is_none() { "absent" } else if error.is_some() { "error" } else { "absent" }; | |
| 569 | } | |
| 570 | if !running { | |
| 571 | return if error.is_some() { "error" } else { "stopped" }; | |
| 572 | } | |
| 573 | match guest.map(|g| (g.state.as_str(), g.runner.is_some())) { | |
| 574 | // Running, once the runner has registered and says so. | |
| 575 | Some(("running" | "stopping", true)) => "running", | |
| 576 | Some(("error", _)) => "error", | |
| 577 | Some(("stopped", _)) => "stopped", | |
| 578 | _ => "starting", | |
| 579 | } | |
| 580 | } | |
| 581 | ||
| 582 | pub fn status(dir: &Path) -> Status { | |
| 583 | let mut saved = Saved::load(dir); | |
| 584 | // A launcher that died mid-way leaves no phase behind. | |
| 585 | if saved.phase.is_some() && !saved.phase_pid.is_some_and(process_alive) { | |
| 586 | saved.phase = None; | |
| 587 | } | |
| 588 | let backend = backend(dir); | |
| 589 | let available = backend.availability(); | |
| 590 | let exists = backend.exists(); | |
| 591 | let running = exists && backend.running(); | |
| 592 | let guest = if running { backend.guest_status() } else { None }; | |
| 593 | let runner = guest.as_ref().and_then(|g| g.runner.clone()); | |
| 594 | let mut error = saved.error.clone().or_else(|| guest.as_ref().and_then(|g| g.error.clone())); | |
| 595 | if !available.available && error.is_none() { | |
| 596 | error = available.cause.clone(); | |
| 597 | } | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 598 | let linux_off = saved.is_off("linux"); |
| 599 | let mut state = linux_state(available.available, exists, running, saved.phase.as_deref(), guest.as_ref(), error.as_deref()); | |
| 600 | // Turned off and not running is stopped by a person: nothing is wrong. | |
| 601 | if linux_off && saved.phase.is_none() && !running { | |
| 602 | state = "stopped"; | |
| 603 | } | |
| 604 | if state == "running" || (linux_off && state == "stopped") { | |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 605 | error = None; |
| 606 | } | |
| 607 | let linux = Capability { | |
| 608 | id: "linux".into(), | |
| 609 | available: available.available, | |
| 610 | state: state.into(), | |
| 611 | isolation: Some(runner.as_ref().map(|r| r.isolation.clone()).unwrap_or_else(|| "nested-container".into())), | |
| 612 | runner_version: runner.as_ref().map(|r| r.version.clone()), | |
| 613 | job: runner.as_ref().and_then(|r| r.job.clone()), | |
| 614 | cause: available.cause.clone(), | |
| 615 | fix: available.fix.clone(), | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 616 | turned_off: linux_off, |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 617 | }; |
| 618 | let mut capabilities = vec![linux.clone()]; | |
| 619 | if let Some((os, found)) = native_capability() { | |
| 620 | let folder = native_dir(dir); | |
| 621 | let native_status: Option<RunnerStatus> = std::fs::read(super::status_path(&folder)).ok().and_then(|b| serde_json::from_slice(&b).ok()); | |
| 622 | let registered = config::load(&folder).is_ok(); | |
| 623 | let alive = native_running(dir); | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 624 | let off = saved.is_off(os); |
| 625 | let failed = saved.native_error.clone().filter(|_| !alive && !off && found.available); | |
| 626 | let state = native_state(registered, alive, off, failed.is_some()); | |
| 627 | let (cause, fix) = match failed { | |
| 628 | Some(error) => (Some(error), None), | |
| 629 | None => (found.cause.clone(), found.fix.clone()), | |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 630 | }; |
| 631 | capabilities.push(Capability { | |
| 632 | id: os.into(), | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 633 | available: found.available, |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 634 | state: state.into(), |
| 635 | isolation: Some("vm".into()), | |
| 636 | runner_version: native_status.as_ref().filter(|_| alive).map(|s| s.version.clone()), | |
| 637 | job: native_status.filter(|_| alive).and_then(|s| s.job), | |
| 638 | cause, | |
| 639 | fix, | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 640 | turned_off: off, |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 641 | }); |
| 642 | } | |
| 643 | Status { | |
| 644 | schema: SCHEMA, | |
| 645 | state: linux.state, | |
| 646 | backend: Some(backend.name().to_owned()), | |
| 647 | runner_version: linux.runner_version, | |
| 648 | launcher_version: VERSION.to_owned(), | |
| 649 | // What the running image says it is, else what was installed. | |
| 650 | image_version: guest.as_ref().and_then(|g| g.image_version.clone()).or_else(|| saved.image_version.clone()), | |
| 651 | isolation: linux.isolation, | |
| 652 | job: linux.job, | |
| 653 | error, | |
| 654 | resources: saved.resources, | |
| 655 | capabilities, | |
| 656 | state_dir: dir.display().to_string(), | |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 657 | warnings: warnings(dir), |
| 658 | } | |
| 659 | } | |
| 660 | ||
| 661 | // --- What weakens it --------------------------------------------------------------- | |
| 662 | ||
| 663 | /// What on this computer lets someone look inside the sandbox: a runner of | |
| 664 | /// this account's that runs jobs as it, and a state folder others can read. | |
| 665 | fn warnings(dir: &Path) -> Vec<String> { | |
| 666 | let mut said: Vec<String> = host_mode_runners(&host_runner_folders()).iter().map(|folder| host_mode_warning(folder)).collect(); | |
| 667 | if dir.exists() { | |
| 668 | let readers = private::readers(dir); | |
| 669 | if !readers.is_empty() { | |
| 670 | said.push(format!( | |
| 671 | "{} can be read by {}: the runners' credentials and the VM's disk are in it. The next `g1t-runner sandbox up` makes it private, or refuses to start.", | |
| 672 | dir.display(), | |
| 673 | readers.join(", ") | |
| 674 | )); | |
| 675 | } | |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 676 | } |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 677 | said |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 678 | } |
| 679 | ||
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 680 | /// Where a runner of this account's may be registered: `g1t-runner`'s own |
| 681 | /// folder (`G1T_RUNNER_DIR`, `~/.g1t-runner`), and the desktop app's for | |
| 682 | /// running jobs directly on the computer. | |
| 683 | fn host_runner_folders() -> Vec<PathBuf> { | |
| 684 | let mut folders = vec![config::folder(None)]; | |
| 685 | let home = std::env::var_os(if cfg!(windows) { "USERPROFILE" } else { "HOME" }).map(PathBuf::from); | |
| 686 | if let Some(home) = &home { | |
| 687 | folders.push(home.join(".g1t-runner")); | |
| 688 | } | |
| 689 | // The desktop app's data folder (its `app_data_dir`): runner/host. | |
| 690 | let data = match std::env::consts::OS { | |
| 691 | "windows" => std::env::var_os("APPDATA").map(PathBuf::from), | |
| 692 | "macos" => home.as_ref().map(|h| h.join("Library").join("Application Support")), | |
| 693 | _ => std::env::var_os("XDG_DATA_HOME").filter(|d| !d.is_empty()).map(PathBuf::from).or_else(|| home.as_ref().map(|h| h.join(".local").join("share"))), | |
| 694 | }; | |
| 695 | if let Some(data) = data { | |
| 696 | folders.push(data.join("sh.g1t.desktop").join("runner").join("host")); | |
| 697 | } | |
| 698 | folders.dedup(); | |
| 699 | folders | |
| 700 | } | |
| 701 | ||
| 702 | /// Those of `folders` that hold a runner registered for host mode: jobs as | |
| 703 | /// processes of this account, neither in a container nor in a VM. | |
| 704 | fn host_mode_runners(folders: &[PathBuf]) -> Vec<PathBuf> { | |
| 705 | folders.iter().filter(|folder| config::load(folder).is_ok_and(|c| !c.docker && !c.native)).cloned().collect() | |
| 706 | } | |
| 707 | ||
| 708 | fn host_mode_warning(folder: &Path) -> String { | |
| 709 | format!( | |
| 710 | "A runner registered in {} runs jobs directly on this computer, as you (host mode). Whenever it runs, what its jobs run can read this sandbox's runner credentials and reach inside the sandbox, as you can. Remove it (`g1t-runner remove --dir {}`), or register it under another account.", | |
| 711 | folder.display(), | |
| 712 | folder.display() | |
| 713 | ) | |
| 714 | } | |
| 715 | ||
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 716 | /// The native capability's state: running while its runner lives; |
| 717 | /// stopped when turned off, or registered and not running; `error` when | |
| 718 | /// it didn't start; `absent` before it is registered. | |
| 719 | pub fn native_state(registered: bool, alive: bool, off: bool, failed: bool) -> &'static str { | |
| 720 | if alive { | |
| 721 | "running" | |
| 722 | } else if off { | |
| 723 | "stopped" | |
| 724 | } else if failed { | |
| 725 | "error" | |
| 726 | } else if registered { | |
| 727 | "stopped" | |
| 728 | } else { | |
| 729 | "absent" | |
| 730 | } | |
| 731 | } | |
| 732 | ||
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 733 | fn print(status: &Status, json: bool) { |
| 734 | if json { | |
| 735 | println!("{}", serde_json::to_string_pretty(status).unwrap_or_default()); | |
| 736 | return; | |
| 737 | } | |
| 738 | println!("Sandbox: {} ({})", status.state, status.backend.as_deref().unwrap_or("none")); | |
| 739 | for capability in &status.capabilities { | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 740 | let said = match (capability.turned_off, capability.available) { |
| 741 | (true, _) => "stopped (turned off; `up --only` turns it on)", | |
| 742 | (false, true) => capability.state.as_str(), | |
| 743 | (false, false) => "unavailable", | |
| 744 | }; | |
| 745 | let mut line = format!(" {:8} {said}", capability.id); | |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 746 | if let Some(job) = &capability.job { |
| 747 | line.push_str(&format!(", running {} in {}", job.name, job.repo)); | |
| 748 | } | |
| 749 | println!("{line}"); | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 750 | if !capability.turned_off && (!capability.available || capability.state == "error") { |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 751 | if let Some(cause) = &capability.cause { |
| 752 | println!(" {cause}"); | |
| 753 | } | |
| 754 | if let Some(fix) = &capability.fix { | |
| 755 | println!(" {fix}"); | |
| 756 | } | |
| 757 | } | |
| 758 | } | |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 759 | for warning in &status.warnings { |
| 760 | println!("Warning: {warning}"); | |
| 761 | } | |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 762 | if let Some(error) = &status.error { |
| 763 | println!("Error: {error}"); | |
| 764 | } | |
| 765 | } | |
| 766 | ||
| 767 | // --- Commands ----------------------------------------------------------------------- | |
| 768 | ||
| 769 | const HELP: &str = "g1t-runner sandbox: self-hosted runners on your own computer, sandboxed. | |
| 770 | ||
| 771 | g1t-runner sandbox up [--url https://g1t.sh --token <registration token>] | |
| 772 | [--name NAME] [--labels a,b] [--group GROUP] | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 773 | [--cpus N] [--memory 8g] [--disk 64g] [--only linux,windows] |
| 774 | [--no-native | --native] [--json] | |
| 775 | g1t-runner sandbox down [--only linux,windows] [--drain] [--delete] [--json] | |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 776 | g1t-runner sandbox status [--json] |
| 777 | g1t-runner sandbox update [--json] | |
| 778 | ||
| 779 | Linux jobs run in a Linux VM of the runner's own (a WSL distribution on | |
| 780 | Windows, a vfkit VM on macOS; on Linux, Sysbox, or a QEMU VM), each in a | |
| 781 | fresh container inside it. Windows jobs run each in a Windows Sandbox of | |
| 782 | its own (Windows 11 Pro, Enterprise or Education). Nothing a job runs | |
| 783 | reaches this computer's files, programs or Docker. | |
| 784 | ||
| 785 | The first `up` needs --url and --token (make a token under Settings, | |
| 786 | Runners). `down` stops now, cutting a running job short; --drain waits | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 787 | for it. Each kind of job starts and stops on its own: `down --only |
| 788 | windows` stops Windows jobs and keeps them off, leaving Linux jobs | |
| 789 | running, until `up --only windows` turns them on again. `update` drains, | |
| 790 | swaps in the newest signed image, and starts | |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 791 | again, keeping the registration. Every command takes --state-dir DIR |
| 792 | (default ~/.g1t-runner/sandbox). | |
| 793 | Docs: https://docs.g1t.sh/guides/self-hosted-runners/#sandbox-on-your-computer"; | |
| 794 | ||
| 795 | const SANDBOX_VALUES: &[&str] = &[ | |
| 796 | "url", | |
| 797 | "token", | |
| 798 | "name", | |
| 799 | "labels", | |
| 800 | "group", | |
| 801 | "cpus", | |
| 802 | "memory", | |
| 803 | "disk", | |
| 804 | "state-dir", | |
| 805 | "image-file", | |
| 806 | "runner-image", | |
| 807 | "runner-image-file", | |
| 808 | "wait", | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 809 | "only", |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 810 | ]; |
| 811 | ||
| 812 | /// `g1t-runner sandbox <command>`: the process's exit code. | |
| 813 | pub fn main(args: &[String]) -> i32 { | |
| 814 | if args.first().map(String::as_str) == Some("guest") { | |
| 815 | return match guest::main(&args[1..]) { | |
| 816 | Ok(()) => 0, | |
| 817 | Err(error) => { | |
| 818 | eprintln!("g1t-runner: {error:#}"); | |
| 819 | 1 | |
| 820 | } | |
| 821 | }; | |
| 822 | } | |
| 823 | let options = match Options::parse(args.get(1..).unwrap_or_default(), SANDBOX_VALUES) { | |
| 824 | Ok(options) => options, | |
| 825 | Err(error) => { | |
| 826 | eprintln!("g1t-runner: {error:#}"); | |
| 827 | return 2; | |
| 828 | } | |
| 829 | }; | |
| 830 | let json = options.flag("json"); | |
| 831 | let dir = options.value("state-dir").map(PathBuf::from).unwrap_or_else(|| config::folder(None).join("sandbox")); | |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 832 | // The state folder holds the runners' credentials and the VM's disk: |
| 833 | // only this account's, or nothing is made or started in it. | |
| 834 | let private = || private::folder(&dir, "--state-dir"); | |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 835 | let outcome = match args.first().map(String::as_str) { |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 836 | Some("up") => private().and_then(|()| up(&options, &dir)), |
| 837 | Some("down") => private().and_then(|()| down(&options, &dir)), | |
| 838 | Some("update") => private().and_then(|()| update_sandbox(&options, &dir)), | |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 839 | Some("status") => Ok(()), |
| 840 | _ => { | |
| 841 | println!("{HELP}"); | |
| 842 | return 0; | |
| 843 | } | |
| 844 | }; | |
| 845 | let mut status = status(&dir); | |
| 846 | match outcome { | |
| 847 | Ok(()) => { | |
| 848 | print(&status, json); | |
| 849 | 0 | |
| 850 | } | |
| 851 | Err(error) => { | |
| 852 | status.error = Some(format!("{error:#}")); | |
| 853 | if json { | |
| 854 | print(&status, true); | |
| 855 | } else { | |
| 856 | eprintln!("g1t-runner: {error:#}"); | |
| 857 | } | |
| 858 | 1 | |
| 859 | } | |
| 860 | } | |
| 861 | } | |
| 862 | ||
| 863 | /// Runs `step` as a phase (`starting`, `updating`) that `status` reports, | |
| 864 | /// keeping what went wrong for it. | |
| 865 | fn in_phase(dir: &Path, phase: &str, step: impl FnOnce(&mut Saved) -> Result<()>) -> Result<()> { | |
| 866 | let mut saved = Saved::load(dir); | |
| 867 | saved.phase = Some(phase.to_owned()); | |
| 868 | saved.phase_pid = Some(std::process::id()); | |
| 869 | saved.error = None; | |
| 870 | saved.save(dir)?; | |
| 871 | let outcome = step(&mut saved); | |
| 872 | saved.phase = None; | |
| 873 | saved.phase_pid = None; | |
| 874 | saved.error = outcome.as_ref().err().map(|e| format!("{e:#}")); | |
| 875 | saved.save(dir)?; | |
| 876 | outcome | |
| 877 | } | |
| 878 | ||
| 879 | fn up(options: &Options, dir: &Path) -> Result<()> { | |
| 880 | let mut saved = Saved::load(dir); | |
| 881 | if let Some(url) = options.value("url") { | |
| 882 | saved.url = Some(url.trim_end_matches('/').to_owned()); | |
| 883 | } | |
| 884 | if let Some(name) = options.value("name") { | |
| 885 | saved.name = Some(super::runner_name(name)); | |
| 886 | } | |
| 887 | if let Some(labels) = options.value("labels") { | |
| 888 | saved.labels = labels.split(',').map(|l| l.trim().to_owned()).filter(|l| !l.is_empty()).collect(); | |
| 889 | } | |
| 890 | if let Some(group) = options.value("group") { | |
| 891 | saved.group = Some(group.to_owned()); | |
| 892 | } | |
| 893 | let defaults = default_resources(std::thread::available_parallelism().map(|n| n.get()).unwrap_or(2), total_memory_mib()); | |
| 894 | let mut resources = if saved.resources == Resources::default() { defaults } else { saved.resources }; | |
| 895 | if let Some(cpus) = options.value("cpus") { | |
| 896 | resources.cpus = cpus.parse().ok().filter(|n| *n > 0).ok_or_else(|| anyhow!("--cpus takes a number of CPUs, not {cpus}"))?; | |
| 897 | } | |
| 898 | if let Some(memory) = options.value("memory") { | |
| 899 | resources.memory_mib = super::exec::mib(memory).filter(|m| *m >= 1024).ok_or_else(|| anyhow!("--memory takes a size of 1g or more, such as 8g, not {memory}"))?; | |
| 900 | } | |
| 901 | if let Some(disk) = options.value("disk") { | |
| 902 | resources.disk_gib = parse_gib(disk).filter(|g| *g >= 16).ok_or_else(|| anyhow!("--disk takes a size of 16g or more, such as 64g, not {disk}"))?; | |
| 903 | } | |
| 904 | saved.resources = resources; | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 905 | // Each capability on or off, kept until said otherwise: `--only` turns |
| 906 | // on what it names and leaves the rest as they are. | |
| 907 | let only = only(options)?; | |
| 908 | if let Some(native) = native_id() { | |
| 909 | if options.flag("no-native") { | |
| 910 | saved.turn(native, false); | |
| 911 | } else if options.flag("native") { | |
| 912 | saved.turn(native, true); | |
| 913 | } | |
| 914 | } | |
| 915 | for kind in only.iter().flatten() { | |
| 916 | saved.turn(kind, true); | |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 917 | } |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 918 | let wanted = |kind: &str| match &only { |
| 919 | Some(only) => only.contains(&kind), | |
| 920 | None => !saved.is_off(kind), | |
| 921 | }; | |
| 922 | let (want_linux, native) = (wanted("linux"), native_capability().filter(|(os, _)| wanted(os))); | |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 923 | if let Some(image) = options.value("runner-image") { |
| 924 | saved.runner_image = Some(image.to_owned()); | |
| 925 | } | |
| 926 | saved.save(dir)?; | |
| A registration token never shows in the process list. The desktop app passed it to g1t-runner sandbox up as --token, which anyone on the computer can read while up runs; it goes in up's environment now, as G1T_RUNNER_TOKEN, which the launcher reads when --token is left out, as register-and-run already did. | 927 | // From G1T_RUNNER_TOKEN too, as register-and-run takes it: a token on |
| 928 | // the command line shows in the process list to anyone on the machine | |
| 929 | // while `up` runs, and the desktop app passes it this way. | |
| 930 | let token = options | |
| 931 | .value("token") | |
| 932 | .map(str::to_owned) | |
| 933 | .or_else(|| std::env::var("G1T_RUNNER_TOKEN").ok().filter(|token| !token.trim().is_empty())); | |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 934 | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 935 | if !want_linux && native.is_none() { |
| 936 | log("Every kind of job is turned off here: `g1t-runner sandbox up --only linux` turns Linux jobs on again."); | |
| 937 | return Ok(()); | |
| 938 | } | |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 939 | let backend = backend(dir); |
| 940 | let found = backend.availability(); | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 941 | let linux = if !want_linux { |
| 942 | None | |
| 943 | } else if found.available { | |
| 944 | Some(in_phase(dir, "starting", |saved| { | |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 945 | let fresh = !backend.exists(); |
| 946 | if fresh { | |
| 947 | let local = obtain_image(options, saved, backend.as_ref(), dir)?; | |
| 948 | backend.create(&local, &saved.resources)?; | |
| 949 | saved.image_version = Some(local.version.clone()); | |
| 950 | if local.runner_image.is_some() { | |
| 951 | saved.runner_image = local.runner_image.clone(); | |
| 952 | } | |
| 953 | } | |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 954 | // An update that stopped half-way, after the old system was |
| 955 | // removed, left the registration here. | |
| 956 | let carried = dir.join("carry").join("config.json"); | |
| 957 | if let Ok(bytes) = std::fs::read(&carried) { | |
| 958 | backend.import_state(&bytes)?; | |
| 959 | let _ = std::fs::remove_dir_all(dir.join("carry")); | |
| 960 | } | |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 961 | if let Some(tar) = options.value("runner-image-file") { |
| 962 | backend.load_image(Path::new(tar))?; | |
| 963 | } | |
| 964 | let Some(runner_image) = saved.runner_image.clone() else { | |
| 965 | bail!("there is no runner image for the sandbox: this build is not a release, so pass --runner-image or --runner-image-file"); | |
| 966 | }; | |
| 967 | backend.configure(&Settings { | |
| 968 | url: saved.url.clone(), | |
| 969 | token: token.clone(), | |
| 970 | name: Some(format!("{}-linux", base_name(saved))), | |
| 971 | labels: saved.labels.clone(), | |
| 972 | group: saved.group.clone(), | |
| 973 | cpus: saved.resources.cpus, | |
| 974 | memory_mib: saved.resources.memory_mib, | |
| 975 | runner_image, | |
| 976 | image_version: saved.image_version.clone().unwrap_or_default(), | |
| 977 | stop: None, | |
| 978 | })?; | |
| 979 | backend.start()?; | |
| 980 | wait_running(backend.as_ref(), Duration::from_secs(if fresh { 600 } else { 180 })) | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 981 | })) |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 982 | } else { |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 983 | Some(Err(anyhow!("{} {}", found.cause.unwrap_or_default(), found.fix.unwrap_or_default()))) |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 984 | }; |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 985 | let native = match native { |
| 986 | Some((os, found)) if found.available => { | |
| 987 | let started = native_up(dir, &Saved::load(dir), token.as_deref(), os); | |
| 988 | // Why it didn't start is the capability's, for `status`, until it does. | |
| 989 | let mut kept = Saved::load(dir); | |
| 990 | kept.native_error = started.as_ref().err().map(|e| format!("{e:#}")); | |
| 991 | kept.save(dir)?; | |
| 992 | Some((os, started)) | |
| 993 | } | |
| 994 | // Asked for by name, and not here: why, rather than nothing. | |
| 995 | Some((os, found)) if only.is_some() => Some((os, Err(anyhow!("{} {}", found.cause.unwrap_or_default(), found.fix.unwrap_or_default()).context(format!("{} jobs can't run here", kind_name(os)))))), | |
| 996 | _ => None, | |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 997 | }; |
| 998 | match (linux, native) { | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 999 | (None | Some(Ok(())), None | Some((_, Ok(())))) => Ok(()), |
| 1000 | (Some(Err(linux)), Some((os, Ok(())))) => Err(linux.context(format!("{} jobs are sandboxed and running, but Linux jobs are not", kind_name(os)))), | |
| 1001 | (Some(Err(linux)), _) => Err(linux), | |
| 1002 | (Some(Ok(())), Some((os, Err(native)))) => Err(native.context(format!("Linux jobs are sandboxed and running, but {} jobs are not", kind_name(os)))), | |
| 1003 | (None, Some((_, Err(native)))) => Err(native), | |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 1004 | } |
| 1005 | } | |
| 1006 | ||
| 1007 | /// Waits until the sandbox's runner is registered and asking for work (it | |
| 1008 | /// has written its status), or says what went wrong. | |
| 1009 | fn wait_running(backend: &dyn Backend, limit: Duration) -> Result<()> { | |
| 1010 | let until = Instant::now() + limit; | |
| 1011 | loop { | |
| 1012 | if let Some(status) = backend.guest_status() { | |
| 1013 | match status.state.as_str() { | |
| 1014 | "running" if status.runner.as_ref().is_some_and(|r| r.state != "stopped") => return Ok(()), | |
| 1015 | "error" => bail!("{}", status.error.unwrap_or_else(|| "the sandbox stopped".into())), | |
| 1016 | _ => {} | |
| 1017 | } | |
| 1018 | } | |
| 1019 | if Instant::now() > until { | |
| 1020 | bail!("the sandbox did not start within {} seconds; `g1t-runner sandbox status` says how it is now", limit.as_secs()); | |
| 1021 | } | |
| 1022 | std::thread::sleep(Duration::from_secs(1)); | |
| 1023 | } | |
| 1024 | } | |
| 1025 | ||
| 1026 | fn obtain_image(options: &Options, saved: &Saved, backend: &dyn Backend, dir: &Path) -> Result<image::Local> { | |
| 1027 | let keys = backend.image_keys(); | |
| 1028 | if let Some(file) = options.value("image-file") { | |
| 1029 | log(&format!("Using {file}, which is not a release: it is not checked against g1t's signature.")); | |
| 1030 | return image::local(&keys, Path::new(file), saved.runner_image.clone()); | |
| 1031 | } | |
| 1032 | if keys.is_empty() { | |
| 1033 | // Sysbox on Linux: only the runner's image, named in the release. | |
| 1034 | let manifest = image::latest(saved.site()); | |
| 1035 | return Ok(image::Local { | |
| 1036 | version: manifest.as_ref().map(|m| m.version.clone()).unwrap_or_else(|_| VERSION.to_owned()), | |
| 1037 | files: Default::default(), | |
| 1038 | runner_image: manifest.ok().and_then(|m| m.sandbox).and_then(|s| s.runner_image).or_else(|| saved.runner_image.clone()), | |
| 1039 | }); | |
| 1040 | } | |
| 1041 | let manifest = image::latest(saved.site())?; | |
| 1042 | image::obtain(saved.site(), &manifest, &keys, &dir.join("images"), &|line| log(line)) | |
| 1043 | } | |
| 1044 | ||
| 1045 | fn down(options: &Options, dir: &Path) -> Result<()> { | |
| 1046 | let mode = if options.flag("drain") { StopMode::Drain } else { StopMode::Now }; | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 1047 | if let Some(only) = only(options)? { |
| 1048 | return down_only(dir, mode, &only, options.flag("delete")); | |
| 1049 | } | |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 1050 | native_down(dir, mode); |
| 1051 | let backend = backend(dir); | |
| 1052 | if options.flag("delete") { | |
| 1053 | backend.remove()?; | |
| 1054 | let _ = std::fs::remove_dir_all(dir.join("images")); | |
| 1055 | let _ = std::fs::remove_dir_all(native_dir(dir)); | |
| 1056 | let _ = std::fs::remove_dir_all(dir.join("carry")); | |
| 1057 | let mut saved = Saved::load(dir); | |
| 1058 | saved.image_version = None; | |
| 1059 | saved.error = None; | |
| 1060 | saved.save(dir)?; | |
| 1061 | log("Deleted the sandbox. Its runners are still listed on g1t until you remove them there."); | |
| 1062 | return Ok(()); | |
| 1063 | } | |
| 1064 | if backend.exists() { | |
| 1065 | backend.stop(mode)?; | |
| 1066 | } | |
| 1067 | let mut saved = Saved::load(dir); | |
| 1068 | saved.error = None; | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 1069 | saved.native_error = None; |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 1070 | saved.save(dir) |
| 1071 | } | |
| 1072 | ||
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 1073 | /// `down --only`: stops those capabilities and turns them off, leaving the |
| 1074 | /// others as they are, running or not. | |
| 1075 | fn down_only(dir: &Path, mode: StopMode, only: &[&'static str], delete: bool) -> Result<()> { | |
| 1076 | if delete { | |
| 1077 | bail!("--delete removes the whole sandbox, so it takes no --only"); | |
| 1078 | } | |
| 1079 | for kind in only { | |
| 1080 | if *kind == "linux" { | |
| 1081 | let backend = backend(dir); | |
| 1082 | if backend.exists() && backend.running() { | |
| 1083 | backend.stop(mode)?; | |
| 1084 | } | |
| 1085 | } else { | |
| 1086 | native_down(dir, mode); | |
| 1087 | } | |
| 1088 | let mut saved = Saved::load(dir); | |
| 1089 | if *kind == "linux" { | |
| 1090 | saved.error = None; | |
| 1091 | } else { | |
| 1092 | saved.native_error = None; | |
| 1093 | } | |
| 1094 | saved.turn(kind, false); | |
| 1095 | saved.save(dir)?; | |
| 1096 | log(&format!("Stopped {} jobs; they stay off until `g1t-runner sandbox up --only {kind}`.", kind_name(kind))); | |
| 1097 | } | |
| 1098 | Ok(()) | |
| 1099 | } | |
| 1100 | ||
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 1101 | fn update_sandbox(options: &Options, dir: &Path) -> Result<()> { |
| 1102 | let backend = backend(dir); | |
| 1103 | if !backend.exists() { | |
| 1104 | bail!("there is no sandbox to update: run `g1t-runner sandbox up`"); | |
| 1105 | } | |
| 1106 | let saved = Saved::load(dir); | |
| 1107 | let local = if options.value("image-file").is_some() { | |
| 1108 | obtain_image(options, &saved, backend.as_ref(), dir)? | |
| 1109 | } else { | |
| 1110 | let manifest = image::latest(saved.site())?; | |
| 1111 | if saved.image_version.as_deref().is_some_and(|current| !super::update::newer(&manifest.version, current)) { | |
| 1112 | log(&format!("The sandbox's image is the newest, {}.", manifest.version)); | |
| 1113 | return Ok(()); | |
| 1114 | } | |
| 1115 | let keys = backend.image_keys(); | |
| 1116 | if keys.is_empty() { | |
| 1117 | obtain_image(options, &saved, backend.as_ref(), dir)? | |
| 1118 | } else { | |
| 1119 | image::obtain(saved.site(), &manifest, &keys, &dir.join("images"), &|line| log(line))? | |
| 1120 | } | |
| 1121 | }; | |
| 1122 | in_phase(dir, "updating", |saved| { | |
| 1123 | let was_running = backend.running(); | |
| 1124 | if was_running { | |
| 1125 | log("Draining: the runner finishes the job it is running, then stops."); | |
| 1126 | backend.stop(StopMode::Drain)?; | |
| 1127 | } | |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 1128 | // The registration, kept on the computer (in the private state |
| 1129 | // folder) only while no sandbox has it: from just before the old | |
| 1130 | // system goes until the new one has it. Should the update stop in | |
| 1131 | // between, the next `up` puts it in the new system and removes it. | |
| 1132 | let carry = dir.join("carry"); | |
| 1133 | let carried = carry.join("config.json"); | |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 1134 | if let Some(bytes) = backend.export_state()? { |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 1135 | std::fs::create_dir_all(&carry)?; |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 1136 | write_private(&carried, &bytes)?; |
| 1137 | } | |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 1138 | if let Err(error) = backend.remove_system() { |
| 1139 | // The old system, and the registration in it, are still there. | |
| 1140 | let _ = std::fs::remove_dir_all(&carry); | |
| 1141 | return Err(error); | |
| 1142 | } | |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 1143 | backend.create(&local, &saved.resources)?; |
| 1144 | if let Ok(bytes) = std::fs::read(&carried) { | |
| 1145 | backend.import_state(&bytes)?; | |
| 1146 | } | |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 1147 | let _ = std::fs::remove_dir_all(&carry); |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 1148 | if let Some(tar) = options.value("runner-image-file") { |
| 1149 | backend.load_image(Path::new(tar))?; | |
| 1150 | } | |
| 1151 | saved.image_version = Some(local.version.clone()); | |
| 1152 | if local.runner_image.is_some() { | |
| 1153 | saved.runner_image = local.runner_image.clone(); | |
| 1154 | } | |
| 1155 | backend.configure(&Settings { | |
| 1156 | runner_image: saved.runner_image.clone().unwrap_or_default(), | |
| 1157 | image_version: local.version.clone(), | |
| 1158 | cpus: saved.resources.cpus, | |
| 1159 | memory_mib: saved.resources.memory_mib, | |
| 1160 | url: saved.url.clone(), | |
| 1161 | name: Some(format!("{}-linux", base_name(saved))), | |
| 1162 | labels: saved.labels.clone(), | |
| 1163 | group: saved.group.clone(), | |
| 1164 | ..Settings::default() | |
| 1165 | })?; | |
| 1166 | if was_running { | |
| 1167 | backend.start()?; | |
| 1168 | wait_running(backend.as_ref(), Duration::from_secs(600))?; | |
| 1169 | } | |
| 1170 | image::prune(&dir.join("images"), &local.version); | |
| 1171 | if local.version == "local" { | |
| 1172 | log("The sandbox is on the image from --image-file."); | |
| 1173 | } else { | |
| 1174 | log(&format!("The sandbox is on image {}.", local.version)); | |
| 1175 | } | |
| 1176 | Ok(()) | |
| 1177 | }) | |
| 1178 | } | |
| 1179 | ||
| 1180 | #[cfg(test)] | |
| 1181 | mod tests { | |
| 1182 | use super::*; | |
| 1183 | ||
| 1184 | #[test] | |
| 1185 | fn the_default_share_leaves_the_computer_room() { | |
| 1186 | assert_eq!(default_resources(16, Some(65536)), Resources { cpus: 8, memory_mib: 8192, disk_gib: 64 }); | |
| 1187 | assert_eq!(default_resources(8, Some(16384)), Resources { cpus: 4, memory_mib: 4096, disk_gib: 64 }); | |
| 1188 | assert_eq!(default_resources(4, Some(8192)), Resources { cpus: 2, memory_mib: 2048, disk_gib: 64 }); | |
| 1189 | // Never more than half a small machine's memory. | |
| 1190 | assert_eq!(default_resources(2, Some(3072)).memory_mib, 1536); | |
| 1191 | assert_eq!(default_resources(1, None).cpus, 1); | |
| 1192 | assert_eq!(parse_gib("64g"), Some(64)); | |
| 1193 | assert_eq!(parse_gib("100000m"), Some(98)); | |
| 1194 | } | |
| 1195 | ||
| 1196 | #[test] | |
| 1197 | fn the_state_says_what_a_person_needs_to_know() { | |
| 1198 | let running = GuestStatus { state: "running".into(), runner: Some(RunnerStatus { state: "idle".into(), ..RunnerStatus::default() }), ..GuestStatus::default() }; | |
| 1199 | let unregistered = GuestStatus { state: "running".into(), ..GuestStatus::default() }; | |
| 1200 | let failed = GuestStatus { state: "error".into(), error: Some("x".into()), ..GuestStatus::default() }; | |
| 1201 | assert_eq!(linux_state(true, false, false, None, None, None), "absent"); | |
| 1202 | assert_eq!(linux_state(false, false, false, None, None, Some("WSL is not installed.")), "error"); | |
| 1203 | assert_eq!(linux_state(true, true, false, None, None, None), "stopped"); | |
| 1204 | assert_eq!(linux_state(true, true, true, None, None, None), "starting"); | |
| 1205 | assert_eq!(linux_state(true, true, true, None, Some(&running), None), "running"); | |
| 1206 | assert_eq!(linux_state(true, true, true, None, Some(&unregistered), None), "starting"); | |
| 1207 | assert_eq!(linux_state(true, true, true, None, Some(&failed), None), "error"); | |
| 1208 | assert_eq!(linux_state(true, true, true, Some("updating"), Some(&running), None), "updating"); | |
| 1209 | assert_eq!(linux_state(true, false, false, Some("starting"), None, None), "starting"); | |
| 1210 | } | |
| 1211 | ||
| 1212 | #[test] | |
| 1213 | fn the_status_contract_holds_its_fields() { | |
| 1214 | let status = Status { | |
| 1215 | schema: SCHEMA, | |
| 1216 | state: "running".into(), | |
| 1217 | backend: Some("wsl2".into()), | |
| 1218 | launcher_version: VERSION.into(), | |
| 1219 | capabilities: vec![Capability { id: "linux".into(), available: true, state: "running".into(), ..Capability::default() }], | |
| 1220 | ..Status::default() | |
| 1221 | }; | |
| 1222 | let value = serde_json::to_value(&status).unwrap(); | |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 1223 | for field in ["schema", "state", "backend", "runner_version", "launcher_version", "image_version", "isolation", "job", "error", "resources", "capabilities", "state_dir", "warnings"] { |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 1224 | assert!(value.get(field).is_some(), "{field}"); |
| 1225 | } | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 1226 | for field in ["id", "available", "state", "isolation", "runner_version", "job", "cause", "fix", "turned_off"] { |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 1227 | assert!(value["capabilities"][0].get(field).is_some(), "{field}"); |
| 1228 | } | |
| 1229 | assert_eq!(value["resources"]["memory_mib"], 0); | |
| 1230 | } | |
| 1231 | ||
| 1232 | #[test] | |
| 1233 | fn a_status_with_nothing_made_is_absent_and_says_where() { | |
| 1234 | let dir = std::env::temp_dir().join(format!("g1t-sandbox-status-{}", std::process::id())); | |
| 1235 | let saved = Saved { phase: Some("updating".into()), phase_pid: Some(u32::MAX - 1), ..Saved::default() }; | |
| 1236 | saved.save(&dir).unwrap(); | |
| 1237 | // A launcher that died mid-update leaves no phase in the status. | |
| 1238 | assert_ne!(status(&dir).state, "updating"); | |
| 1239 | assert_eq!(status(&dir).state_dir, dir.display().to_string()); | |
| 1240 | let _ = std::fs::remove_dir_all(&dir); | |
| 1241 | } | |
| 1242 | ||
| 1243 | #[test] | |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 1244 | fn only_names_what_this_computer_has() { |
| 1245 | let windows = ["linux", "windows"]; | |
| 1246 | assert_eq!(only_of("windows", &windows).unwrap(), ["windows"]); | |
| 1247 | assert_eq!(only_of("linux, windows,linux", &windows).unwrap(), ["linux", "windows"]); | |
| 1248 | assert_eq!(only_of("native", &windows).unwrap(), ["windows"]); | |
| 1249 | assert!(only_of("macos", &windows).unwrap_err().to_string().contains("linux or windows")); | |
| 1250 | assert!(only_of(",", &windows).is_err()); | |
| 1251 | // Linux has no native capability: `native` is not one of its kinds. | |
| 1252 | assert!(only_of("native", &["linux"]).unwrap_err().to_string().contains("not native")); | |
| 1253 | } | |
| 1254 | ||
| 1255 | #[test] | |
| 1256 | fn a_capability_turned_off_stays_off_and_reads_as_stopped() { | |
| 1257 | let mut saved = Saved::default(); | |
| 1258 | saved.turn("windows", false); | |
| 1259 | saved.turn("windows", false); | |
| 1260 | assert_eq!(saved.off, ["windows"]); | |
| 1261 | assert!(saved.is_off("windows") && !saved.is_off("linux")); | |
| 1262 | saved.turn("windows", true); | |
| 1263 | assert!(saved.off.is_empty()); | |
| 1264 | // Stopped by a person is not failing, and running is running whatever it says. | |
| 1265 | assert_eq!(native_state(true, false, true, true), "stopped"); | |
| 1266 | assert_eq!(native_state(true, true, true, false), "running"); | |
| 1267 | assert_eq!(native_state(true, false, false, true), "error"); | |
| 1268 | assert_eq!(native_state(true, false, false, false), "stopped"); | |
| 1269 | assert_eq!(native_state(false, false, false, false), "absent"); | |
| 1270 | } | |
| 1271 | ||
| 1272 | #[test] | |
| 1273 | fn an_earlier_no_native_is_read_as_turned_off() { | |
| 1274 | let dir = std::env::temp_dir().join(format!("g1t-sandbox-off-{}", std::process::id())); | |
| 1275 | std::fs::create_dir_all(&dir).unwrap(); | |
| 1276 | std::fs::write(dir.join("sandbox.json"), r#"{"no_native":true,"labels":[]}"#).unwrap(); | |
| 1277 | let saved = Saved::load(&dir); | |
| 1278 | assert!(!saved.no_native); | |
| 1279 | assert_eq!(saved.off, native_id().into_iter().collect::<Vec<_>>()); | |
| 1280 | saved.save(&dir).unwrap(); | |
| 1281 | let written = std::fs::read_to_string(dir.join("sandbox.json")).unwrap(); | |
| 1282 | assert!(!written.contains("no_native")); | |
| 1283 | let _ = std::fs::remove_dir_all(&dir); | |
| 1284 | } | |
| 1285 | ||
| 1286 | #[test] | |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 1287 | fn a_host_mode_runner_of_the_same_account_is_warned_of() { |
| 1288 | let root = std::env::temp_dir().join(format!("g1t-sandbox-host-{}", std::process::id())); | |
| 1289 | let registered = |name: &str, how: &str| { | |
| 1290 | let folder = root.join(name); | |
| 1291 | std::fs::create_dir_all(&folder).unwrap(); | |
| 1292 | let config = format!( | |
| 1293 | r#"{{"url":"https://g1t.sh","api":"https://api.g1t.sh","runner":"rnr_1","name":"a","credential":"g1tr_x","workspace":"acme","labels":[],"work_dir":"/tmp/w"{how}}}"# | |
| 1294 | ); | |
| 1295 | std::fs::write(folder.join("config.json"), config).unwrap(); | |
| 1296 | folder | |
| 1297 | }; | |
| 1298 | let host = registered("host", r#","docker":false"#); | |
| 1299 | let docker = registered("docker", ""); | |
| 1300 | let native = registered("native", r#","docker":false,"native":true"#); | |
| 1301 | let none = root.join("none"); | |
| 1302 | let found = host_mode_runners(&[host.clone(), docker, native, none]); | |
| 1303 | assert_eq!(found.len(), 1); | |
| 1304 | assert_eq!(found[0], host); | |
| 1305 | let said = host_mode_warning(&host); | |
| 1306 | assert!(said.contains("as you") && said.contains("g1t-runner remove --dir"), "{said}"); | |
| 1307 | // The desktop app's folder for running jobs directly is looked at. | |
| 1308 | assert!(host_runner_folders().iter().any(|f| f.ends_with(Path::new("sh.g1t.desktop").join("runner").join("host")))); | |
| 1309 | // An older status, without warnings, still reads. | |
| 1310 | let old: Status = serde_json::from_str(r#"{"schema":1,"state":"absent","backend":null,"runner_version":null,"launcher_version":"0.3.0","image_version":null,"isolation":null,"job":null,"error":null,"resources":{"cpus":0,"memory_mib":0,"disk_gib":0},"capabilities":[],"state_dir":"x"}"#).unwrap(); | |
| 1311 | assert!(old.warnings.is_empty()); | |
| 1312 | let _ = std::fs::remove_dir_all(&root); | |
| 1313 | } | |
| 1314 | ||
| 1315 | #[test] | |
| Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it. | 1316 | fn this_process_is_alive() { |
| 1317 | assert!(process_alive(std::process::id())); | |
| 1318 | assert!(!process_alive(u32::MAX - 1)); | |
| 1319 | assert!(total_memory_mib().unwrap_or(1) > 0); | |
| 1320 | } | |
| 1321 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.