Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
480 linesCodeBlameRaw

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//! Jobs native to the runner's own OS, each in a throwaway virtual machine
2//! made for it and destroyed after: `vm` isolation (sandbox.rs).
3//!
4//! - **Windows: Windows Sandbox**, through its command line (`wsb`, in
5//! Windows 11 24H2 and later). Each job gets a fresh sandbox: a Hyper-V
6//! VM with its own kernel, started from the machine's own Windows image
7//! and discarded, disk and all, when it stops. Its configuration turns
8//! off everything that reaches the machine: no GPU, no clipboard, no
9//! printers, no microphone or camera, and one folder mapped read-only,
10//! which holds only a copy of the harness and the job's script. The
11//! runner on the machine starts the sandbox, runs the script in it as
12//! SYSTEM (`wsb exec`, which waits and returns the exit code) and stops
13//! it. Windows Sandbox needs Windows 11 Pro, Enterprise or Education
14//! with the Windows Sandbox feature on; it is not in Home.
15//!
16//! Chosen over Hyper-V-isolated Windows containers, which need the
17//! Containers and Hyper-V features (Windows 11 Pro or Enterprise, and an
18//! administrator to turn them on), a container engine in Windows
19//! containers mode, and an image matching the machine's Windows build.
20//! A Windows Sandbox needs none of that, only the one feature, and its
21//! disposable VM is the same kind of wall.
22//!
23//! - **macOS: a cloned macOS VM** (designed, not run on a Mac yet). Each
24//! job gets an APFS clone of a signed macOS image, booted with Apple's
25//! Virtualization framework through `tart` with no display, no audio,
26//! no clipboard and no shared folder, and deleted after. Apple's licence
27//! allows two macOS VMs per Mac at once ([`MacVm::MAX_VMS`]); a runner
28//! runs one job at a time, so a Mac runs at most two such runners.
29
30use std::collections::BTreeMap;
31use std::path::{Path, PathBuf};
32use std::process::{Command, Stdio};
33
34use anyhow::{Context, Result, anyhow, bail};
35use serde::Serialize;
36
37use super::sandbox::{Isolation, SandboxDriver};
38
39/// Whether a kind of sandbox can be made here, and if not, why and what
40/// to do about it: what `g1t-runner sandbox status` reports for each
41/// capability, and what the desktop app shows.
42#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
43pub struct Availability {
44 pub available: bool,
45 /// Why it is not, in a sentence.
46 pub cause: Option<String>,
47 /// What the person can do about it, in a sentence.
48 pub fix: Option<String>,
49}
50
51impl Availability {
52 pub fn yes() -> Availability {
53 Availability { available: true, cause: None, fix: None }
54 }
55 pub fn no(cause: impl Into<String>, fix: impl Into<String>) -> Availability {
56 Availability { available: false, cause: Some(cause.into()), fix: Some(fix.into()) }
57 }
58}
59
60// --- Windows Sandbox --------------------------------------------------------
61
62/// Where the job's read-only folder appears inside the sandbox.
63pub const DROP_IN_SANDBOX: &str = r"C:\g1t";
64/// Where a workflow job keeps its files inside the sandbox.
65pub const WORK_IN_SANDBOX: &str = r"C:\work";
66
67/// Windows Sandbox, one per job.
68pub struct WindowsSandbox;
69
70/// What a machine has: the feature's program, and its command line.
71#[derive(Clone, Copy, Debug, Default)]
72pub struct WsbFound {
73 /// `WindowsSandbox.exe` in System32: the feature is on.
74 pub feature: bool,
75 /// `wsb.exe` answers: Windows 11 24H2 or later.
76 pub cli: bool,
77}
78
79impl WsbFound {
80 pub fn look() -> WsbFound {
81 let windir = std::env::var_os("windir").map(PathBuf::from).unwrap_or_else(|| PathBuf::from(r"C:\Windows"));
82 let feature = windir.join("System32").join("WindowsSandbox.exe").exists();
83 let cli = feature && wsb().arg("--version").stdout(Stdio::null()).stderr(Stdio::null()).status().is_ok_and(|s| s.success());
84 WsbFound { feature, cli }
85 }
86
87 pub fn availability(self) -> Availability {
88 if !cfg!(windows) {
89 return Availability::no("Windows Sandbox is only on Windows.", "Use a Windows machine for Windows jobs.");
90 }
91 if !self.feature {
92 return Availability::no(
93 "Windows Sandbox is not turned on, or this edition of Windows has none (Home does not).",
94 "On Windows 11 Pro, Enterprise or Education, turn on Windows Sandbox under Settings, System, Optional features, More Windows features, then restart.",
95 );
96 }
97 if !self.cli {
98 return Availability::no(
99 "This Windows has Windows Sandbox but not its command line, wsb, which came in Windows 11 24H2.",
100 "Update Windows to 24H2 or later.",
101 );
102 }
103 Availability::yes()
104 }
105}
106
107impl SandboxDriver for WindowsSandbox {
108 fn name(&self) -> &'static str {
109 "Windows Sandbox"
110 }
111 fn preflight(&self) -> Result<Isolation> {
112 let found = WsbFound::look().availability();
113 if found.available {
114 return Ok(Isolation::Vm);
115 }
116 bail!("{} {}", found.cause.unwrap_or_default(), found.fix.unwrap_or_default())
117 }
118}
119
120fn wsb() -> Command {
121 let mut command = Command::new("wsb");
122 hide_window(&mut command);
123 command
124}
125
126/// No console window flashes up for a command the runner runs.
127pub fn hide_window(command: &mut Command) {
128 #[cfg(windows)]
129 {
130 use std::os::windows::process::CommandExt;
131 const CREATE_NO_WINDOW: u32 = 0x0800_0000;
132 command.creation_flags(CREATE_NO_WINDOW);
133 }
134 #[cfg(not(windows))]
135 let _ = command;
136}
137
138fn xml_escape(text: &str) -> String {
139 text.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;").replace('"', "&quot;").replace('\'', "&apos;")
140}
141
142/// A job's sandbox's configuration (a `.wsb` file's XML, which `wsb start
143/// -c` takes as a string). Every redirection that reaches the machine is
144/// off; the only folder is `drop`, read-only.
145pub fn wsb_config(drop: &Path, memory_mib: u64) -> String {
146 format!(
147 "<Configuration>\
148<vGPU>Disable</vGPU>\
149<Networking>Enable</Networking>\
150<AudioInput>Disable</AudioInput>\
151<VideoInput>Disable</VideoInput>\
152<ClipboardRedirection>Disable</ClipboardRedirection>\
153<PrinterRedirection>Disable</PrinterRedirection>\
154<ProtectedClient>Enable</ProtectedClient>\
155<MemoryInMB>{memory_mib}</MemoryInMB>\
156<MappedFolders><MappedFolder><HostFolder>{}</HostFolder><SandboxFolder>{DROP_IN_SANDBOX}</SandboxFolder><ReadOnly>true</ReadOnly></MappedFolder></MappedFolders>\
157</Configuration>",
158 xml_escape(&drop.display().to_string())
159 )
160}
161
162/// A string in PowerShell's single quotes, where nothing is expanded and a
163/// quote is written twice.
164pub fn ps_quote(text: &str) -> String {
165 format!("'{}'", text.replace('\'', "''"))
166}
167
168/// The script a job runs in its sandbox: a firewall rule against the
169/// machine, its variables, then the harness. The variables are written
170/// here, in the job's read-only folder, rather than on `wsb exec`'s
171/// command line, where the machine's other programs could read them.
172///
173/// The firewall rule keeps the job from the machine's own services on the
174/// address the sandbox reaches it by (its gateway), but the job runs as
175/// SYSTEM in its sandbox and could remove it: it is a speed bump, not a
176/// wall. The wall is the VM.
177pub fn job_script(env: &BTreeMap<String, String>) -> String {
178 let mut script = String::from(
179 "$ErrorActionPreference = 'Stop'\r\n\
180$gateway = (Get-NetRoute -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue | Select-Object -First 1).NextHop\r\n\
181if ($gateway) { New-NetFirewallRule -DisplayName 'g1t: not the machine' -Direction Outbound -Action Block -Protocol TCP -RemoteAddress $gateway | Out-Null }\r\n",
182 );
183 script.push_str(&format!("New-Item -ItemType Directory -Force -Path {} | Out-Null\r\n", ps_quote(WORK_IN_SANDBOX)));
184 for (name, value) in env {
185 // Names are the harness's own (`ACTIONS_TOKEN`, `MODE`…): only
186 // letters, digits and underscores reach `$env:`.
187 if name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
188 script.push_str(&format!("${{env:{name}}} = {}\r\n", ps_quote(value)));
189 }
190 }
191 script.push_str(&format!("$env:G1T_RUNNER_ROOT = {}\r\n", ps_quote(WORK_IN_SANDBOX)));
192 script.push_str(&format!("Set-Location {}\r\n", ps_quote(WORK_IN_SANDBOX)));
193 script.push_str(&format!("& {}\r\nexit $LASTEXITCODE\r\n", ps_quote(&format!(r"{DROP_IN_SANDBOX}\g1t-runner.exe"))));
194 script
195}
196
197/// What `wsb exec` runs.
198pub fn exec_command() -> String {
199 format!(r"powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File {DROP_IN_SANDBOX}\job.ps1")
200}
201
202/// `{"ExitCode": 7}`, from `wsb exec --raw`.
203pub fn parse_exit_code(raw: &str) -> Option<i32> {
204 let start = raw.find('{')?;
205 let value: serde_json::Value = serde_json::from_str(raw[start..].trim()).ok()?;
206 value.get("ExitCode").and_then(serde_json::Value::as_i64).map(|code| code as i32)
207}
208
209/// `{"Id": "…"}`, from `wsb start --raw`.
210pub fn parse_id(raw: &str) -> Option<String> {
211 let start = raw.find('{')?;
212 let value: serde_json::Value = serde_json::from_str(raw[start..].trim()).ok()?;
213 value.get("Id").and_then(serde_json::Value::as_str).map(str::to_owned)
214}
215
216/// A job's sandbox, while it exists.
217pub struct WsbJob {
218 pub id: String,
219 /// The job's read-only folder on the machine, removed after.
220 pub drop: PathBuf,
221}
222
223impl WsbJob {
224 /// Makes the job's folder (a copy of this program, which is the
225 /// harness, and its script) and starts its sandbox.
226 pub fn start(drop: PathBuf, env: &BTreeMap<String, String>, memory_mib: u64) -> Result<WsbJob> {
227 let _ = std::fs::remove_dir_all(&drop);
228 std::fs::create_dir_all(&drop).with_context(|| format!("could not make {}", drop.display()))?;
229 let me = std::env::current_exe().context("could not find this program, the job's harness")?;
230 std::fs::copy(&me, drop.join("g1t-runner.exe")).context("could not copy the harness for the sandbox")?;
231 std::fs::write(drop.join("job.ps1"), job_script(env))?;
232 let output = wsb().args(["start", "--raw", "-c", &wsb_config(&drop, memory_mib)]).output().context("could not run wsb")?;
233 let text = String::from_utf8_lossy(&output.stdout);
234 let Some(id) = parse_id(&text).filter(|_| output.status.success()) else {
235 let _ = std::fs::remove_dir_all(&drop);
236 bail!("Windows Sandbox did not start: {} {}", text.trim(), String::from_utf8_lossy(&output.stderr).trim());
237 };
238 if let Some(parent) = drop.parent() {
239 let _ = std::fs::write(parent.join("sandbox-id"), &id);
240 }
241 Ok(WsbJob { id, drop })
242 }
243
244 /// The process that runs the job and waits for it: its standard output
245 /// carries the exit code ([`parse_exit_code`]).
246 pub fn exec(&self) -> Command {
247 let mut command = wsb();
248 command.args(["exec", "--raw", "--id", &self.id, "-c", &exec_command(), "-r", "System"]);
249 command.stdout(Stdio::piped()).stderr(Stdio::null()).stdin(Stdio::null());
250 command
251 }
252
253 /// Stops the sandbox, which discards it.
254 pub fn stop(&self) {
255 let _ = wsb().args(["stop", "--id", &self.id]).stdout(Stdio::null()).stderr(Stdio::null()).status();
256 }
257
258 pub fn clean(&self) {
259 self.stop();
260 let _ = std::fs::remove_dir_all(&self.drop);
261 if let Some(parent) = self.drop.parent() {
262 let _ = std::fs::remove_file(parent.join("sandbox-id"));
263 }
264 }
265}
266
267/// Where a runner notes the sandbox it is running, so that one, and only
268/// that one, is stopped if the runner stopped mid-job: the person may have
269/// Windows Sandboxes of their own open.
270pub fn noted(folder: &Path) -> PathBuf {
271 folder.join("native").join("sandbox-id")
272}
273
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.274/// Stops the sandbox an earlier run of this runner left, if any, and
275/// removes the job folders it left: a runner stopped mid-job (a crash, or
276/// `taskkill /F`) never cleaned its own, and a job's script holds the
277/// job's token.
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.278pub fn sweep(folder: &Path) {
279 let note = noted(folder);
280 if let Ok(id) = std::fs::read_to_string(&note) {
281 let id = id.trim();
282 if parse_list(&String::from_utf8_lossy(&wsb().args(["list", "--raw"]).output().map(|o| o.stdout).unwrap_or_default())).iter().any(|running| running == id) {
283 let _ = wsb().args(["stop", "--id", id]).stdout(Stdio::null()).stderr(Stdio::null()).status();
284 }
285 let _ = std::fs::remove_file(&note);
286 }
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.287 for left in job_folders(&folder.join("native")) {
288 let _ = std::fs::remove_dir_all(left);
289 }
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.290}
291
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.292/// The job folders in a runner's `native` folder: `g1t-<job id>`, as
293/// exec.rs names them.
294pub fn job_folders(native: &Path) -> Vec<PathBuf> {
295 let Ok(entries) = std::fs::read_dir(native) else { return Vec::new() };
296 entries
297 .flatten()
298 .filter(|entry| entry.file_type().is_ok_and(|t| t.is_dir()) && entry.file_name().to_string_lossy().starts_with("g1t-"))
299 .map(|entry| entry.path())
300 .collect()
301}
302
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.303/// The ids in `wsb list --raw`.
304pub fn parse_list(raw: &str) -> Vec<String> {
305 let Some(start) = raw.find('{') else { return Vec::new() };
306 let Ok(value) = serde_json::from_str::<serde_json::Value>(raw[start..].trim()) else { return Vec::new() };
307 value
308 .get("WindowsSandboxEnvironments")
309 .and_then(serde_json::Value::as_array)
310 .map(|all| all.iter().filter_map(|e| e.get("Id").and_then(serde_json::Value::as_str).map(str::to_owned)).collect())
311 .unwrap_or_default()
312}
313
314// --- macOS VMs (designed) -----------------------------------------------------
315
316/// A cloned macOS VM per job, through `tart` on Apple's Virtualization
317/// framework. **Designed, not run on a Mac yet:** its commands are built
318/// and tested here, and [`SandboxDriver::preflight`] says it is not
319/// available, so no Mac registers as able to take macOS jobs until it has
320/// been run on one.
321///
322/// - The image is an OCI image of a macOS VM, named by digest in the
323/// signed release manifest, so `tart clone` fetches exactly that.
324/// - `tart run --no-graphics --no-audio --no-clipboard`, with no `--dir`:
325/// no display, no sound, no clipboard, no folder of the Mac's.
326/// - The job runs through `tart exec` (the guest agent in the image), and
327/// the VM is stopped and deleted after.
328pub struct MacVm;
329
330// The commands are built and tested ahead of the driver that runs them.
331#[allow(dead_code)]
332impl MacVm {
333 /// Apple's macOS licence: two virtual copies per Mac.
334 pub const MAX_VMS: u32 = 2;
335
336 pub fn clone_args(image: &str, name: &str) -> Vec<String> {
337 vec!["clone".into(), image.into(), name.into()]
338 }
339
340 pub fn run_args(name: &str) -> Vec<String> {
341 vec!["run".into(), "--no-graphics".into(), "--no-audio".into(), "--no-clipboard".into(), name.into()]
342 }
343
344 pub fn exec_args(name: &str, script: &str) -> Vec<String> {
345 vec!["exec".into(), name.into(), "/bin/bash".into(), "-lc".into(), script.into()]
346 }
347
348 pub fn delete_args(name: &str) -> Vec<String> {
349 vec!["delete".into(), name.into()]
350 }
351
352 pub fn availability() -> Availability {
353 if !cfg!(target_os = "macos") {
354 return Availability::no("macOS VMs are only on a Mac.", "Use a Mac for macOS jobs.");
355 }
356 Availability::no(
357 "Sandboxed macOS jobs are not released yet.",
358 "Run macOS jobs on a runner registered with --no-docker (host mode, unsandboxed) for now, or wait for a release that has them.",
359 )
360 }
361}
362
363impl SandboxDriver for MacVm {
364 fn name(&self) -> &'static str {
365 "a cloned macOS VM per job"
366 }
367 fn preflight(&self) -> Result<Isolation> {
368 let why = MacVm::availability();
369 Err(anyhow!("{} {}", why.cause.unwrap_or_default(), why.fix.unwrap_or_default()))
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376
377 #[test]
378 fn a_job_sandbox_reaches_nothing_of_the_machines_but_its_read_only_folder() {
379 let xml = wsb_config(Path::new(r"C:\Users\a & b\.g1t-runner\native\job_1"), 4096);
380 for off in ["vGPU", "AudioInput", "VideoInput", "ClipboardRedirection", "PrinterRedirection"] {
381 assert!(xml.contains(&format!("<{off}>Disable</{off}>")), "{off} in {xml}");
382 }
383 assert!(xml.contains("<ProtectedClient>Enable</ProtectedClient>"));
384 assert_eq!(xml.matches("<MappedFolder>").count(), 1);
385 assert!(xml.contains("<ReadOnly>true</ReadOnly>"));
386 assert!(xml.contains(r"<HostFolder>C:\Users\a &amp; b\.g1t-runner\native\job_1</HostFolder>"));
387 assert!(xml.contains("<MemoryInMB>4096</MemoryInMB>"));
388 assert!(!xml.contains("LogonCommand"), "the job runs through wsb exec, not at logon");
389 }
390
391 #[test]
392 fn the_job_script_quotes_every_value() {
393 let mut env = BTreeMap::new();
394 env.insert("ACTIONS_TOKEN".to_owned(), "it's $(evil) `x`".to_owned());
395 env.insert("MODE".to_owned(), "actions".to_owned());
396 env.insert("BAD NAME;rm".to_owned(), "x".to_owned());
397 let script = job_script(&env);
398 assert!(script.contains("${env:ACTIONS_TOKEN} = 'it''s $(evil) `x`'"), "{script}");
399 assert!(script.contains("${env:MODE} = 'actions'"));
400 assert!(!script.contains("BAD NAME"));
401 assert!(script.contains(r"$env:G1T_RUNNER_ROOT = 'C:\work'"));
402 assert!(script.ends_with("& 'C:\\g1t\\g1t-runner.exe'\r\nexit $LASTEXITCODE\r\n"));
403 assert!(script.contains("New-NetFirewallRule"));
404 }
405
406 #[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.407 fn a_runner_stopped_mid_job_has_its_job_folders_swept() {
408 let folder = std::env::temp_dir().join(format!("g1t-native-sweep-{}", std::process::id()));
409 let native = folder.join("native");
410 std::fs::create_dir_all(native.join("g1t-job_1")).unwrap();
411 std::fs::write(native.join("g1t-job_1").join("job.ps1"), "${env:ACTIONS_TOKEN} = 't'").unwrap();
412 std::fs::create_dir_all(native.join("native")).unwrap();
413 std::fs::write(native.join("runner.log"), "").unwrap();
414 assert_eq!(job_folders(&native), [native.join("g1t-job_1")]);
415 // No sandbox was noted, so nothing is stopped; the job's folder goes.
416 sweep(&folder);
417 assert!(!native.join("g1t-job_1").exists());
418 assert!(native.join("native").exists() && native.join("runner.log").exists(), "only job folders");
419 let _ = std::fs::remove_dir_all(&folder);
420 }
421
422 #[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.423 fn wsb_output_reads() {
424 assert_eq!(parse_exit_code("{\n \"ExitCode\": 7\n}\n"), Some(7));
425 assert_eq!(parse_exit_code("nothing"), None);
426 assert_eq!(parse_id("{\n \"Id\": \"902c8565-4b36\"\n}"), Some("902c8565-4b36".into()));
427 assert_eq!(parse_list("{\"WindowsSandboxEnvironments\": [{\"Id\": \"a\"}, {\"Id\": \"b\"}]}"), vec!["a", "b"]);
428 assert!(parse_list("{\"WindowsSandboxEnvironments\": []}").is_empty());
429 assert!(exec_command().ends_with(r"-File C:\g1t\job.ps1"));
430 }
431
432 #[test]
433 fn what_is_missing_says_how_to_fix_it() {
434 if cfg!(windows) {
435 let off = WsbFound { feature: false, cli: false }.availability();
436 assert!(!off.available);
437 assert!(off.cause.unwrap().contains("not turned on"));
438 assert!(off.fix.unwrap().contains("Pro, Enterprise or Education"));
439 assert!(WsbFound { feature: true, cli: false }.availability().cause.unwrap().contains("24H2"));
440 assert!(WsbFound { feature: true, cli: true }.availability().available);
441 } else {
442 assert!(!WsbFound { feature: true, cli: true }.availability().available);
443 }
444 }
445
446 #[test]
447 fn a_macos_vm_shares_nothing_and_is_not_offered_until_run() {
448 let run = MacVm::run_args("g1t-job_1");
449 assert!(run.iter().any(|a| a == "--no-graphics") && run.iter().any(|a| a == "--no-clipboard"));
450 assert!(!run.iter().any(|a| a.starts_with("--dir")), "no folder of the Mac's");
451 assert_eq!(MacVm::clone_args("g1t.sh/flagon-io/macos@sha256:ab", "g1t-job_1"), vec!["clone", "g1t.sh/flagon-io/macos@sha256:ab", "g1t-job_1"]);
452 assert_eq!(MacVm::delete_args("x"), vec!["delete", "x"]);
453 assert_eq!(MacVm::exec_args("x", "echo hi")[0], "exec");
454 assert!(MacVm.preflight().is_err());
455 assert_eq!(MacVm::MAX_VMS, 2);
456 }
457
458 /// Starts a real Windows Sandbox: run with `cargo test -p g1t-runner -- --ignored windows_sandbox`
459 /// on a machine with the feature on.
460 #[test]
461 #[ignore]
462 fn windows_sandbox_runs_a_script_and_keeps_its_folder_read_only() {
463 let drop = std::env::temp_dir().join(format!("g1t-wsb-test-{}", std::process::id()));
464 let _ = std::fs::remove_dir_all(&drop);
465 std::fs::create_dir_all(&drop).unwrap();
466 std::fs::write(
467 drop.join("job.ps1"),
468 "try { 'x' | Out-File C:\\g1t\\written.txt -ErrorAction Stop; exit 3 } catch { }\r\nif (Test-Path 'C:\\Users\\Public\\g1t-from-host') { exit 4 }\r\nexit 7\r\n",
469 )
470 .unwrap();
471 let output = wsb().args(["start", "--raw", "-c", &wsb_config(&drop, 2048)]).output().unwrap();
472 let id = parse_id(&String::from_utf8_lossy(&output.stdout)).expect("a sandbox id");
473 let job = WsbJob { id, drop: drop.clone() };
474 let out = job.exec().output().unwrap();
475 let code = parse_exit_code(&String::from_utf8_lossy(&out.stdout));
476 job.clean();
477 assert_eq!(code, Some(7), "the script ran, and could not write to its folder");
478 assert!(!drop.exists());
479 }
480}

This file's history is long; its oldest lines are credited to the oldest commit read.