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.

627 linesCodeBlameRaw
1//! What a computer still lacks before a kind of job can run sandboxed,
2//! each by name (`status --json`'s `needs`), found before anything is
3//! downloaded: the desktop app says each in plain words and sets up those
4//! it can, behind one administrator prompt.
5//!
6//! | Need | Kind of job | What is missing | Set up by the app |
7//! | --- | --- | --- | --- |
8//! | `restart` | Linux, Windows | Windows features were turned on, and take effect after a restart | No: Restart now |
9//! | `windows_update` | Linux, Windows | Windows older than 2004 (WSL 2), or than 11 24H2 (`wsb`) | No: Windows Update |
10//! | `virtualization` | all | Virtualization (VT-x, AMD-V) off in the firmware | No: steps |
11//! | `nested_virtualization` | all | A VM whose host doesn't pass virtualization through | No: steps |
12//! | `vm_platform` | Linux on Windows | The Virtual Machine Platform feature | Yes |
13//! | `wsl` | Linux on Windows | WSL | Yes |
14//! | `wsl_update` | Linux on Windows | WSL older than 2.0, or the one built into Windows | Yes |
15//! | `windows_edition` | Windows | Windows Home, which has no Windows Sandbox | No: Linux jobs still run |
16//! | `windows_sandbox` | Windows | The Windows Sandbox feature | Yes |
17//! | `kvm_module` | Linux on Linux | `/dev/kvm`: its module isn't loaded | No: command |
18//! | `kvm_group` | Linux on Linux | Permission to use `/dev/kvm` | No: command |
19//! | `qemu` | Linux on Linux | QEMU | No: command |
20//! | `macos_update` | Linux on macOS | macOS older than 13 | No: Software Update |
21//! | `homebrew` | Linux on macOS | Homebrew, which installs vfkit | No: steps |
22//! | `vfkit` | Linux on macOS | vfkit | No: command |
23//!
24//! Each is said only when the facts say it for certain: a computer that
25//! can run the sandbox is never held back by a guess. What can't be known
26//! beforehand (virtualization inside a VM) is named from the error that
27//! starting the sandbox gives instead.
28
29use std::collections::BTreeMap;
30use std::path::Path;
31use std::process::{Command, Stdio};
32
33use serde::{Deserialize, Serialize};
34
35/// The file the desktop app's setup leaves in the launcher's state folder
36/// when what it turned on takes a restart: this boot's time, as
37/// [`WindowsFacts::booted`] says it.
38pub const RESTART_FILE: &str = "restart-pending";
39
40/// The oldest Windows build with WSL 2 (Windows 10 2004).
41pub const WSL2_BUILD: u32 = 19041;
42/// The oldest WSL the sandbox takes: the one from the Microsoft Store, 2.0 or later.
43pub const WSL_VERSION: (u32, u32, u32) = (2, 0, 0);
44/// The oldest macOS vfkit runs on.
45pub const MACOS_MAJOR: u32 = 13;
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "snake_case")]
49pub enum Need {
50 Restart,
51 WindowsUpdate,
52 Virtualization,
53 NestedVirtualization,
54 VmPlatform,
55 Wsl,
56 WslUpdate,
57 WindowsEdition,
58 WindowsSandbox,
59 KvmModule,
60 KvmGroup,
61 Qemu,
62 MacosUpdate,
63 Homebrew,
64 Vfkit,
65}
66
67impl Need {
68 /// Why, and what to do, in the launcher's words: what `status` says
69 /// as a capability's `cause` and `fix`. The first sentences of those a
70 /// desktop app from before `needs` reads are kept as they were.
71 pub fn said(self, kind: &str) -> (String, String) {
72 let (cause, fix) = match (self, kind) {
73 (Need::Restart, _) => (
74 "Windows needs a restart to finish turning on what the sandbox needs.",
75 "Restart Windows; the sandbox starts after.",
76 ),
77 (Need::WindowsUpdate, "windows") => (
78 "This Windows has Windows Sandbox but not its command line, wsb, which came in Windows 11 24H2.",
79 "Update Windows to 24H2 or later.",
80 ),
81 (Need::WindowsUpdate, _) => (
82 "This Windows is older than Windows 10 version 2004, the first with WSL 2.",
83 "Update Windows under Settings, Windows Update, then restart.",
84 ),
85 (Need::Virtualization, "linux") if cfg!(target_os = "linux") => (
86 "This machine has neither Sysbox nor KVM: virtualization (Intel VT-x or AMD-V) is off in its firmware, or its processor has none.",
87 "Turn on Intel VT-x or AMD-V in the firmware (BIOS or UEFI) settings and restart, or install Sysbox 0.7 or later.",
88 ),
89 (Need::Virtualization, _) => (
90 "Virtualization (Intel VT-x or AMD-V) is turned off in this computer's firmware, and the sandbox is a small virtual machine.",
91 "Restart into the firmware (BIOS or UEFI) settings, turn on Intel Virtualization Technology (VT-x) or SVM Mode (AMD-V), save and restart.",
92 ),
93 (Need::NestedVirtualization, _) => (
94 "This computer is a virtual machine whose host doesn't pass virtualization through to it, and the sandbox is a virtual machine inside it.",
95 "Turn on nested virtualization for this VM on its host, then start it again: on Hyper-V, `Set-VMProcessor -VMName <name> -ExposeVirtualizationExtensions $true` with the VM off.",
96 ),
97 (Need::VmPlatform, _) => (
98 "Virtual Machine Platform, the Windows feature WSL 2 runs on, is turned off.",
99 "Open a terminal as administrator and run `wsl --install --no-distribution`, which turns it on, then restart Windows.",
100 ),
101 (Need::Wsl, _) => (
102 "WSL is not installed, and the sandbox for Linux jobs is a WSL distribution of its own.",
103 "Open a terminal as administrator and run `wsl --install --no-distribution`, then restart Windows. It works on every edition of Windows 11, Home included.",
104 ),
105 (Need::WslUpdate, _) => (
106 "This WSL is older than the one the sandbox needs: WSL 2.0 or later, from the Microsoft Store.",
107 "Open a terminal as administrator and run `wsl --update`.",
108 ),
109 (Need::WindowsEdition, _) => (
110 "Windows Sandbox, which Windows jobs run in, isn't in Windows Home.",
111 "Linux jobs run on every edition. Windows jobs need Windows 11 Pro, Enterprise or Education.",
112 ),
113 (Need::WindowsSandbox, _) => (
114 "Windows Sandbox is not turned on, and Windows jobs each run in one.",
115 "Turn on Windows Sandbox under Settings, System, Optional features, More Windows features, then restart.",
116 ),
117 (Need::KvmModule, _) => (
118 "This machine has neither Sysbox nor KVM: its processor can run virtual machines, but /dev/kvm isn't there.",
119 "Load KVM with `sudo modprobe kvm_intel` (or `kvm_amd`); if it says the firmware disabled it, turn on VT-x or AMD-V there. Or install Sysbox 0.7 or later.",
120 ),
121 (Need::KvmGroup, _) => ("This user may not use /dev/kvm.", "Run `sudo usermod -aG kvm $USER`, then log out and in again."),
122 (Need::Qemu, _) => (
123 "QEMU, which runs the sandbox's VM, is not installed.",
124 "Install QEMU (qemu-system-x86 on Debian and Ubuntu, qemu-kvm on Fedora), or Sysbox 0.7 or later.",
125 ),
126 (Need::MacosUpdate, _) => ("The sandbox's VM needs macOS 13 or later.", "Update macOS under System Settings, General, Software Update."),
127 (Need::Homebrew, _) => (
128 "vfkit, which runs the sandbox's VM with Apple's Virtualization framework, is not installed, nor is Homebrew, which installs it.",
129 "Install Homebrew (brew.sh), then run `brew install vfkit`.",
130 ),
131 (Need::Vfkit, _) => (
132 "vfkit, which runs the sandbox's VM with Apple's Virtualization framework, is not installed.",
133 "Install it with `brew install vfkit` (macOS 13 or later), then start the sandbox again.",
134 ),
135 };
136 (cause.to_owned(), fix.to_owned())
137 }
138}
139
140// --- Windows ------------------------------------------------------------------------
141
142/// What Windows says of itself, read without an administrator. Each is
143/// None (or empty) when it couldn't be read, and nothing is concluded from it.
144#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
145#[serde(default)]
146pub struct WindowsFacts {
147 /// `EditionID`: `Core` (Home), `Professional`, `Enterprise`, …
148 pub edition: Option<String>,
149 pub build: Option<u32>,
150 /// A hypervisor is running (`Win32_ComputerSystem.HypervisorPresent`).
151 pub hypervisor: Option<bool>,
152 /// Virtualization on in the firmware (`Win32_Processor`). It reads
153 /// false whenever a hypervisor is running, so it says something only
154 /// when none is.
155 pub firmware: Option<bool>,
156 pub model: Option<String>,
157 pub manufacturer: Option<String>,
158 /// `Win32_OptionalFeature.InstallState`: 1 on, 2 off, 3 not in this edition.
159 pub features: BTreeMap<String, u32>,
160 /// When Windows started, in UTC.
161 pub booted: Option<String>,
162}
163
164pub const VM_PLATFORM: &str = "VirtualMachinePlatform";
165pub const WINDOWS_SANDBOX: &str = "Containers-DisposableClientVM";
166
167/// The facts, as one PowerShell call prints them: about a second, so only
168/// asked while something is missing.
169const PROBE: &str = r#"$ErrorActionPreference = 'SilentlyContinue'
170$v = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
171$cs = Get-CimInstance Win32_ComputerSystem
172$cpu = Get-CimInstance Win32_Processor | Select-Object -First 1
173$features = @{}
174Get-CimInstance Win32_OptionalFeature | Where-Object { $_.Name -in @('VirtualMachinePlatform', 'Containers-DisposableClientVM') } | ForEach-Object { $features[$_.Name] = [int]$_.InstallState }
175$os = Get-CimInstance Win32_OperatingSystem
176[pscustomobject]@{
177 edition = $v.EditionID
178 build = [int]$v.CurrentBuildNumber
179 hypervisor = $cs.HypervisorPresent
180 firmware = $cpu.VirtualizationFirmwareEnabled
181 model = $cs.Model
182 manufacturer = $cs.Manufacturer
183 features = $features
184 booted = $os.LastBootUpTime.ToUniversalTime().ToString('o')
185} | ConvertTo-Json -Compress"#;
186
187/// PowerShell's `-EncodedCommand`: a script as UTF-16LE in base64, so
188/// nothing in it is read by a shell on the way.
189pub fn encoded(script: &str) -> String {
190 use base64::Engine;
191 let bytes: Vec<u8> = script.encode_utf16().flat_map(u16::to_le_bytes).collect();
192 base64::engine::general_purpose::STANDARD.encode(bytes)
193}
194
195impl WindowsFacts {
196 /// Asks Windows, once a process: what it can't say is left unknown.
197 pub fn look() -> WindowsFacts {
198 static FACTS: std::sync::OnceLock<WindowsFacts> = std::sync::OnceLock::new();
199 FACTS
200 .get_or_init(|| {
201 let mut command = Command::new("powershell.exe");
202 command.args(["-NoProfile", "-NonInteractive", "-EncodedCommand", &encoded(PROBE)]).stdin(Stdio::null()).stderr(Stdio::null());
203 super::native::hide_window(&mut command);
204 command.output().ok().and_then(|out| serde_json::from_slice(&out.stdout).ok()).unwrap_or_default()
205 })
206 .clone()
207 }
208
209 /// [`look`](Self::look), kept in the launcher's state folder until
210 /// Windows restarts: what it says (the edition, the processor, a
211 /// hypervisor running, a feature turned on) changes only across a
212 /// restart, and asking takes a second or two, which a status every
213 /// few seconds shouldn't.
214 pub fn look_in(dir: &Path) -> WindowsFacts {
215 let path = dir.join(FACTS_FILE);
216 let Some(boot) = booted_at() else { return WindowsFacts::look() };
217 let kept: Option<Kept> = std::fs::read(&path).ok().and_then(|bytes| serde_json::from_slice(&bytes).ok());
218 if let Some(kept) = kept.filter(|kept| same_boot(kept.boot, boot)) {
219 return kept.facts;
220 }
221 let facts = WindowsFacts::look();
222 // Kept only when Windows answered.
223 if facts.booted.is_some() {
224 let _ = std::fs::create_dir_all(dir);
225 let _ = std::fs::write(&path, serde_json::to_vec(&Kept { boot, facts: facts.clone() }).unwrap_or_default());
226 }
227 facts
228 }
229
230 /// Windows Home (`Core`, `CoreN`, `CoreSingleLanguage`, `CoreCountrySpecific`).
231 pub fn home(&self) -> bool {
232 self.edition.as_deref().is_some_and(|e| e.starts_with("Core"))
233 }
234
235 /// This Windows is itself a virtual machine.
236 pub fn virtual_machine(&self) -> bool {
237 let said = format!("{} {}", self.model.as_deref().unwrap_or(""), self.manufacturer.as_deref().unwrap_or("")).to_ascii_lowercase();
238 ["virtual machine", "vmware", "virtualbox", "qemu", "kvm", "parallels", "xen", "bochs"].iter().any(|vm| said.contains(vm))
239 }
240
241 /// A feature on (Some(true)), off or not here (Some(false)), or not known.
242 pub fn feature(&self, name: &str) -> Option<bool> {
243 self.features.get(name).map(|state| *state == 1)
244 }
245
246 /// Virtualization is off in the firmware, for certain: a physical
247 /// computer with no hypervisor running whose processor says so.
248 pub fn firmware_off(&self) -> bool {
249 !self.virtual_machine() && self.hypervisor == Some(false) && self.firmware == Some(false)
250 }
251
252 /// What the computer lacks for virtualization, said from an error
253 /// that starting a VM gave: off in the firmware, or not passed through to this VM.
254 pub fn no_virtualization(&self) -> Need {
255 if self.virtual_machine() { Need::NestedVirtualization } else { Need::Virtualization }
256 }
257}
258
259/// The file [`WindowsFacts::look_in`] keeps them in.
260pub const FACTS_FILE: &str = "windows-facts.json";
261
262#[derive(Serialize, Deserialize)]
263struct Kept {
264 /// When Windows started, in seconds since 1970.
265 boot: u64,
266 facts: WindowsFacts,
267}
268
269/// Two readings of when Windows started are the same boot: they are
270/// worked out from the time now and the time since, so differ by a second or so.
271fn same_boot(a: u64, b: u64) -> bool {
272 a.abs_diff(b) <= 30
273}
274
275/// When Windows started, in seconds since 1970: now, less how long it has been running.
276fn booted_at() -> Option<u64> {
277 #[cfg(windows)]
278 {
279 unsafe extern "system" {
280 fn GetTickCount64() -> u64;
281 }
282 // SAFETY: GetTickCount64 takes nothing and only reads a counter.
283 let up = unsafe { GetTickCount64() } / 1000;
284 let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).ok()?.as_secs();
285 Some(now.saturating_sub(up))
286 }
287 #[cfg(not(windows))]
288 None
289}
290
291/// Whether setup left a restart to do, this boot: the file setup wrote
292/// names the boot it ran in. One from an earlier boot is removed.
293pub fn restart_pending(dir: &Path, facts: &WindowsFacts) -> bool {
294 let path = dir.join(RESTART_FILE);
295 let Ok(said) = std::fs::read_to_string(&path) else { return false };
296 match facts.booted.as_deref() {
297 Some(booted) if said.trim() == booted => true,
298 Some(_) => {
299 let _ = std::fs::remove_file(&path);
300 false
301 }
302 // Not known: a restart is still to be done until Windows says otherwise.
303 None => true,
304 }
305}
306
307/// What WSL says of itself.
308#[derive(Clone, Debug, Default, PartialEq, Eq)]
309pub struct WslFacts {
310 /// `wsl --status` answers.
311 pub installed: bool,
312 /// `wsl --version`'s, which the WSL built into Windows doesn't answer.
313 pub version: Option<(u32, u32, u32)>,
314 /// A WSL 2 distribution is here already: WSL 2 itself works.
315 pub has_wsl2: bool,
316}
317
318/// The first `a.b.c` in `wsl --version`'s first line, in any language:
319/// `WSL version: 2.5.9.0`, `WSL-Version: 2.5.9.0`.
320pub fn wsl_version(text: &str) -> Option<(u32, u32, u32)> {
321 let line = text.replace('\0', "");
322 let first = line.lines().find(|l| !l.trim().is_empty())?.to_owned();
323 first.split(|c: char| !(c.is_ascii_digit() || c == '.')).find_map(|word| {
324 let mut parts = word.split('.').map(|p| p.parse::<u32>().ok());
325 match (parts.next().flatten(), parts.next().flatten(), parts.next().flatten()) {
326 (Some(a), Some(b), Some(c)) => Some((a, b, c)),
327 _ => None,
328 }
329 })
330}
331
332/// What Linux jobs on Windows still need, first what must come first.
333pub fn wsl_needs(win: &WindowsFacts, wsl: &WslFacts, restart: bool) -> Vec<Need> {
334 let mut needs = Vec::new();
335 if win.build.is_some_and(|b| b < WSL2_BUILD) {
336 return vec![Need::WindowsUpdate];
337 }
338 // A WSL 2 distribution that runs says virtualization and the platform are there.
339 if !wsl.has_wsl2 {
340 if win.firmware_off() && !restart {
341 return vec![Need::Virtualization];
342 }
343 if win.feature(VM_PLATFORM) == Some(false) {
344 needs.push(Need::VmPlatform);
345 }
346 }
347 if !wsl.installed {
348 needs.push(Need::Wsl);
349 } else if wsl.version.is_none_or(|v| v < WSL_VERSION) {
350 needs.push(Need::WslUpdate);
351 }
352 if restart && (needs.iter().any(|n| matches!(n, Need::VmPlatform)) || win.hypervisor == Some(false)) {
353 // What is left to set up after it is said once it's done.
354 return vec![Need::Restart];
355 }
356 needs
357}
358
359/// What Windows jobs still need: Windows Sandbox (`feature`, its program
360/// is here) and its command line (`cli`).
361pub fn windows_sandbox_needs(win: &WindowsFacts, feature: bool, cli: bool, restart: bool) -> Vec<Need> {
362 if feature && cli {
363 return vec![];
364 }
365 if feature {
366 return vec![Need::WindowsUpdate];
367 }
368 if win.home() || (win.feature(WINDOWS_SANDBOX).is_none() && win.features.contains_key(VM_PLATFORM)) {
369 // Home, or an edition whose feature list has no Windows Sandbox.
370 return vec![Need::WindowsEdition];
371 }
372 if restart {
373 return vec![Need::Restart];
374 }
375 if win.firmware_off() {
376 return vec![Need::Virtualization];
377 }
378 let mut needs = vec![Need::WindowsSandbox];
379 if win.build.is_some_and(|b| b < 26100) {
380 needs.push(Need::WindowsUpdate);
381 }
382 needs
383}
384
385/// Whether a sentence about a VM that wouldn't start is about virtualization.
386pub fn about_virtualization(text: &str) -> bool {
387 let lower = text.to_ascii_lowercase();
388 ["virtualization is not enabled", "enable virtualization", "ensure virtualization", "hcs_e_hyperv_not_installed", "hypervisor is not present", "vt-x", "nested virtualization"]
389 .iter()
390 .any(|said| lower.contains(said))
391}
392
393// --- Linux --------------------------------------------------------------------------
394
395/// What a Linux computer has for a QEMU VM with KVM.
396#[derive(Clone, Debug, Default, PartialEq, Eq)]
397pub struct KvmFacts {
398 /// `/dev/kvm` is there.
399 pub device: bool,
400 /// This user may open it.
401 pub usable: bool,
402 /// The processor says it can virtualize (`vmx`, `svm`): None on an
403 /// architecture whose flags don't say.
404 pub cpu: Option<bool>,
405 /// This Linux is itself a virtual machine (`hypervisor`).
406 pub guest: bool,
407 pub qemu: bool,
408}
409
410/// `/proc/cpuinfo`'s flags: whether the processor can virtualize, and
411/// whether this is a guest. On x86 only; elsewhere it says nothing.
412pub fn cpu_flags(cpuinfo: &str) -> (Option<bool>, bool) {
413 let Some(flags) = cpuinfo.lines().find(|l| l.starts_with("flags")).and_then(|l| l.split_once(':')).map(|(_, f)| f) else {
414 return (None, false);
415 };
416 let has = |flag: &str| flags.split_whitespace().any(|f| f == flag);
417 (Some(has("vmx") || has("svm")), has("hypervisor"))
418}
419
420pub fn kvm_needs(facts: &KvmFacts) -> Vec<Need> {
421 let mut needs = Vec::new();
422 if !facts.device {
423 needs.push(match facts.cpu {
424 Some(false) if facts.guest => Need::NestedVirtualization,
425 Some(false) => Need::Virtualization,
426 _ => Need::KvmModule,
427 });
428 } else if !facts.usable {
429 needs.push(Need::KvmGroup);
430 }
431 if !facts.qemu {
432 needs.push(Need::Qemu);
433 }
434 needs
435}
436
437// --- macOS --------------------------------------------------------------------------
438
439/// `sw_vers -productVersion`'s major version: `14.6.1` is 14.
440pub fn macos_major(text: &str) -> Option<u32> {
441 text.trim().split('.').next()?.parse().ok()
442}
443
444pub fn vfkit_needs(macos: Option<u32>, vfkit: bool, homebrew: bool) -> Vec<Need> {
445 if macos.is_some_and(|major| major < MACOS_MAJOR) {
446 return vec![Need::MacosUpdate];
447 }
448 match (vfkit, homebrew) {
449 (true, _) => vec![],
450 (false, true) => vec![Need::Vfkit],
451 (false, false) => vec![Need::Homebrew],
452 }
453}
454
455/// Where Homebrew puts programs, which a program a Mac app starts doesn't
456/// have on its PATH: Apple silicon's, then Intel's.
457pub const HOMEBREW_BINS: [&str; 2] = ["/opt/homebrew/bin", "/usr/local/bin"];
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462
463 /// What the probe printed on a Windows 11 Pro desktop with Hyper-V running.
464 const PRO: &str = r#"{"edition":"Professional","build":26220,"hypervisor":true,"firmware":false,"model":"MS-7D25","manufacturer":"Micro-Star International Co., Ltd.","features":{"VirtualMachinePlatform":1,"Containers-DisposableClientVM":1},"booted":"2026-10-14T20:17:29.5000000Z"}"#;
465
466 fn facts(json: &str) -> WindowsFacts {
467 serde_json::from_str(json).unwrap()
468 }
469
470 /// A clean Windows 11 Home laptop: nothing turned on, virtualization on in its firmware.
471 fn clean_home() -> WindowsFacts {
472 facts(r#"{"edition":"Core","build":26100,"hypervisor":false,"firmware":true,"model":"XPS 13","manufacturer":"Dell Inc.","features":{"VirtualMachinePlatform":2},"booted":"2026-10-14T08:00:00Z"}"#)
473 }
474
475 fn wsl(installed: bool, version: Option<(u32, u32, u32)>, has_wsl2: bool) -> WslFacts {
476 WslFacts { installed, version, has_wsl2 }
477 }
478
479 #[test]
480 fn a_computer_that_has_everything_needs_nothing() {
481 let pro = facts(PRO);
482 assert!(!pro.home() && !pro.virtual_machine() && !pro.firmware_off());
483 // Hyper-V running: firmware reads false, and means nothing.
484 assert_eq!(wsl_needs(&pro, &wsl(true, Some((2, 5, 9)), true), false), vec![]);
485 assert_eq!(wsl_needs(&pro, &wsl(true, Some((2, 5, 9)), false), false), vec![]);
486 assert_eq!(windows_sandbox_needs(&pro, true, true, false), vec![]);
487 }
488
489 #[test]
490 fn a_clean_computer_needs_the_platform_and_wsl_set_up() {
491 let home = clean_home();
492 assert_eq!(wsl_needs(&home, &wsl(false, None, false), false), vec![Need::VmPlatform, Need::Wsl]);
493 // The WSL built into Windows answers --status but not --version.
494 assert_eq!(wsl_needs(&home, &wsl(true, None, false), false), vec![Need::VmPlatform, Need::WslUpdate]);
495 assert_eq!(wsl_needs(&home, &wsl(true, Some((1, 2, 5)), false), false), vec![Need::VmPlatform, Need::WslUpdate]);
496 // Home has no Windows Sandbox, and that's said as such, not as "off".
497 assert_eq!(windows_sandbox_needs(&home, false, false, false), vec![Need::WindowsEdition]);
498 }
499
500 #[test]
501 fn after_setup_it_says_restart_until_windows_has_restarted() {
502 let home = clean_home();
503 assert_eq!(wsl_needs(&home, &wsl(true, Some((2, 5, 9)), false), true), vec![Need::Restart]);
504 let pro = WindowsFacts { edition: Some("Professional".into()), features: [(VM_PLATFORM.to_owned(), 1), (WINDOWS_SANDBOX.to_owned(), 2)].into(), ..clean_home() };
505 assert_eq!(windows_sandbox_needs(&pro, false, false, true), vec![Need::Restart]);
506 // Restarted, with everything there: nothing.
507 let after = WindowsFacts { hypervisor: Some(true), firmware: Some(false), features: [(VM_PLATFORM.to_owned(), 1)].into(), ..clean_home() };
508 assert_eq!(wsl_needs(&after, &wsl(true, Some((2, 5, 9)), false), false), vec![]);
509 // The file names the boot it was written in; a later boot's facts remove it.
510 let dir = std::env::temp_dir().join(format!("g1t-needs-restart-{}", std::process::id()));
511 std::fs::create_dir_all(&dir).unwrap();
512 std::fs::write(dir.join(RESTART_FILE), "2026-10-14T08:00:00Z\r\n").unwrap();
513 assert!(restart_pending(&dir, &home));
514 let rebooted = WindowsFacts { booted: Some("2026-10-14T09:30:00Z".into()), ..clean_home() };
515 assert!(!restart_pending(&dir, &rebooted));
516 assert!(!dir.join(RESTART_FILE).exists());
517 let _ = std::fs::remove_dir_all(&dir);
518 }
519
520 #[test]
521 fn virtualization_off_in_the_firmware_comes_first_and_is_never_guessed() {
522 let off = WindowsFacts { firmware: Some(false), ..clean_home() };
523 assert!(off.firmware_off());
524 assert_eq!(wsl_needs(&off, &wsl(false, None, false), false), vec![Need::Virtualization]);
525 let pro = WindowsFacts { edition: Some("Professional".into()), features: [(VM_PLATFORM.to_owned(), 2), (WINDOWS_SANDBOX.to_owned(), 2)].into(), ..off.clone() };
526 assert_eq!(windows_sandbox_needs(&pro, false, false, false), vec![Need::Virtualization]);
527 // Pro whose feature list leaves Windows Sandbox out (Windows Server, N editions): no Windows Sandbox.
528 let without = WindowsFacts { features: [(VM_PLATFORM.to_owned(), 2)].into(), ..pro };
529 assert_eq!(windows_sandbox_needs(&without, false, false, false), vec![Need::WindowsEdition]);
530 // Not known: not said.
531 let unknown = WindowsFacts { firmware: None, ..off.clone() };
532 assert_eq!(wsl_needs(&unknown, &wsl(false, None, false), false), vec![Need::VmPlatform, Need::Wsl]);
533 // In a VM the firmware's flag says nothing; an error names it instead.
534 let guest = WindowsFacts { model: Some("Virtual Machine".into()), manufacturer: Some("Microsoft Corporation".into()), ..off };
535 assert!(guest.virtual_machine() && !guest.firmware_off());
536 assert_eq!(guest.no_virtualization(), Need::NestedVirtualization);
537 assert_eq!(clean_home().no_virtualization(), Need::Virtualization);
538 assert!(about_virtualization("WSL could not import the sandbox: Please enable the Virtual Machine Platform Windows feature and ensure virtualization is enabled in the BIOS."));
539 assert!(!about_virtualization("could not fetch https://g1t.sh"));
540 // Nothing read at all (PowerShell blocked): nothing concluded but what WSL says.
541 assert_eq!(wsl_needs(&WindowsFacts::default(), &wsl(false, None, false), false), vec![Need::Wsl]);
542 assert_eq!(windows_sandbox_needs(&WindowsFacts::default(), false, false, false), vec![Need::WindowsSandbox]);
543 }
544
545 #[test]
546 fn old_windows_says_update() {
547 let old = WindowsFacts { build: Some(18363), ..clean_home() };
548 assert_eq!(wsl_needs(&old, &wsl(false, None, false), false), vec![Need::WindowsUpdate]);
549 let pro = facts(PRO);
550 assert_eq!(windows_sandbox_needs(&pro, true, false, false), vec![Need::WindowsUpdate]);
551 let pro_23h2 = WindowsFacts { build: Some(22631), features: [(VM_PLATFORM.to_owned(), 1), (WINDOWS_SANDBOX.to_owned(), 2)].into(), ..pro };
552 assert_eq!(windows_sandbox_needs(&pro_23h2, false, false, false), vec![Need::WindowsSandbox, Need::WindowsUpdate]);
553 }
554
555 /// Asks this Windows: `cargo test -p g1t-runner -- --ignored this_windows --nocapture`.
556 #[test]
557 #[ignore]
558 fn this_windows_says_what_it_has() {
559 let facts = WindowsFacts::look();
560 println!("{facts:#?}");
561 assert!(facts.edition.is_some() && facts.build.is_some() && facts.booted.is_some(), "{facts:?}");
562 assert!(facts.features.contains_key(VM_PLATFORM), "{facts:?}");
563 // Kept until Windows restarts: asked once, then read back at once.
564 let dir = std::env::temp_dir().join(format!("g1t-facts-{}", std::process::id()));
565 let first = WindowsFacts::look_in(&dir);
566 let started = std::time::Instant::now();
567 assert_eq!(WindowsFacts::look_in(&dir), first);
568 println!("read back in {:?}", started.elapsed());
569 assert!(started.elapsed() < std::time::Duration::from_millis(100));
570 let _ = std::fs::remove_dir_all(&dir);
571 }
572
573 #[test]
574 fn what_windows_says_is_kept_until_it_restarts() {
575 assert!(same_boot(1_760_000_000, 1_760_000_002));
576 assert!(!same_boot(1_760_000_000, 1_760_003_600));
577 let dir = std::env::temp_dir().join(format!("g1t-facts-kept-{}", std::process::id()));
578 std::fs::create_dir_all(&dir).unwrap();
579 let Some(boot) = booted_at() else { return };
580 let kept = Kept { boot, facts: clean_home() };
581 std::fs::write(dir.join(FACTS_FILE), serde_json::to_vec(&kept).unwrap()).unwrap();
582 assert_eq!(WindowsFacts::look_in(&dir), clean_home());
583 let _ = std::fs::remove_dir_all(&dir);
584 }
585
586 #[test]
587 fn wsl_says_its_version_in_any_language() {
588 assert_eq!(wsl_version("WSL version: 2.5.9.0\nKernel version: 6.6.87.2-1\n"), Some((2, 5, 9)));
589 assert_eq!(wsl_version("W\0S\0L\0-\0V\0e\0r\0s\0i\0o\0n\0:\0 \x002\0.\x000\0.\x004\0.\x000\0"), Some((2, 0, 4)));
590 assert_eq!(wsl_version("Version de WSL : 1.2.5.0"), Some((1, 2, 5)));
591 assert_eq!(wsl_version("Invalid command line option: --version"), None);
592 }
593
594 #[test]
595 fn linux_says_what_kvm_lacks() {
596 assert_eq!(cpu_flags("processor\t: 0\nflags\t\t: fpu vme sse vmx ept\n"), (Some(true), false));
597 assert_eq!(cpu_flags("flags\t\t: fpu sse hypervisor\n"), (Some(false), true));
598 assert_eq!(cpu_flags("Features\t: fp asimd\n"), (None, false));
599 let all = KvmFacts { device: true, usable: true, cpu: Some(true), guest: false, qemu: true };
600 assert_eq!(kvm_needs(&all), vec![]);
601 assert_eq!(kvm_needs(&KvmFacts { usable: false, ..all.clone() }), vec![Need::KvmGroup]);
602 assert_eq!(kvm_needs(&KvmFacts { device: false, usable: false, ..all.clone() }), vec![Need::KvmModule]);
603 assert_eq!(kvm_needs(&KvmFacts { device: false, usable: false, cpu: Some(false), ..all.clone() }), vec![Need::Virtualization]);
604 assert_eq!(kvm_needs(&KvmFacts { device: false, usable: false, cpu: Some(false), guest: true, qemu: false }), vec![Need::NestedVirtualization, Need::Qemu]);
605 }
606
607 #[test]
608 fn a_mac_says_what_vfkit_lacks() {
609 assert_eq!(macos_major("14.6.1\n"), Some(14));
610 assert_eq!(vfkit_needs(Some(12), false, true), vec![Need::MacosUpdate]);
611 assert_eq!(vfkit_needs(Some(15), false, false), vec![Need::Homebrew]);
612 assert_eq!(vfkit_needs(Some(15), false, true), vec![Need::Vfkit]);
613 assert_eq!(vfkit_needs(None, true, false), vec![]);
614 }
615
616 #[test]
617 fn needs_are_said_by_name_and_old_apps_still_read_them() {
618 assert_eq!(serde_json::to_string(&Need::VmPlatform).unwrap(), r#""vm_platform""#);
619 assert_eq!(serde_json::from_str::<Need>(r#""nested_virtualization""#).unwrap(), Need::NestedVirtualization);
620 // A desktop app from before `needs` reads the cause's words.
621 assert!(Need::Wsl.said("linux").0.to_ascii_lowercase().contains("wsl is not installed"));
622 assert!(Need::WindowsSandbox.said("windows").0.to_ascii_lowercase().contains("windows sandbox is not turned on"));
623 assert!(Need::Virtualization.said("linux").0.to_ascii_lowercase().contains("vt-x"));
624 assert!(Need::Homebrew.said("linux").0.contains("vfkit") && Need::Homebrew.said("linux").0.contains("not installed"));
625 assert!(Need::WindowsUpdate.said("windows").0.contains("24H2"));
626 }
627}