The desktop app gets its runner running on a clean computer the way Docker Desktop does (#50). g1t-runner sandbox status names what a computer lacks for each kind of job, before anything is downloaded (needs, crates/runner/src/selfhosted/needs.rs): on Windows virtualization off in the firmware, a VM without nested virtualization, Virtual Machine Platform, WSL or an older WSL, Windows Home, Windows Sandbox, a Windows too old, a restart still to do; on Linux KVM, its group and QEMU; on macOS Homebrew, vfkit and macOS 13. Each is said only when Windows, /proc or the Mac says it for certain, so a computer that can run the sandbox is never held back; virtualization inside a VM is named from the error starting it gives. vfkit is found where Homebrew puts it, which a Mac app's PATH lacks.
Each kind of job's card says what is missing in plain words. On Windows one Set up button turns on the features and installs or updates WSL behind a single administrator prompt (a fixed script the app runs as -EncodedCommand, on a thread of its own, the prompt in front of g1t's window), the cards say Setting up, then Restart needed with Restart now or Later; g1t opens again after the restart (RunOnce) and carries on. What only the person can do (the firmware, nested virtualization, Home, Windows Update, KVM, QEMU, Homebrew) has exact steps and a button. One kind of job missing something never stops the other. The desktop guide has "Getting the runner running", with a row for each prerequisite on each system.
- Checked:
- cargo test -p g1t-runner (258 and 5), the app's tests (55, with a scenario per need), clippy; status on this Windows 11 Pro names no needs, its facts read live (ignored test); the setup script run unelevated fails plainly ("requires elevation") and changes nothing; the Runner window and the docs rendered with each scenario.
- G1t-Session:
- ses_01m4yfkg80ev2t3yyd05fak46z
11 files+1789−600/11 viewed
| 60 | 60 | NoVfkit, | |
| 61 | 61 | /// Linux, with neither Sysbox in Docker nor KVM. | |
| 62 | 62 | NoSysbox, | |
| 63 | + | /// This computer lacks something the sandbox needs, by name (the | |
| 64 | + | /// launcher's `needs`): each kind of job's card says what, and Set up | |
| 65 | + | /// does what Windows can do itself. | |
| 66 | + | NeedsSetup, | |
| 63 | 67 | /// The newest release is published without the sandbox's images yet. | |
| 64 | 68 | WaitingForImage, | |
| 65 | 69 | /// The sandbox's image couldn't be downloaded, or isn't the release's. | |
| ⋯ | |||
| 270 | 274 | /// The person stopped the whole runner, or turned it off. | |
| 271 | 275 | pub held: bool, | |
| 272 | 276 | pub update: Update, | |
| 277 | + | /// How Set up stands (setup.rs). | |
| 278 | + | pub setup: SetUp, | |
| 279 | + | /// What installs QEMU here: `sudo apt install qemu-system-x86`. | |
| 280 | + | pub qemu_install: Option<String>, | |
| 281 | + | } | |
| 282 | + | ||
| 283 | + | /// How Set up stands: what Windows can do itself for the sandbox, done | |
| 284 | + | /// behind one administrator prompt (setup.rs). | |
| 285 | + | #[derive(Clone, Debug, Default, PartialEq, Eq)] | |
| 286 | + | pub struct SetUp { | |
| 287 | + | /// Under way, and what it does: `turns on Virtual Machine Platform and installs WSL`. | |
| 288 | + | pub running: Option<String>, | |
| 289 | + | /// Why the last one didn't finish, until it is tried again. | |
| 290 | + | pub failed: Option<String>, | |
| 291 | + | /// The person chose Later over restarting now. | |
| 292 | + | pub later: bool, | |
| 293 | + | /// Every kind of job's needs, of those that are on: one press of Set | |
| 294 | + | /// up meets them all, so each card says all it does. | |
| 295 | + | pub needs: Vec<String>, | |
| 273 | 296 | } | |
| 274 | 297 | ||
| 275 | 298 | /// How the runner's updates stand. | |
| ⋯ | |||
| 360 | 383 | const VIEW_LOG: Action = Action::now("view_log", "View log"); | |
| 361 | 384 | const REPORT: Action = Action::now("report", "Report"); | |
| 362 | 385 | const VIEW_RUNNERS: Action = Action::now("view_runners", "View on g1t"); | |
| 386 | + | /// What Windows can do itself for the sandbox, at once, behind its own | |
| 387 | + | /// administrator prompt (setup.rs): busy until the card says it is setting up. | |
| 388 | + | const SET_UP: Action = Action::waits("set_up", "Set up"); | |
| 389 | + | const RESTART_WINDOWS: Action = Action::waits("restart_windows", "Restart now") | |
| 390 | + | .confirming("Restart Windows now? Save your work first. g1t opens again after the restart and finishes setting up the runner."); | |
| 391 | + | const RESTART_LATER: Action = Action::waits("restart_later", "Later"); | |
| 392 | + | const WINDOWS_UPDATE: Action = Action::now("windows_update", "Open Windows Update"); | |
| 393 | + | const WINDOWS_ACTIVATION: Action = Action::now("windows_activation", "Open Activation"); | |
| 394 | + | const FIRMWARE_HELP: Action = Action::now("firmware_help", "How to turn it on"); | |
| 395 | + | const NESTED_HELP: Action = Action::now("nested_help", "How to turn it on"); | |
| 396 | + | const SETUP_HELP: Action = Action::now("setup_help", "Learn more"); | |
| 397 | + | const COPY_KVM_LOAD: Action = Action::now("copy_kvm_load", "Copy command"); | |
| 398 | + | const COPY_KVM_GROUP: Action = Action::now("copy_kvm_group", "Copy command"); | |
| 399 | + | const COPY_QEMU_INSTALL: Action = Action::now("copy_qemu_install", "Copy command"); | |
| 400 | + | const GET_HOMEBREW: Action = Action::now("get_homebrew", "Get Homebrew"); | |
| 401 | + | const SOFTWARE_UPDATE: Action = Action::now("software_update", "Open Software Update"); | |
| 402 | + | const OPEN_RUNNER: Action = Action::now("open_runner", "See what's missing"); | |
| 363 | 403 | ||
| 364 | 404 | fn some(text: impl Into<String>) -> Option<String> { | |
| 365 | 405 | Some(text.into()) | |
| ⋯ | |||
| 378 | 418 | pub fn diagnose(code: Code, facts: &Facts) -> Diagnosis { | |
| 379 | 419 | let workspace = facts.workspace.as_deref().unwrap_or("your workspace"); | |
| 380 | 420 | let detail = facts.detail.clone(); | |
| 421 | + | let capabilities = facts.sandbox.as_ref().map(|status| capabilities(status, facts)).unwrap_or_default(); | |
| 381 | 422 | let (tone, title, cause, fix, actions): (Tone, String, Option<String>, Option<String>, Vec<Action>) = match code { | |
| 382 | 423 | Code::Off => ( | |
| 383 | 424 | Tone::Off, | |
| ⋯ | |||
| 509 | 550 | some("Install Sysbox 0.7 or later, or turn on virtualization and install QEMU so /dev/kvm exists. The runner starts by itself once one is there."), | |
| 510 | 551 | vec![INSTALL_SYSBOX, CHOOSE_BACKEND], | |
| 511 | 552 | ), | |
| 553 | + | Code::NeedsSetup => { | |
| 554 | + | // The first kind of job that waits on something, in its card's words. | |
| 555 | + | let first = capabilities.iter().find(|kind| kind.enabled && kind.headline.is_some() && matches!(kind.tone, Tone::Warning | Tone::Problem | Tone::Working)); | |
| 556 | + | match first { | |
| 557 | + | Some(kind) => ( | |
| 558 | + | if kind.state == KindState::Starting { Tone::Working } else { Tone::Warning }, | |
| 559 | + | format!("Not running: {}", lowered(kind.headline.as_deref().unwrap_or_default())), | |
| 560 | + | kind.cause.clone(), | |
| 561 | + | kind.fix.clone(), | |
| 562 | + | if kind.actions.is_empty() { vec![OPEN_RUNNER] } else { kind.actions.clone() }, | |
| 563 | + | ), | |
| 564 | + | None => ( | |
| 565 | + | Tone::Warning, | |
| 566 | + | "Not running: this computer needs setting up".into(), | |
| 567 | + | some("The sandbox needs something this computer doesn't have yet."), | |
| 568 | + | some("Each kind of job's card says what, and sets up what it can."), | |
| 569 | + | vec![OPEN_RUNNER], | |
| 570 | + | ), | |
| 571 | + | } | |
| 572 | + | } | |
| 512 | 573 | Code::WaitingForImage => { | |
| 513 | 574 | let (release, image) = detail.as_deref().and_then(launcher::missing_images).map_or((None, None), |(release, image)| (Some(release), image)); | |
| 514 | 575 | let release = release.map_or_else(|| "The newest g1t-runner".to_owned(), |release| format!("g1t-runner {release}")); | |
| ⋯ | |||
| 630 | 691 | vec![RESTART, VIEW_LOG], | |
| 631 | 692 | ), | |
| 632 | 693 | }; | |
| 633 | − | let capabilities = facts.sandbox.as_ref().map(|status| capabilities(status, facts)).unwrap_or_default(); | |
| 634 | 694 | let in_capabilities = !capabilities.is_empty() | |
| 635 | 695 | && match code { | |
| 636 | − | Code::Idle | Code::Busy | Code::KindsStopped => true, | |
| 696 | + | Code::Idle | Code::Busy | Code::KindsStopped | Code::NeedsSetup => true, | |
| 637 | 697 | Code::Starting | Code::Updating => facts.live, | |
| 638 | 698 | // Said by a card, for its kind of job, word for word. | |
| 639 | 699 | Code::NoWsl | |
| ⋯ | |||
| 704 | 764 | } | |
| 705 | 765 | } | |
| 706 | 766 | ||
| 767 | + | /// A headline inside a sentence: `WSL isn't installed` stays, `Virtualization is off` is `virtualization is off`. | |
| 768 | + | fn lowered(headline: &str) -> String { | |
| 769 | + | let mut letters = headline.chars(); | |
| 770 | + | let Some(first) = letters.next() else { return String::new() }; | |
| 771 | + | // An initialism (WSL, KVM, QEMU) keeps its capitals. | |
| 772 | + | if headline.chars().nth(1).is_some_and(char::is_uppercase) { | |
| 773 | + | return headline.to_owned(); | |
| 774 | + | } | |
| 775 | + | first.to_lowercase().chain(letters).collect() | |
| 776 | + | } | |
| 777 | + | ||
| 778 | + | /// What a kind of job's card says of what this computer lacks for it. | |
| 779 | + | struct NeedWords { | |
| 780 | + | state: KindState, | |
| 781 | + | tone: Tone, | |
| 782 | + | title: &'static str, | |
| 783 | + | headline: String, | |
| 784 | + | cause: String, | |
| 785 | + | fix: Option<String>, | |
| 786 | + | actions: Vec<Action>, | |
| 787 | + | } | |
| 788 | + | ||
| 789 | + | /// The card's words for what this computer lacks (the launcher's `needs`, | |
| 790 | + | /// first what must come first): what Set up does when Windows can do all | |
| 791 | + | /// of it, else the first, with exact steps. None for a need this app | |
| 792 | + | /// doesn't know, from a newer launcher: its own words say it then. | |
| 793 | + | fn need_words(needs: &[String], id: &str, facts: &Facts) -> Option<NeedWords> { | |
| 794 | + | let label = kind_label(id); | |
| 795 | + | let first = needs.first()?.as_str(); | |
| 796 | + | let words = |headline: &str, cause: String, fix: String, actions: Vec<Action>| NeedWords { | |
| 797 | + | state: KindState::Unavailable, | |
| 798 | + | tone: Tone::Warning, | |
| 799 | + | title: "Needs setting up", | |
| 800 | + | headline: headline.into(), | |
| 801 | + | cause, | |
| 802 | + | fix: Some(fix), | |
| 803 | + | actions, | |
| 804 | + | }; | |
| 805 | + | if super::setup::fixable(needs) { | |
| 806 | + | // What one press does: for every kind of job that is on, this one's included. | |
| 807 | + | let all = if facts.setup.needs.is_empty() { needs } else { &facts.setup.needs[..] }; | |
| 808 | + | let plan = super::setup::plan_of(all); | |
| 809 | + | if let Some(doing) = &facts.setup.running { | |
| 810 | + | return Some(NeedWords { | |
| 811 | + | state: KindState::Starting, | |
| 812 | + | tone: Tone::Working, | |
| 813 | + | title: "Setting up", | |
| 814 | + | headline: "Setting up".into(), | |
| 815 | + | cause: format!("g1t {doing}. Windows asks you to allow it first, and it takes a few minutes."), | |
| 816 | + | fix: None, | |
| 817 | + | actions: vec![], | |
| 818 | + | }); | |
| 819 | + | } | |
| 820 | + | let has = |need: &str| needs.iter().any(|n| n == need); | |
| 821 | + | let headline = if has("wsl") { | |
| 822 | + | "WSL isn't installed" | |
| 823 | + | } else if has("vm_platform") { | |
| 824 | + | "Virtual Machine Platform is off" | |
| 825 | + | } else if has("wsl_update") { | |
| 826 | + | "WSL needs an update" | |
| 827 | + | } else { | |
| 828 | + | "Windows Sandbox is off" | |
| 829 | + | }; | |
| 830 | + | let cause = if id == "linux" { | |
| 831 | + | let mut lacks = Vec::new(); | |
| 832 | + | if has("vm_platform") { | |
| 833 | + | lacks.push("Virtual Machine Platform turned on"); | |
| 834 | + | } | |
| 835 | + | if has("wsl") { | |
| 836 | + | lacks.push("WSL installed"); | |
| 837 | + | } else if has("wsl_update") { | |
| 838 | + | lacks.push("a newer WSL"); | |
| 839 | + | } | |
| 840 | + | format!("Linux jobs run in a small Linux VM of their own under WSL 2, which needs {}.", lacks.join(" and ")) | |
| 841 | + | } else { | |
| 842 | + | "Windows jobs each run in a Windows Sandbox of their own, which is turned off.".to_owned() | |
| 843 | + | }; | |
| 844 | + | let restart = if plan.features.is_empty() { "" } else { ", and to restart after" }; | |
| 845 | + | let fix = format!("Set up {} in one go. Windows asks you to allow it{restart}.", plan.said()); | |
| 846 | + | return Some(match &facts.setup.failed { | |
| 847 | + | Some(why) => NeedWords { | |
| 848 | + | cause: format!("Set up didn't finish: {why}"), | |
| 849 | + | fix: Some(format!("{fix} Its log says what happened.")), | |
| 850 | + | actions: vec![SET_UP, VIEW_LOG], | |
| 851 | + | ..words(headline, String::new(), String::new(), vec![]) | |
| 852 | + | }, | |
| 853 | + | None => words(headline, cause, fix, vec![SET_UP, SETUP_HELP]), | |
| 854 | + | }); | |
| 855 | + | } | |
| 856 | + | let linux_host = facts.platform == "linux"; | |
| 857 | + | Some(match first { | |
| 858 | + | "restart" => NeedWords { | |
| 859 | + | title: "Restart needed", | |
| 860 | + | ..words( | |
| 861 | + | "Restart to finish setting up", | |
| 862 | + | format!("Windows has turned on what {label} need, and it takes effect once Windows restarts."), | |
| 863 | + | if facts.setup.later { | |
| 864 | + | "Restart when it suits you: g1t opens again after, and carries on by itself.".into() | |
| 865 | + | } else { | |
| 866 | + | "Restart now, or later: g1t opens again after the restart and carries on by itself.".into() | |
| 867 | + | }, | |
| 868 | + | if facts.setup.later { vec![RESTART_WINDOWS] } else { vec![RESTART_WINDOWS, RESTART_LATER] }, | |
| 869 | + | ) | |
| 870 | + | }, | |
| 871 | + | "windows_update" if id == "windows" => words( | |
| 872 | + | "Windows needs an update", | |
| 873 | + | "Windows jobs use Windows Sandbox's command line, which came with Windows 11 24H2, and this Windows is older.".into(), | |
| 874 | + | "Update Windows to 24H2 or later in Windows Update, and restart. g1t carries on after.".into(), | |
| 875 | + | vec![WINDOWS_UPDATE, CHECK_NOW], | |
| 876 | + | ), | |
| 877 | + | "windows_update" => words( | |
| 878 | + | "Windows needs an update", | |
| 879 | + | "Linux jobs run in a small Linux VM under WSL 2, and this Windows is older than the first with it, Windows 10 version 2004.".into(), | |
| 880 | + | "Update Windows in Windows Update, and restart. g1t sets up the rest after.".into(), | |
| 881 | + | vec![WINDOWS_UPDATE, CHECK_NOW], | |
| 882 | + | ), | |
| 883 | + | "virtualization" => words( | |
| 884 | + | "Virtualization is off", | |
| 885 | + | if linux_host { | |
| 886 | + | "The sandbox is a small virtual machine, and this computer's processor has virtualization (Intel VT-x or AMD-V) turned off in its firmware, or has none.".into() | |
| 887 | + | } else { | |
| 888 | + | "The sandbox is a small virtual machine, and this computer has virtualization (Intel VT-x or AMD-V) turned off in its firmware, where only you can turn it on.".into() | |
| 889 | + | }, | |
| 890 | + | "Restart into the firmware settings (BIOS or UEFI: a key such as F2, F10, Del or Esc as the computer starts), turn on Intel Virtualization Technology or SVM Mode, then save and restart. g1t carries on after.".into(), | |
| 891 | + | vec![FIRMWARE_HELP, CHECK_NOW], | |
| 892 | + | ), | |
| 893 | + | "nested_virtualization" => words( | |
| 894 | + | "This VM can't run the sandbox", | |
| 895 | + | "g1t is running in a virtual machine whose host doesn't pass virtualization through to it, and the sandbox is a virtual machine inside it.".into(), | |
| 896 | + | "On the host, turn on nested virtualization for this VM, then start it again. On Hyper-V, with the VM off: Set-VMProcessor -VMName <name> -ExposeVirtualizationExtensions $true.".into(), | |
| 897 | + | vec![NESTED_HELP, CHECK_NOW], | |
| 898 | + | ), | |
| 899 | + | "windows_edition" => NeedWords { | |
| 900 | + | tone: Tone::Off, | |
| 901 | + | title: "Not available", | |
| 902 | + | ..words( | |
| 903 | + | "Not in Windows Home", | |
| 904 | + | "Windows jobs each run in a Windows Sandbox, and Windows Home doesn't have it.".into(), | |
| 905 | + | "Linux jobs run here all the same. Windows jobs need Windows 11 Pro, Enterprise or Education.".into(), | |
| 906 | + | vec![WINDOWS_ACTIVATION], | |
| 907 | + | ) | |
| 908 | + | }, | |
| 909 | + | "kvm_module" => words( | |
| 910 | + | "KVM isn't loaded", | |
| 911 | + | "Linux jobs run in a small VM under QEMU with KVM, and /dev/kvm isn't there, though this processor can run virtual machines.".into(), | |
| 912 | + | format!("Load it with `{}` (Copy command), then Check now. If it says the firmware disabled it, turn on VT-x or AMD-V there.", super::launcher::KVM_LOAD), | |
| 913 | + | vec![COPY_KVM_LOAD, CHECK_NOW], | |
| 914 | + | ), | |
| 915 | + | "kvm_group" => words( | |
| 916 | + | "No access to KVM", | |
| 917 | + | "/dev/kvm is here, but your account isn't allowed to use it.".into(), | |
| 918 | + | format!("Run `{}` (Copy command), then log out and back in.", super::launcher::KVM_GROUP), | |
| 919 | + | vec![COPY_KVM_GROUP, CHECK_NOW], | |
| 920 | + | ), | |
| 921 | + | "qemu" => words( | |
| 922 | + | "QEMU isn't installed", | |
| 923 | + | "Linux jobs run in a small VM under QEMU, which isn't installed.".into(), | |
| 924 | + | match &facts.qemu_install { | |
| 925 | + | Some(command) => format!("Install it with `{command}` (Copy command), then Check now. Or install Sysbox 0.7 or later instead."), | |
| 926 | + | None => "Install QEMU with your package manager, then Check now. Or install Sysbox 0.7 or later instead.".into(), | |
| 927 | + | }, | |
| 928 | + | if facts.qemu_install.is_some() { vec![COPY_QEMU_INSTALL, CHECK_NOW] } else { vec![CHECK_NOW, INSTALL_SYSBOX] }, | |
| 929 | + | ), | |
| 930 | + | "macos_update" => words( | |
| 931 | + | "macOS needs an update", | |
| 932 | + | "Linux jobs run in a small VM with Apple's Virtualization framework, through vfkit, which needs macOS 13 or later.".into(), | |
| 933 | + | "Update macOS in Software Update, then open g1t again.".into(), | |
| 934 | + | vec![SOFTWARE_UPDATE], | |
| 935 | + | ), | |
| 936 | + | "homebrew" => words( | |
| 937 | + | "vfkit isn't installed", | |
| 938 | + | "Linux jobs run in a small VM under vfkit, which installs with Homebrew, and neither is on this Mac.".into(), | |
| 939 | + | format!("Install Homebrew from brew.sh (Get Homebrew), then run `{}` in Terminal (Copy command). The runner starts by itself once vfkit is there.", launcher::VFKIT_INSTALL), | |
| 940 | + | vec![GET_HOMEBREW, COPY_VFKIT_INSTALL], | |
| 941 | + | ), | |
| 942 | + | "vfkit" => words( | |
| 943 | + | "vfkit isn't installed", | |
| 944 | + | "Linux jobs run in a small VM that vfkit runs with Apple's Virtualization framework, and vfkit isn't installed.".into(), | |
| 945 | + | format!("Run `{}` in Terminal (Copy command). The runner starts by itself once it's there.", launcher::VFKIT_INSTALL), | |
| 946 | + | vec![COPY_VFKIT_INSTALL, CHECK_NOW], | |
| 947 | + | ), | |
| 948 | + | _ => return None, | |
| 949 | + | }) | |
| 950 | + | } | |
| 951 | + | ||
| 707 | 952 | /// A catalogue title as a card's headline: `Not running: virtualization is off` is `Virtualization is off`. | |
| 708 | 953 | fn headline(title: &str) -> String { | |
| 709 | 954 | let said = title.strip_prefix("Not running: ").unwrap_or(title); | |
| ⋯ | |||
| 766 | 1011 | said(&mut line, KindState::Stopped, Tone::Off, some(format!("You stopped {label} on this computer. They stay stopped until you start them.")), None, vec![]); | |
| 767 | 1012 | return line; | |
| 768 | 1013 | } | |
| 1014 | + | // What this computer lacks for it, by name: said plainly, with Set up | |
| 1015 | + | // for what Windows can do itself, and exact steps for the rest. | |
| 1016 | + | if let Some(words) = need_words(&capability.needs, id, facts) { | |
| 1017 | + | said(&mut line, words.state, words.tone, Some(words.cause), words.fix, words.actions); | |
| 1018 | + | line.title = words.title.into(); | |
| 1019 | + | line.headline = Some(words.headline); | |
| 1020 | + | return line; | |
| 1021 | + | } | |
| 769 | 1022 | // This computer can't run it, and why. | |
| 770 | 1023 | if !capability.available { | |
| 771 | 1024 | if id == "linux" | |
| ⋯ | |||
| 889 | 1142 | Code::Off => (Tone::Off, "Runner off".to_owned()), | |
| 890 | 1143 | Code::Stopped | Code::KindsStopped => (Tone::Off, "Runner stopped".to_owned()), | |
| 891 | 1144 | Code::Offline => (Tone::Warning, "Runner offline".to_owned()), | |
| 1145 | + | Code::NeedsSetup if diagnosis.tone == Tone::Working => (Tone::Working, "Runner setting up".to_owned()), | |
| 892 | 1146 | _ => (diagnosis.tone, "Runner needs you".to_owned()), | |
| 893 | 1147 | }; | |
| 894 | 1148 | Chip { tone, label } | |
| ⋯ | |||
| 1272 | 1526 | let signed_out = diagnose(Code::SignedOut, &Facts { sandbox: Some(status(NO_WSL_PRO)), ..facts() }); | |
| 1273 | 1527 | assert!(!signed_out.in_capabilities); | |
| 1274 | 1528 | } | |
| 1529 | + | ||
| 1530 | + | // --- What this computer lacks, by name (the launcher's `needs`) ------------------ | |
| 1531 | + | ||
| 1532 | + | /// A launcher's status with each kind of job's needs: `None` for a kind | |
| 1533 | + | /// that is running, waiting for jobs. | |
| 1534 | + | fn needing(linux: Option<&[&str]>, native: Option<(&str, Option<&[&str]>)>) -> Status { | |
| 1535 | + | let kind = |id: &str, needs: Option<&[&str]>| match needs { | |
| 1536 | + | None => format!(r#"{{"id":"{id}","available":true,"state":"running","runner_version":"0.3.5","turned_off":false,"needs":[]}}"#), | |
| 1537 | + | // What the launcher says of macOS jobs, which need nothing: they aren't released. | |
| 1538 | + | Some([]) => format!(r#"{{"id":"{id}","available":false,"state":"absent","turned_off":false,"cause":"Sandboxed macOS jobs are not released yet.","fix":"Run macOS jobs on a runner registered with --no-docker for now.","needs":[]}}"#), | |
| 1539 | + | Some(needs) => format!( | |
| 1540 | + | r#"{{"id":"{id}","available":false,"state":"{}","turned_off":false,"cause":"In the launcher's words.","fix":"Its fix.","needs":{}}}"#, | |
| 1541 | + | if id == "linux" { "error" } else { "absent" }, | |
| 1542 | + | serde_json::to_string(needs).unwrap() | |
| 1543 | + | ), | |
| 1544 | + | }; | |
| 1545 | + | let mut kinds = vec![kind("linux", linux)]; | |
| 1546 | + | if let Some((id, needs)) = native { | |
| 1547 | + | kinds.push(kind(id, needs)); | |
| 1548 | + | } | |
| 1549 | + | let state = if linux.is_none() { "running" } else { "error" }; | |
| 1550 | + | status(&format!(r#"{{"schema":1,"state":"{state}","backend":"wsl2","launcher_version":"0.3.5","capabilities":[{}]}}"#, kinds.join(","))) | |
| 1551 | + | } | |
| 1552 | + | ||
| 1553 | + | /// What the whole runner and each card say, as the app would show it: the code `shown` picks. | |
| 1554 | + | fn shown_for(status: Status, facts: Facts) -> Diagnosis { | |
| 1555 | + | let code = if status.running() { Code::Idle } else if status.linux_needs().is_empty() { Code::SandboxFailed } else { Code::NeedsSetup }; | |
| 1556 | + | // As the app fills it in: the needs of each kind that is on. | |
| 1557 | + | let setup = SetUp { needs: super::super::setup::needs(&status, |kind| !facts.off.iter().any(|off| off == kind)), ..facts.setup.clone() }; | |
| 1558 | + | diagnose(code, &Facts { sandbox: Some(status), live: true, setup, ..facts }) | |
| 1559 | + | } | |
| 1560 | + | ||
| 1561 | + | fn ids(actions: &[Action]) -> Vec<&'static str> { | |
| 1562 | + | actions.iter().map(|a| a.id).collect() | |
| 1563 | + | } | |
| 1564 | + | ||
| 1565 | + | /// Writes each scenario's diagnosis to `G1T_SCREENS_OUT`, when it is set, for the screens. | |
| 1566 | + | fn screen(name: &str, diagnosis: &Diagnosis) { | |
| 1567 | + | if let Some(dir) = std::env::var_os("G1T_SCREENS_OUT") { | |
| 1568 | + | let dir = std::path::PathBuf::from(dir); | |
| 1569 | + | std::fs::create_dir_all(&dir).unwrap(); | |
| 1570 | + | let shown = serde_json::json!({ "diagnosis": diagnosis, "chip": chip(diagnosis) }); | |
| 1571 | + | std::fs::write(dir.join(format!("{name}.json")), serde_json::to_vec_pretty(&shown).unwrap()).unwrap(); | |
| 1572 | + | } | |
| 1573 | + | } | |
| 1574 | + | ||
| 1575 | + | #[test] | |
| 1576 | + | fn a_clean_windows_home_is_set_up_with_one_button_and_says_windows_jobs_plainly() { | |
| 1577 | + | let home = shown_for(needing(Some(&["vm_platform", "wsl"]), Some(("windows", Some(&["windows_edition"])))), facts()); | |
| 1578 | + | screen("windows-home-clean", &home); | |
| 1579 | + | assert_eq!(home.code, Code::NeedsSetup); | |
| 1580 | + | assert_eq!(home.title, "Not running: WSL isn't installed"); | |
| 1581 | + | assert!(home.in_capabilities); | |
| 1582 | + | let [linux, windows] = &home.capabilities[..] else { panic!() }; | |
| 1583 | + | assert_eq!((linux.title.as_str(), linux.tone, linux.state), ("Needs setting up", Tone::Warning, KindState::Unavailable)); | |
| 1584 | + | assert_eq!(linux.headline.as_deref(), Some("WSL isn't installed")); | |
| 1585 | + | assert_eq!(linux.cause.as_deref(), Some("Linux jobs run in a small Linux VM of their own under WSL 2, which needs Virtual Machine Platform turned on and WSL installed.")); | |
| 1586 | + | assert_eq!(linux.fix.as_deref(), Some("Set up turns on Virtual Machine Platform and installs WSL in one go. Windows asks you to allow it, and to restart after.")); | |
| 1587 | + | assert_eq!(ids(&linux.actions), ["set_up", "setup_help"]); | |
| 1588 | + | assert!(SET_UP.waits && SET_UP.confirm.is_none(), "Windows' own prompt asks"); | |
| 1589 | + | // Home: no dead end, and no button that can't work. | |
| 1590 | + | assert_eq!((windows.title.as_str(), windows.tone), ("Not available", Tone::Off)); | |
| 1591 | + | assert_eq!(windows.headline.as_deref(), Some("Not in Windows Home")); | |
| 1592 | + | assert_eq!(windows.fix.as_deref(), Some("Linux jobs run here all the same. Windows jobs need Windows 11 Pro, Enterprise or Education.")); | |
| 1593 | + | assert_eq!(ids(&windows.actions), ["windows_activation"]); | |
| 1594 | + | // The chip asks for the person. | |
| 1595 | + | assert_eq!(chip(&home).label, "Runner needs you"); | |
| 1596 | + | } | |
| 1597 | + | ||
| 1598 | + | #[test] | |
| 1599 | + | fn pro_sets_up_both_kinds_and_each_says_its_own() { | |
| 1600 | + | let pro = shown_for(needing(Some(&["vm_platform", "wsl"]), Some(("windows", Some(&["windows_sandbox"])))), facts()); | |
| 1601 | + | screen("windows-pro-clean", &pro); | |
| 1602 | + | let [linux, windows] = &pro.capabilities[..] else { panic!() }; | |
| 1603 | + | assert_eq!(ids(&linux.actions), ["set_up", "setup_help"]); | |
| 1604 | + | assert_eq!(windows.headline.as_deref(), Some("Windows Sandbox is off")); | |
| 1605 | + | // One press sets up both kinds, and each card says all it does. | |
| 1606 | + | let both = "Set up turns on Virtual Machine Platform and Windows Sandbox and installs WSL in one go. Windows asks you to allow it, and to restart after."; | |
| 1607 | + | assert_eq!((linux.fix.as_deref(), windows.fix.as_deref()), (Some(both), Some(both))); | |
| 1608 | + | // Windows jobs stopped by the person: Set up leaves their feature alone, and says so. | |
| 1609 | + | let linux_only = shown_for( | |
| 1610 | + | needing(Some(&["vm_platform", "wsl"]), Some(("windows", Some(&["windows_sandbox"])))), | |
| 1611 | + | Facts { setup: SetUp { needs: vec!["vm_platform".into(), "wsl".into()], ..SetUp::default() }, off: vec!["windows".into()], ..facts() }, | |
| 1612 | + | ); | |
| 1613 | + | assert_eq!(linux_only.capabilities[0].fix.as_deref(), Some("Set up turns on Virtual Machine Platform and installs WSL in one go. Windows asks you to allow it, and to restart after.")); | |
| 1614 | + | // Linux jobs run while Windows Sandbox is off: waiting for jobs, and one card wants a look. | |
| 1615 | + | let linux_runs = shown_for(needing(None, Some(("windows", Some(&["windows_sandbox"])))), facts()); | |
| 1616 | + | screen("windows-pro-sandbox-off", &linux_runs); | |
| 1617 | + | assert_eq!(linux_runs.title, "Waiting for jobs"); | |
| 1618 | + | assert_eq!(linux_runs.capabilities[0].title, "Waiting for jobs"); | |
| 1619 | + | assert_eq!(ids(&linux_runs.capabilities[1].actions), ["set_up", "setup_help"]); | |
| 1620 | + | assert_eq!(chip(&linux_runs), Chip { tone: Tone::Warning, label: "Runner running".into() }); | |
| 1621 | + | // Windows jobs run while Linux jobs need WSL: each kind on its own. | |
| 1622 | + | let windows_runs = shown_for(needing(Some(&["wsl"]), Some(("windows", None))), facts()); | |
| 1623 | + | screen("windows-pro-wsl-missing", &windows_runs); | |
| 1624 | + | assert_eq!(windows_runs.code, Code::Idle); | |
| 1625 | + | assert_eq!(chip(&windows_runs), Chip { tone: Tone::Warning, label: "Runner running".into() }); | |
| 1626 | + | assert_eq!(windows_runs.capabilities[1].title, "Waiting for jobs"); | |
| 1627 | + | assert_eq!(windows_runs.capabilities[0].fix.as_deref(), Some("Set up installs WSL in one go. Windows asks you to allow it.")); | |
| 1628 | + | // An older WSL is updated, not installed again. | |
| 1629 | + | let old = shown_for(needing(Some(&["wsl_update"]), None), facts()); | |
| 1630 | + | assert_eq!(old.capabilities[0].headline.as_deref(), Some("WSL needs an update")); | |
| 1631 | + | assert_eq!(old.capabilities[0].fix.as_deref(), Some("Set up updates WSL in one go. Windows asks you to allow it.")); | |
| 1632 | + | } | |
| 1633 | + | ||
| 1634 | + | #[test] | |
| 1635 | + | fn set_up_shows_while_it_works_then_a_restart_now_or_later() { | |
| 1636 | + | let clean = needing(Some(&["vm_platform", "wsl"]), Some(("windows", Some(&["windows_sandbox"])))); | |
| 1637 | + | let working = SetUp { running: Some("turns on Virtual Machine Platform and Windows Sandbox and installs WSL".into()), ..SetUp::default() }; | |
| 1638 | + | let setting_up = shown_for(clean.clone(), Facts { setup: working, ..facts() }); | |
| 1639 | + | screen("windows-setting-up", &setting_up); | |
| 1640 | + | assert_eq!(setting_up.title, "Not running: setting up"); | |
| 1641 | + | assert_eq!(setting_up.tone, Tone::Working); | |
| 1642 | + | assert_eq!(chip(&setting_up), Chip { tone: Tone::Working, label: "Runner setting up".into() }); | |
| 1643 | + | for kind in &setting_up.capabilities { | |
| 1644 | + | assert_eq!((kind.title.as_str(), kind.state, kind.tone), ("Setting up", KindState::Starting, Tone::Working)); | |
| 1645 | + | assert!(kind.actions.is_empty()); | |
| 1646 | + | assert_eq!(kind.cause.as_deref(), Some("g1t turns on Virtual Machine Platform and Windows Sandbox and installs WSL. Windows asks you to allow it first, and it takes a few minutes.")); | |
| 1647 | + | } | |
| 1648 | + | let restart = shown_for(needing(Some(&["restart"]), Some(("windows", Some(&["restart"])))), facts()); | |
| 1649 | + | screen("windows-restart", &restart); | |
| 1650 | + | assert_eq!(restart.title, "Not running: restart to finish setting up"); | |
| 1651 | + | let linux = &restart.capabilities[0]; | |
| 1652 | + | assert_eq!((linux.title.as_str(), linux.tone), ("Restart needed", Tone::Warning)); | |
| 1653 | + | assert_eq!(ids(&linux.actions), ["restart_windows", "restart_later"]); | |
| 1654 | + | assert!(RESTART_WINDOWS.confirm.is_some_and(|q| q.starts_with("Restart Windows now?"))); | |
| 1655 | + | assert_eq!(linux.fix.as_deref(), Some("Restart now, or later: g1t opens again after the restart and carries on by itself.")); | |
| 1656 | + | let later = shown_for(needing(Some(&["restart"]), None), Facts { setup: SetUp { later: true, ..SetUp::default() }, ..facts() }); | |
| 1657 | + | screen("windows-restart-later", &later); | |
| 1658 | + | assert_eq!(ids(&later.capabilities[0].actions), ["restart_windows"]); | |
| 1659 | + | // Said No to Windows' prompt: nothing changed, and Set up again. | |
| 1660 | + | let declined = SetUp { failed: Some("you said No to Windows' prompt, so nothing changed.".into()), ..SetUp::default() }; | |
| 1661 | + | let again = shown_for(clean, Facts { setup: declined, ..facts() }); | |
| 1662 | + | screen("windows-set-up-declined", &again); | |
| 1663 | + | assert_eq!(again.capabilities[0].cause.as_deref(), Some("Set up didn't finish: you said No to Windows' prompt, so nothing changed.")); | |
| 1664 | + | assert_eq!(ids(&again.capabilities[0].actions), ["set_up", "view_log"]); | |
| 1665 | + | } | |
| 1666 | + | ||
| 1667 | + | #[test] | |
| 1668 | + | fn what_only_the_person_can_do_is_said_step_by_step() { | |
| 1669 | + | let firmware = shown_for(needing(Some(&["virtualization"]), Some(("windows", Some(&["virtualization"])))), facts()); | |
| 1670 | + | screen("windows-virtualization-off", &firmware); | |
| 1671 | + | assert_eq!(firmware.title, "Not running: virtualization is off"); | |
| 1672 | + | for kind in &firmware.capabilities { | |
| 1673 | + | assert_eq!(kind.headline.as_deref(), Some("Virtualization is off")); | |
| 1674 | + | assert!(kind.fix.as_deref().is_some_and(|fix| fix.contains("BIOS or UEFI") && fix.contains("SVM Mode"))); | |
| 1675 | + | assert_eq!(ids(&kind.actions), ["firmware_help", "retry"]); | |
| 1676 | + | } | |
| 1677 | + | let nested = shown_for(needing(Some(&["nested_virtualization"]), Some(("windows", None))), facts()); | |
| 1678 | + | screen("windows-in-a-vm", &nested); | |
| 1679 | + | assert_eq!(nested.capabilities[0].headline.as_deref(), Some("This VM can't run the sandbox")); | |
| 1680 | + | assert!(nested.capabilities[0].fix.as_deref().is_some_and(|fix| fix.contains("ExposeVirtualizationExtensions"))); | |
| 1681 | + | let old = shown_for(needing(Some(&["windows_update"]), Some(("windows", Some(&["windows_update"])))), facts()); | |
| 1682 | + | screen("windows-too-old", &old); | |
| 1683 | + | assert_eq!(ids(&old.capabilities[0].actions), ["windows_update", "retry"]); | |
| 1684 | + | assert!(old.capabilities[0].cause.as_deref().is_some_and(|c| c.contains("version 2004"))); | |
| 1685 | + | assert!(old.capabilities[1].cause.as_deref().is_some_and(|c| c.contains("24H2"))); | |
| 1686 | + | } | |
| 1687 | + | ||
| 1688 | + | #[test] | |
| 1689 | + | fn linux_and_macos_say_the_command_that_fixes_it() { | |
| 1690 | + | let linux = Facts { platform: "linux".into(), qemu_install: Some("sudo apt install qemu-system-x86".into()), ..facts() }; | |
| 1691 | + | let group = shown_for(needing(Some(&["kvm_group"]), None), linux.clone()); | |
| 1692 | + | screen("linux-kvm-group", &group); | |
| 1693 | + | assert_eq!(group.title, "Not running: no access to KVM"); | |
| 1694 | + | assert_eq!(group.capabilities[0].fix.as_deref(), Some("Run `sudo usermod -aG kvm $USER` (Copy command), then log out and back in.")); | |
| 1695 | + | assert_eq!(ids(&group.capabilities[0].actions), ["copy_kvm_group", "retry"]); | |
| 1696 | + | let bare = shown_for(needing(Some(&["kvm_module", "qemu"]), None), linux.clone()); | |
| 1697 | + | screen("linux-no-kvm", &bare); | |
| 1698 | + | assert_eq!(bare.title, "Not running: KVM isn't loaded"); | |
| 1699 | + | assert_eq!(ids(&bare.capabilities[0].actions), ["copy_kvm_load", "retry"]); | |
| 1700 | + | let qemu = shown_for(needing(Some(&["qemu"]), None), linux.clone()); | |
| 1701 | + | assert_eq!(qemu.capabilities[0].fix.as_deref(), Some("Install it with `sudo apt install qemu-system-x86` (Copy command), then Check now. Or install Sysbox 0.7 or later instead.")); | |
| 1702 | + | let unknown_distro = shown_for(needing(Some(&["qemu"]), None), Facts { qemu_install: None, ..linux.clone() }); | |
| 1703 | + | assert_eq!(ids(&unknown_distro.capabilities[0].actions), ["retry", "install_sysbox"]); | |
| 1704 | + | let firmware = shown_for(needing(Some(&["virtualization"]), None), linux); | |
| 1705 | + | assert!(firmware.capabilities[0].cause.as_deref().is_some_and(|c| c.contains("or has none"))); | |
| 1706 | + | let mac = Facts { platform: "macos".into(), ..facts() }; | |
| 1707 | + | let no_brew = shown_for(needing(Some(&["homebrew"]), Some(("macos", Some(&[])))), mac.clone()); | |
| 1708 | + | screen("macos-no-homebrew", &no_brew); | |
| 1709 | + | assert_eq!(no_brew.title, "Not running: vfkit isn't installed"); | |
| 1710 | + | assert_eq!(ids(&no_brew.capabilities[0].actions), ["get_homebrew", "copy_vfkit_install"]); | |
| 1711 | + | let old = shown_for(needing(Some(&["macos_update"]), None), mac); | |
| 1712 | + | assert_eq!(ids(&old.capabilities[0].actions), ["software_update"]); | |
| 1713 | + | } | |
| 1714 | + | ||
| 1715 | + | #[test] | |
| 1716 | + | fn every_need_has_a_way_forward() { | |
| 1717 | + | let needs = [ | |
| 1718 | + | "restart", "windows_update", "virtualization", "nested_virtualization", "vm_platform", "wsl", "wsl_update", "windows_edition", "windows_sandbox", "kvm_module", "kvm_group", "qemu", "macos_update", "homebrew", | |
| 1719 | + | "vfkit", | |
| 1720 | + | ]; | |
| 1721 | + | for need in needs { | |
| 1722 | + | for kind in ["linux", "windows"] { | |
| 1723 | + | let words = need_words(&[need.to_owned()], kind, &facts()).unwrap_or_else(|| panic!("{need} has no words")); | |
| 1724 | + | assert!(!words.actions.is_empty(), "{need} on {kind} is a dead end"); | |
| 1725 | + | assert!(words.fix.is_some_and(|fix| fix.ends_with('.')), "{need}"); | |
| 1726 | + | assert!(words.cause.ends_with('.'), "{need}"); | |
| 1727 | + | } | |
| 1728 | + | } | |
| 1729 | + | // A need from a newer launcher this app doesn't know: the launcher's own words. | |
| 1730 | + | let newer = shown_for(needing(Some(&["something_new"]), None), facts()); | |
| 1731 | + | assert_eq!(newer.capabilities[0].cause.as_deref(), Some("In the launcher's words.")); | |
| 1732 | + | // From a launcher before needs: as before, by its words. | |
| 1733 | + | let before = diagnose(Code::NoWsl, &Facts { sandbox: Some(status(NO_WSL_PRO)), ..facts() }); | |
| 1734 | + | assert_eq!(ids(&before.capabilities[0].actions), ["copy_wsl_install", "learn_more"]); | |
| 1735 | + | assert_eq!(lowered("WSL isn't installed"), "WSL isn't installed"); | |
| 1736 | + | assert_eq!(lowered("No access to KVM"), "no access to KVM"); | |
| 1737 | + | } | |
| 1275 | 1738 | } | |
| 51 | 51 | pub const WSL_INSTALL: &str = "wsl --install --no-distribution"; | |
| 52 | 52 | /// What installs vfkit on a Mac. | |
| 53 | 53 | pub const VFKIT_INSTALL: &str = "brew install vfkit"; | |
| 54 | + | /// What loads KVM on Linux, Intel's or AMD's. | |
| 55 | + | pub const KVM_LOAD: &str = "sudo modprobe kvm_intel || sudo modprobe kvm_amd"; | |
| 56 | + | /// What lets this account use /dev/kvm, after logging in again. | |
| 57 | + | pub const KVM_GROUP: &str = "sudo usermod -aG kvm $USER"; | |
| 58 | + | ||
| 59 | + | /// What installs QEMU on this Linux, from `/etc/os-release` and the | |
| 60 | + | /// machine (`x86_64`, `aarch64`): None where it isn't known. | |
| 61 | + | pub fn qemu_install(os_release: &str, arch: &str) -> Option<String> { | |
| 62 | + | let field = |name: &str| { | |
| 63 | + | os_release.lines().find_map(|line| line.strip_prefix(name).and_then(|rest| rest.strip_prefix('='))).map(|value| value.trim_matches('"').to_ascii_lowercase()).unwrap_or_default() | |
| 64 | + | }; | |
| 65 | + | let family = format!("{} {}", field("ID"), field("ID_LIKE")); | |
| 66 | + | let is = |name: &str| family.split_whitespace().any(|id| id == name); | |
| 67 | + | let system = if arch == "aarch64" { "qemu-system-arm" } else { "qemu-system-x86" }; | |
| 68 | + | if is("debian") || is("ubuntu") { | |
| 69 | + | Some(format!("sudo apt install {system}")) | |
| 70 | + | } else if is("fedora") || is("rhel") || is("centos") { | |
| 71 | + | Some("sudo dnf install qemu-kvm".into()) | |
| 72 | + | } else if is("arch") { | |
| 73 | + | Some("sudo pacman -S qemu-base".into()) | |
| 74 | + | } else if is("opensuse") || is("suse") { | |
| 75 | + | Some("sudo zypper install qemu-kvm".into()) | |
| 76 | + | } else { | |
| 77 | + | None | |
| 78 | + | } | |
| 79 | + | } | |
| 54 | 80 | /// How often the app looks again for a release with the sandbox's images, | |
| 55 | 81 | /// while the newest is published without them. | |
| 56 | 82 | pub const IMAGES_AGAIN: Duration = Duration::from_secs(5 * 60); | |
| ⋯ | |||
| 107 | 133 | /// The person stopped it (`down --only`): stopped, not failing. None | |
| 108 | 134 | /// from a launcher before it could stop one kind of job on its own. | |
| 109 | 135 | pub turned_off: Option<bool>, | |
| 136 | + | /// What this computer still lacks for it, by name, first what must | |
| 137 | + | /// come first: `wsl`, `vm_platform`, `virtualization`, `restart`, … | |
| 138 | + | /// (the launcher's needs.rs). diagnosis.rs says each. Empty from a | |
| 139 | + | /// launcher before them, whose causes are read by their words (`why`). | |
| 140 | + | pub needs: Vec<String>, | |
| 110 | 141 | } | |
| 111 | 142 | ||
| 112 | 143 | impl Capability { | |
| ⋯ | |||
| 194 | 225 | .or_else(|| self.launcher_version.clone()) | |
| 195 | 226 | } | |
| 196 | 227 | ||
| 228 | + | /// What this computer lacks for Linux jobs, by name: none when the | |
| 229 | + | /// person stopped them. | |
| 230 | + | pub fn linux_needs(&self) -> Vec<String> { | |
| 231 | + | let linux = self.linux(); | |
| 232 | + | if linux.off() { vec![] } else { linux.needs } | |
| 233 | + | } | |
| 234 | + | ||
| 197 | 235 | /// What is wrong with Linux jobs, in the launcher's words: its error, | |
| 198 | 236 | /// else why the capability is unavailable. | |
| 199 | 237 | pub fn trouble(&self) -> Option<(String, Option<String>)> { | |
| 60 | 60 | mod launcher; | |
| 61 | 61 | mod log; | |
| 62 | 62 | mod native; | |
| 63 | + | mod setup; | |
| 63 | 64 | ||
| 64 | 65 | use std::collections::VecDeque; | |
| 65 | 66 | use std::fs::{File, OpenOptions}; | |
| ⋯ | |||
| 77 | 78 | ||
| 78 | 79 | pub use backend::Backend; | |
| 79 | 80 | use backend::{Engine, Missing}; | |
| 80 | − | use diagnosis::{Code, Diagnosis, Facts}; | |
| 81 | + | use diagnosis::{Code, Diagnosis, Facts, SetUp}; | |
| 81 | 82 | use launcher::{Down, State, Status}; | |
| 82 | 83 | use native::Fault; | |
| 83 | 84 | ||
| ⋯ | |||
| 207 | 208 | tail: Mutex<VecDeque<String>>, | |
| 208 | 209 | tx: Mutex<Option<Sender<Ask>>>, | |
| 209 | 210 | updates: Mutex<Updates>, | |
| 211 | + | setup: Mutex<SetUp>, | |
| 210 | 212 | } | |
| 211 | 213 | ||
| 212 | 214 | /// What settings, the tray and the site are shown. | |
| ⋯ | |||
| 258 | 260 | Exited(u64), | |
| 259 | 261 | /// What `sandbox status` said. | |
| 260 | 262 | Sandbox(u64, Result<Box<Status>, String>), | |
| 263 | + | /// Set up is done (setup.rs): how it went, and what it logged. | |
| 264 | + | SetUp(setup::Outcome, Vec<String>), | |
| 261 | 265 | Quit(Sender<()>), | |
| 262 | 266 | } | |
| 263 | 267 | ||
| ⋯ | |||
| 383 | 387 | download: now.download.clone(), | |
| 384 | 388 | held: !settings.runner || *app.state::<Shared>().paused.held(), | |
| 385 | 389 | update: update_facts(app, now, backend), | |
| 390 | + | setup: SetUp { | |
| 391 | + | needs: now.sandbox.as_ref().map(|status| setup::needs(status, |kind| settings.runner_capabilities.on(kind))).unwrap_or_default(), | |
| 392 | + | ..app.state::<Shared>().setup.held().clone() | |
| 393 | + | }, | |
| 394 | + | qemu_install: qemu_install(), | |
| 386 | 395 | } | |
| 387 | 396 | } | |
| 388 | 397 | ||
| ⋯ | |||
| 686 | 695 | "get_docker" => browse("https://docs.docker.com/get-started/get-docker/")?, | |
| 687 | 696 | "install_sysbox" => browse("https://github.com/nestybox/sysbox#installation")?, | |
| 688 | 697 | "learn_more" => browse("https://docs.g1t.sh/guides/desktop/#how-jobs-run")?, | |
| 698 | + | "setup_help" => browse("https://docs.g1t.sh/guides/desktop/#getting-the-runner-running")?, | |
| 699 | + | "firmware_help" => browse("https://docs.g1t.sh/guides/desktop/#turning-on-virtualization")?, | |
| 700 | + | "nested_help" => browse("https://docs.g1t.sh/guides/desktop/#in-a-virtual-machine")?, | |
| 701 | + | "set_up" => set_up(app)?, | |
| 702 | + | "restart_windows" => setup::restart_windows(&std::env::current_exe().map_err(|e| e.to_string())?)?, | |
| 703 | + | "restart_later" => { | |
| 704 | + | if let Ok(exe) = std::env::current_exe() { | |
| 705 | + | setup::open_after_restart(&exe); | |
| 706 | + | } | |
| 707 | + | app.state::<Shared>().setup.held().later = true; | |
| 708 | + | publish(app); | |
| 709 | + | } | |
| 710 | + | "windows_update" => open_system("ms-settings:windowsupdate")?, | |
| 711 | + | "windows_activation" => open_system("ms-settings:activation")?, | |
| 712 | + | "software_update" => open_system("x-apple.systempreferences:com.apple.Software-Update-Settings.extension")?, | |
| 713 | + | "copy_kvm_load" => copy(launcher::KVM_LOAD)?, | |
| 714 | + | "copy_kvm_group" => copy(launcher::KVM_GROUP)?, | |
| 715 | + | "copy_qemu_install" => copy(&qemu_install().ok_or("This Linux's package manager isn't one g1t knows: install QEMU with it.")?)?, | |
| 716 | + | "get_homebrew" => browse("https://brew.sh")?, | |
| 689 | 717 | "report" => browse(config::server(app).join("support").map_err(|e| e.to_string())?.as_str())?, | |
| 690 | 718 | other => return Err(format!("the runner has no action called {other}")), | |
| 691 | 719 | } | |
| ⋯ | |||
| 730 | 758 | Err(format!("It couldn't be copied. The command is: {text}")) | |
| 731 | 759 | } | |
| 732 | 760 | ||
| 761 | + | /// What installs QEMU on this Linux, if it is one g1t knows. | |
| 762 | + | fn qemu_install() -> Option<String> { | |
| 763 | + | if !cfg!(target_os = "linux") { | |
| 764 | + | return None; | |
| 765 | + | } | |
| 766 | + | launcher::qemu_install(&std::fs::read_to_string("/etc/os-release").unwrap_or_default(), std::env::consts::ARCH) | |
| 767 | + | } | |
| 768 | + | ||
| 769 | + | /// Opens one of the system's own settings pages: `ms-settings:windowsupdate`. | |
| 770 | + | fn open_system(page: &str) -> Result<(), String> { | |
| 771 | + | let opener = if cfg!(windows) { "explorer.exe" } else { "open" }; | |
| 772 | + | backend::quiet(opener).arg(page).spawn().map(|_| ()).map_err(|e| format!("It couldn't be opened: {e}")) | |
| 773 | + | } | |
| 774 | + | ||
| 775 | + | /// Set up: what the sandbox still needs that Windows can do itself, done | |
| 776 | + | /// at once behind Windows' own administrator prompt, on a thread of its | |
| 777 | + | /// own (setup.rs). The cards say it is setting up until it is done; then | |
| 778 | + | /// the runner looks again, at once. | |
| 779 | + | fn set_up(app: &AppHandle) -> Result<(), String> { | |
| 780 | + | let shared = app.state::<Shared>(); | |
| 781 | + | let status = shared.now.held().sandbox.clone().ok_or("g1t hasn't heard from the sandbox yet: try again in a moment.")?; | |
| 782 | + | let kinds = config::settings(app).runner_capabilities; | |
| 783 | + | let plan = setup::plan(&status, |kind| kinds.on(kind)); | |
| 784 | + | if plan.is_empty() { | |
| 785 | + | return Err("There's nothing here for Set up to do.".into()); | |
| 786 | + | } | |
| 787 | + | { | |
| 788 | + | let mut setup = shared.setup.held(); | |
| 789 | + | if setup.running.is_some() { | |
| 790 | + | return Ok(()); | |
| 791 | + | } | |
| 792 | + | *setup = SetUp { running: Some(plan.said()), ..SetUp::default() }; | |
| 793 | + | } | |
| 794 | + | publish(app); | |
| 795 | + | // Windows' prompt in front of g1t's window, not behind it: asked here, | |
| 796 | + | // on the main thread, whose window it is. | |
| 797 | + | #[cfg(windows)] | |
| 798 | + | let window = app.get_window(crate::WINDOW).and_then(|w| w.hwnd().ok()).map_or(0, |hwnd| hwnd.0 as isize); | |
| 799 | + | #[cfg(not(windows))] | |
| 800 | + | let window = 0; | |
| 801 | + | let log = folder(app).join("setup.log"); | |
| 802 | + | let marker = native_dir(app, Backend::Sandboxed).join(setup::RESTART_FILE); | |
| 803 | + | let _ = std::fs::create_dir_all(folder(app)); | |
| 804 | + | let _ = std::fs::remove_file(&log); | |
| 805 | + | let script = setup::script(&plan, &log, &marker); | |
| 806 | + | let tx = shared.tx.held().clone(); | |
| 807 | + | let spawned = std::thread::Builder::new().name("g1t-runner-setup".into()).spawn(move || { | |
| 808 | + | let outcome = setup::run_elevated(&script, window); | |
| 809 | + | let lines = std::fs::read_to_string(&log).map(|text| text.lines().map(str::to_owned).collect()).unwrap_or_default(); | |
| 810 | + | let _ = std::fs::remove_file(&log); | |
| 811 | + | if let Some(tx) = tx { | |
| 812 | + | let _ = tx.send(Ask::SetUp(outcome, lines)); | |
| 813 | + | } | |
| 814 | + | }); | |
| 815 | + | if let Err(error) = spawned { | |
| 816 | + | *shared.setup.held() = SetUp { failed: Some(format!("it couldn't start ({error}).")), ..SetUp::default() }; | |
| 817 | + | publish(app); | |
| 818 | + | } | |
| 819 | + | Ok(()) | |
| 820 | + | } | |
| 821 | + | ||
| 733 | 822 | /// Windows' "Turn Windows features on or off", where Windows Sandbox and | |
| 734 | 823 | /// Virtual Machine Platform are. | |
| 735 | 824 | fn windows_features() -> Result<(), String> { | |
| ⋯ | |||
| 1081 | 1170 | Ask::Line(generation, line, out) => self.line(generation, &line, out), | |
| 1082 | 1171 | Ask::Exited(generation) => self.exited(generation), | |
| 1083 | 1172 | Ask::Sandbox(generation, status) => self.polled(generation, status), | |
| 1173 | + | Ask::SetUp(outcome, lines) => self.set_up_done(outcome, lines), | |
| 1084 | 1174 | } | |
| 1085 | 1175 | } | |
| 1086 | 1176 | ||
| 1177 | + | /// Set up is done: what it logged goes in the runner's log, and the | |
| 1178 | + | /// runner looks again at once, so the cards say what is left (a | |
| 1179 | + | /// restart, or nothing) and what can run now starts. | |
| 1180 | + | fn set_up_done(&mut self, outcome: setup::Outcome, lines: Vec<String>) { | |
| 1181 | + | for line in &lines { | |
| 1182 | + | self.write_log(line); | |
| 1183 | + | } | |
| 1184 | + | let failed = match outcome { | |
| 1185 | + | setup::Outcome::Done | setup::Outcome::Restart => None, | |
| 1186 | + | setup::Outcome::Declined => Some("you said No to Windows' prompt, so nothing changed.".to_owned()), | |
| 1187 | + | setup::Outcome::Failed(why) => Some(lines.iter().rev().find_map(|l| l.strip_prefix("g1t setup: Failed: ")).map_or(why, str::to_owned)), | |
| 1188 | + | }; | |
| 1189 | + | if let Some(why) = &failed { | |
| 1190 | + | self.write_log(&format!("g1t: Set up didn't finish: {why}")); | |
| 1191 | + | } | |
| 1192 | + | *self.app.state::<Shared>().setup.held() = SetUp { failed, ..SetUp::default() }; | |
| 1193 | + | self.handle(Ask::Retry); | |
| 1194 | + | publish(&self.app); | |
| 1195 | + | } | |
| 1196 | + | ||
| 1087 | 1197 | fn line(&mut self, generation: u64, line: &str, out: bool) { | |
| 1088 | 1198 | let Some(run) = self.run.as_mut().filter(|run| run.generation == generation) else { return }; | |
| 1089 | 1199 | if run.engine == Engine::Launcher { | |
| ⋯ | |||
| 2205 | 2315 | if status.running() { | |
| 2206 | 2316 | return (Code::Idle, Extra::default()); | |
| 2207 | 2317 | } | |
| 2318 | + | // Something this computer lacks, by name: the cards say what. | |
| 2319 | + | if !status.linux_needs().is_empty() { | |
| 2320 | + | return (Code::NeedsSetup, Extra::default()); | |
| 2321 | + | } | |
| 2208 | 2322 | if let Some((code, cause, fix)) = diagnosis::sandbox_trouble(status) { | |
| 2209 | 2323 | return (code, Extra { detail: Some(cause), fix, ..Extra::default() }); | |
| 2210 | 2324 | } | |
| 1 | + | //! Set up: what the sandbox still needs on Windows that Windows can do | |
| 2 | + | //! itself, done at once behind one administrator prompt, as Docker | |
| 3 | + | //! Desktop does. The launcher says what is missing, by name, before | |
| 4 | + | //! anything is downloaded (`needs`, crates/runner/src/selfhosted/needs.rs); | |
| 5 | + | //! this turns on the Virtual Machine Platform and Windows Sandbox features | |
| 6 | + | //! and installs or updates WSL, for the kinds of job the person has on. | |
| 7 | + | //! | |
| 8 | + | //! The script is the app's own and fixed: nothing in it comes from the | |
| 9 | + | //! network or from a file another program could change between writing it | |
| 10 | + | //! and Windows running it as administrator. It goes to PowerShell as | |
| 11 | + | //! `-EncodedCommand`, and its only values are the app's own paths. | |
| 12 | + | //! | |
| 13 | + | //! What it turned on that takes a restart, it says by writing this boot's | |
| 14 | + | //! time to the launcher's `restart-pending`: the launcher then says | |
| 15 | + | //! `restart` for the kinds of job that wait on it, until Windows has | |
| 16 | + | //! restarted. After a restart g1t opens again (RunOnce, if it doesn't open | |
| 17 | + | //! at sign-in anyway) and starts the runner as it always does. | |
| 18 | + | //! | |
| 19 | + | //! Everything here runs on a thread of its own: the prompt waits on the | |
| 20 | + | //! person, and installing WSL takes minutes. | |
| 21 | + | ||
| 22 | + | use std::path::Path; | |
| 23 | + | ||
| 24 | + | use super::launcher::Status; | |
| 25 | + | ||
| 26 | + | /// The needs Set up meets. | |
| 27 | + | pub const FIXABLE: [&str; 4] = ["vm_platform", "wsl", "wsl_update", "windows_sandbox"]; | |
| 28 | + | /// The launcher's file that says a restart is still to be done (needs.rs's RESTART_FILE). | |
| 29 | + | pub const RESTART_FILE: &str = "restart-pending"; | |
| 30 | + | /// What Windows' installer says when it worked and wants a restart. | |
| 31 | + | const NEEDS_RESTART: u32 = 3010; | |
| 32 | + | /// What Windows says when the person said No to the prompt (ERROR_CANCELLED). | |
| 33 | + | const CANCELLED: u32 = 1223; | |
| 34 | + | ||
| 35 | + | /// What Set up is to do. | |
| 36 | + | #[derive(Clone, Debug, Default, PartialEq, Eq)] | |
| 37 | + | pub struct Plan { | |
| 38 | + | /// Windows features to turn on. | |
| 39 | + | pub features: Vec<&'static str>, | |
| 40 | + | pub wsl: Option<Wsl>, | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| 44 | + | pub enum Wsl { | |
| 45 | + | Install, | |
| 46 | + | Update, | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | impl Plan { | |
| 50 | + | pub fn is_empty(&self) -> bool { | |
| 51 | + | self.features.is_empty() && self.wsl.is_none() | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | /// What it does, said: `turns on Virtual Machine Platform and installs WSL`. | |
| 55 | + | pub fn said(&self) -> String { | |
| 56 | + | let mut steps: Vec<String> = Vec::new(); | |
| 57 | + | let named: Vec<&str> = self | |
| 58 | + | .features | |
| 59 | + | .iter() | |
| 60 | + | .map(|feature| match *feature { | |
| 61 | + | "VirtualMachinePlatform" => "Virtual Machine Platform", | |
| 62 | + | "Containers-DisposableClientVM" => "Windows Sandbox", | |
| 63 | + | other => other, | |
| 64 | + | }) | |
| 65 | + | .collect(); | |
| 66 | + | if !named.is_empty() { | |
| 67 | + | steps.push(format!("turns on {}", named.join(" and "))); | |
| 68 | + | } | |
| 69 | + | match self.wsl { | |
| 70 | + | Some(Wsl::Install) => steps.push("installs WSL".into()), | |
| 71 | + | Some(Wsl::Update) => steps.push("updates WSL".into()), | |
| 72 | + | None => {} | |
| 73 | + | } | |
| 74 | + | steps.join(" and ") | |
| 75 | + | } | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | /// Whether a kind of job's needs are all ones Set up meets. | |
| 79 | + | pub fn fixable(needs: &[String]) -> bool { | |
| 80 | + | !needs.is_empty() && needs.iter().all(|need| FIXABLE.contains(&need.as_str())) | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | /// What Set up does for the kinds of job that are on (`on`) and wait only on it. | |
| 84 | + | pub fn plan(status: &Status, on: impl Fn(&str) -> bool) -> Plan { | |
| 85 | + | plan_of(&needs(status, on)) | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | /// The needs of the kinds of job that are on (`on`). | |
| 89 | + | pub fn needs(status: &Status, on: impl Fn(&str) -> bool) -> Vec<String> { | |
| 90 | + | status.capabilities.iter().filter(|c| on(&c.id) && !c.off()).flat_map(|c| c.needs.iter().cloned()).collect() | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | /// What Set up does for these needs. | |
| 94 | + | pub fn plan_of(needs: &[String]) -> Plan { | |
| 95 | + | let mut plan = Plan::default(); | |
| 96 | + | for need in needs { | |
| 97 | + | match need.as_str() { | |
| 98 | + | "vm_platform" if !plan.features.contains(&"VirtualMachinePlatform") => plan.features.push("VirtualMachinePlatform"), | |
| 99 | + | "windows_sandbox" if !plan.features.contains(&"Containers-DisposableClientVM") => plan.features.push("Containers-DisposableClientVM"), | |
| 100 | + | "wsl" => plan.wsl = Some(Wsl::Install), | |
| 101 | + | "wsl_update" if plan.wsl.is_none() => plan.wsl = Some(Wsl::Update), | |
| 102 | + | _ => {} | |
| 103 | + | } | |
| 104 | + | } | |
| 105 | + | plan | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | /// A path as a PowerShell single-quoted string. | |
| 109 | + | fn quoted(path: &Path) -> String { | |
| 110 | + | format!("'{}'", path.display().to_string().replace('\'', "''")) | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | /// The script, run as administrator: it logs to `log`, and leaves this | |
| 114 | + | /// boot's time in `marker` when what it did takes a restart. | |
| 115 | + | pub fn script(plan: &Plan, log: &Path, marker: &Path) -> String { | |
| 116 | + | let features = plan.features.iter().map(|f| format!("'{f}'")).collect::<Vec<_>>().join(", "); | |
| 117 | + | let wsl = match plan.wsl { | |
| 118 | + | Some(Wsl::Install) => "Install-Wsl '--install', '--no-distribution'", | |
| 119 | + | Some(Wsl::Update) => "Install-Wsl '--update'", | |
| 120 | + | None => "", | |
| 121 | + | }; | |
| 122 | + | format!( | |
| 123 | + | r#"$ErrorActionPreference = 'Stop' | |
| 124 | + | $log = {log} | |
| 125 | + | $env:WSL_UTF8 = '1' | |
| 126 | + | function Say($text) {{ Add-Content -LiteralPath $log -Value ('g1t setup: ' + $text) }} | |
| 127 | + | function Install-Wsl {{ | |
| 128 | + | # From GitHub first (--web-download), which needs no Microsoft Store; else from the Store. | |
| 129 | + | foreach ($from in @(@('--web-download'), @())) {{ | |
| 130 | + | $out = Join-Path $env:TEMP 'g1t-setup-wsl.txt' | |
| 131 | + | $p = Start-Process -FilePath 'wsl.exe' -ArgumentList ($args + $from) -Wait -PassThru -WindowStyle Hidden -RedirectStandardOutput $out | |
| 132 | + | Get-Content -LiteralPath $out -ErrorAction SilentlyContinue | Where-Object {{ $_.Trim() }} | ForEach-Object {{ Say ('wsl: ' + $_.Trim()) }} | |
| 133 | + | Remove-Item -LiteralPath $out -ErrorAction SilentlyContinue | |
| 134 | + | if ($p.ExitCode -eq 0) {{ return }} | |
| 135 | + | Say ('wsl.exe ' + (($args + $from) -join ' ') + ' exited with ' + $p.ExitCode) | |
| 136 | + | }} | |
| 137 | + | throw 'WSL could not be installed or updated.' | |
| 138 | + | }} | |
| 139 | + | $restart = $false | |
| 140 | + | try {{ | |
| 141 | + | foreach ($name in @({features})) {{ | |
| 142 | + | $feature = Get-WindowsOptionalFeature -Online -FeatureName $name | |
| 143 | + | if ($feature.State -ne 'Enabled') {{ | |
| 144 | + | Say "Turning on $name." | |
| 145 | + | $result = Enable-WindowsOptionalFeature -Online -FeatureName $name -All -NoRestart -WarningAction SilentlyContinue | |
| 146 | + | if ($result.RestartNeeded) {{ $restart = $true }} | |
| 147 | + | }} | |
| 148 | + | }} | |
| 149 | + | {wsl} | |
| 150 | + | if ($restart) {{ | |
| 151 | + | Set-Content -LiteralPath {marker} -Value (Get-CimInstance Win32_OperatingSystem).LastBootUpTime.ToUniversalTime().ToString('o') | |
| 152 | + | Say 'Done; Windows needs a restart to finish.' | |
| 153 | + | exit {NEEDS_RESTART} | |
| 154 | + | }} | |
| 155 | + | Say 'Done.' | |
| 156 | + | exit 0 | |
| 157 | + | }} catch {{ | |
| 158 | + | Say ('Failed: ' + $_.Exception.Message) | |
| 159 | + | exit 1 | |
| 160 | + | }} | |
| 161 | + | "#, | |
| 162 | + | log = quoted(log), | |
| 163 | + | marker = quoted(marker), | |
| 164 | + | ) | |
| 165 | + | } | |
| 166 | + | ||
| 167 | + | /// PowerShell's `-EncodedCommand`: UTF-16LE in base64. | |
| 168 | + | pub fn encoded(script: &str) -> String { | |
| 169 | + | use base64::Engine; | |
| 170 | + | let bytes: Vec<u8> = script.encode_utf16().flat_map(u16::to_le_bytes).collect(); | |
| 171 | + | base64::engine::general_purpose::STANDARD.encode(bytes) | |
| 172 | + | } | |
| 173 | + | ||
| 174 | + | /// How Set up went. | |
| 175 | + | #[derive(Clone, Debug, PartialEq, Eq)] | |
| 176 | + | pub enum Outcome { | |
| 177 | + | Done, | |
| 178 | + | /// Done, and Windows needs a restart to finish. | |
| 179 | + | Restart, | |
| 180 | + | /// The person said No to Windows' prompt. | |
| 181 | + | Declined, | |
| 182 | + | Failed(String), | |
| 183 | + | } | |
| 184 | + | ||
| 185 | + | /// What an exit code means. | |
| 186 | + | pub fn outcome(code: u32) -> Outcome { | |
| 187 | + | match code { | |
| 188 | + | 0 => Outcome::Done, | |
| 189 | + | NEEDS_RESTART => Outcome::Restart, | |
| 190 | + | CANCELLED => Outcome::Declined, | |
| 191 | + | other => Outcome::Failed(format!("Set up stopped (exit code {other}).")), | |
| 192 | + | } | |
| 193 | + | } | |
| 194 | + | ||
| 195 | + | /// Runs the script as administrator, asking Windows first (its prompt, | |
| 196 | + | /// in front of `window`), and waits for it. | |
| 197 | + | #[cfg(windows)] | |
| 198 | + | pub fn run_elevated(script: &str, window: isize) -> Outcome { | |
| 199 | + | use std::ffi::OsStr; | |
| 200 | + | use std::os::windows::ffi::OsStrExt; | |
| 201 | + | ||
| 202 | + | #[repr(C)] | |
| 203 | + | struct ShellExecuteInfo { | |
| 204 | + | size: u32, | |
| 205 | + | mask: u32, | |
| 206 | + | window: isize, | |
| 207 | + | verb: *const u16, | |
| 208 | + | file: *const u16, | |
| 209 | + | parameters: *const u16, | |
| 210 | + | directory: *const u16, | |
| 211 | + | show: i32, | |
| 212 | + | instance: isize, | |
| 213 | + | id_list: *mut std::ffi::c_void, | |
| 214 | + | class: *const u16, | |
| 215 | + | class_key: isize, | |
| 216 | + | hot_key: u32, | |
| 217 | + | icon_or_monitor: isize, | |
| 218 | + | process: isize, | |
| 219 | + | } | |
| 220 | + | #[link(name = "shell32")] | |
| 221 | + | unsafe extern "system" { | |
| 222 | + | fn ShellExecuteExW(info: *mut ShellExecuteInfo) -> i32; | |
| 223 | + | } | |
| 224 | + | unsafe extern "system" { | |
| 225 | + | fn WaitForSingleObject(handle: isize, millis: u32) -> u32; | |
| 226 | + | fn GetExitCodeProcess(handle: isize, code: *mut u32) -> i32; | |
| 227 | + | fn CloseHandle(handle: isize) -> i32; | |
| 228 | + | fn GetLastError() -> u32; | |
| 229 | + | } | |
| 230 | + | const SEE_MASK_NOCLOSEPROCESS: u32 = 0x40; | |
| 231 | + | const SEE_MASK_NOASYNC: u32 = 0x100; | |
| 232 | + | const SW_HIDE: i32 = 0; | |
| 233 | + | const INFINITE: u32 = u32::MAX; | |
| 234 | + | ||
| 235 | + | let wide = |text: &str| OsStr::new(text).encode_wide().chain(std::iter::once(0)).collect::<Vec<u16>>(); | |
| 236 | + | let verb = wide("runas"); | |
| 237 | + | let file = wide("powershell.exe"); | |
| 238 | + | let parameters = wide(&format!("-NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand {}", encoded(script))); | |
| 239 | + | let mut info = ShellExecuteInfo { | |
| 240 | + | size: std::mem::size_of::<ShellExecuteInfo>() as u32, | |
| 241 | + | mask: SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NOASYNC, | |
| 242 | + | window, | |
| 243 | + | verb: verb.as_ptr(), | |
| 244 | + | file: file.as_ptr(), | |
| 245 | + | parameters: parameters.as_ptr(), | |
| 246 | + | directory: std::ptr::null(), | |
| 247 | + | show: SW_HIDE, | |
| 248 | + | instance: 0, | |
| 249 | + | id_list: std::ptr::null_mut(), | |
| 250 | + | class: std::ptr::null(), | |
| 251 | + | class_key: 0, | |
| 252 | + | hot_key: 0, | |
| 253 | + | icon_or_monitor: 0, | |
| 254 | + | process: 0, | |
| 255 | + | }; | |
| 256 | + | // SAFETY: `info` and the strings it points to live until the call returns. | |
| 257 | + | if unsafe { ShellExecuteExW(&mut info) } == 0 { | |
| 258 | + | let error = unsafe { GetLastError() }; | |
| 259 | + | return if error == CANCELLED { Outcome::Declined } else { Outcome::Failed(format!("Windows couldn't start Set up (error {error}).")) }; | |
| 260 | + | } | |
| 261 | + | if info.process == 0 { | |
| 262 | + | return Outcome::Failed("Windows started Set up without a process to wait for.".into()); | |
| 263 | + | } | |
| 264 | + | let mut code = 0u32; | |
| 265 | + | // SAFETY: the process handle is ours until CloseHandle. | |
| 266 | + | unsafe { | |
| 267 | + | WaitForSingleObject(info.process, INFINITE); | |
| 268 | + | GetExitCodeProcess(info.process, &mut code); | |
| 269 | + | CloseHandle(info.process); | |
| 270 | + | } | |
| 271 | + | outcome(code) | |
| 272 | + | } | |
| 273 | + | ||
| 274 | + | #[cfg(not(windows))] | |
| 275 | + | pub fn run_elevated(_script: &str, _window: isize) -> Outcome { | |
| 276 | + | Outcome::Failed("Set up is for Windows; the runner's card says what to do here.".into()) | |
| 277 | + | } | |
| 278 | + | ||
| 279 | + | /// Restarts Windows now. g1t opens again after, once (RunOnce), and | |
| 280 | + | /// carries on setting up. | |
| 281 | + | pub fn restart_windows(app_exe: &Path) -> Result<(), String> { | |
| 282 | + | if !cfg!(windows) { | |
| 283 | + | return Err("Only Windows is restarted from g1t.".into()); | |
| 284 | + | } | |
| 285 | + | open_after_restart(app_exe); | |
| 286 | + | let status = super::backend::quiet("shutdown.exe") | |
| 287 | + | .args(["/r", "/t", "0", "/c", "g1t is restarting Windows to finish setting up the runner."]) | |
| 288 | + | .status() | |
| 289 | + | .map_err(|e| format!("Windows couldn't be restarted: {e}"))?; | |
| 290 | + | if status.success() { Ok(()) } else { Err("Windows didn't restart: restart it from the Start menu.".into()) } | |
| 291 | + | } | |
| 292 | + | ||
| 293 | + | /// g1t opens once when the person next signs in to Windows, after the | |
| 294 | + | /// restart (`RunOnce`, which Windows clears as it runs it), whether or not | |
| 295 | + | /// it opens at sign-in anyway: setup carries on from there. | |
| 296 | + | pub fn open_after_restart(app_exe: &Path) { | |
| 297 | + | if cfg!(windows) { | |
| 298 | + | let _ = super::backend::quiet("reg") | |
| 299 | + | .args(["add", r"HKCU\Software\Microsoft\Windows\CurrentVersion\RunOnce", "/v", "g1t", "/t", "REG_SZ", "/d", &format!("\"{}\"", app_exe.display()), "/f"]) | |
| 300 | + | .stdout(std::process::Stdio::null()) | |
| 301 | + | .stderr(std::process::Stdio::null()) | |
| 302 | + | .status(); | |
| 303 | + | } | |
| 304 | + | } | |
| 305 | + | ||
| 306 | + | #[cfg(test)] | |
| 307 | + | mod tests { | |
| 308 | + | use super::*; | |
| 309 | + | ||
| 310 | + | fn status(json: &str) -> Status { | |
| 311 | + | super::super::launcher::parse(json).unwrap() | |
| 312 | + | } | |
| 313 | + | ||
| 314 | + | #[test] | |
| 315 | + | fn set_up_does_what_the_kinds_that_are_on_need() { | |
| 316 | + | let clean = status( | |
| 317 | + | r#"{"schema":1,"state":"error","capabilities":[ | |
| 318 | + | {"id":"linux","available":false,"state":"error","needs":["vm_platform","wsl"]}, | |
| 319 | + | {"id":"windows","available":false,"state":"absent","needs":["windows_sandbox"]}]}"#, | |
| 320 | + | ); | |
| 321 | + | let both = plan(&clean, |_| true); | |
| 322 | + | assert_eq!(both.features, ["VirtualMachinePlatform", "Containers-DisposableClientVM"]); | |
| 323 | + | assert_eq!(both.wsl, Some(Wsl::Install)); | |
| 324 | + | assert_eq!(both.said(), "turns on Virtual Machine Platform and Windows Sandbox and installs WSL"); | |
| 325 | + | // Windows jobs stopped by the person: their feature is left alone. | |
| 326 | + | let linux = plan(&clean, |kind| kind == "linux"); | |
| 327 | + | assert_eq!(linux.features, ["VirtualMachinePlatform"]); | |
| 328 | + | assert_eq!(linux.said(), "turns on Virtual Machine Platform and installs WSL"); | |
| 329 | + | let update = status(r#"{"schema":1,"state":"error","capabilities":[{"id":"linux","available":false,"needs":["wsl_update"]}]}"#); | |
| 330 | + | assert_eq!(plan(&update, |_| true).said(), "updates WSL"); | |
| 331 | + | // Home: nothing Set up can do for Windows jobs. | |
| 332 | + | let home = status(r#"{"schema":1,"state":"running","capabilities":[{"id":"linux","available":true,"state":"running"},{"id":"windows","available":false,"needs":["windows_edition"]}]}"#); | |
| 333 | + | assert!(plan(&home, |_| true).is_empty()); | |
| 334 | + | assert!(fixable(&["vm_platform".into(), "wsl".into()])); | |
| 335 | + | assert!(!fixable(&["virtualization".into(), "vm_platform".into()])); | |
| 336 | + | assert!(!fixable(&[])); | |
| 337 | + | } | |
| 338 | + | ||
| 339 | + | #[test] | |
| 340 | + | fn the_script_is_fixed_and_says_a_restart_by_its_exit_code() { | |
| 341 | + | let plan = Plan { features: vec!["VirtualMachinePlatform"], wsl: Some(Wsl::Install) }; | |
| 342 | + | let script = script(&plan, Path::new(r"C:\Users\o'neil\AppData\Local\sh.g1t\runner\setup.log"), Path::new(r"C:\x\restart-pending")); | |
| 343 | + | assert!(script.contains(r"$log = 'C:\Users\o''neil\AppData\Local\sh.g1t\runner\setup.log'")); | |
| 344 | + | assert!(script.contains("foreach ($name in @('VirtualMachinePlatform'))")); | |
| 345 | + | assert!(script.contains("Install-Wsl '--install', '--no-distribution'")); | |
| 346 | + | assert!(script.contains("exit 3010")); | |
| 347 | + | assert!(!script.contains("{features}") && !script.contains("{wsl}")); | |
| 348 | + | assert_eq!(outcome(0), Outcome::Done); | |
| 349 | + | assert_eq!(outcome(3010), Outcome::Restart); | |
| 350 | + | assert_eq!(outcome(1223), Outcome::Declined); | |
| 351 | + | assert!(matches!(outcome(1), Outcome::Failed(_))); | |
| 352 | + | // UTF-16LE in base64, as PowerShell reads it: "a" is 61 00. | |
| 353 | + | assert_eq!(encoded("a"), "YQA="); | |
| 354 | + | } | |
| 355 | + | ||
| 356 | + | /// Runs the script as this user, not as administrator, so it changes | |
| 357 | + | /// nothing: it must parse, log, and fail plainly. Windows only: | |
| 358 | + | /// `cargo test -- --ignored the_script_runs --nocapture`. | |
| 359 | + | #[test] | |
| 360 | + | #[ignore] | |
| 361 | + | fn the_script_runs_and_without_an_administrator_says_why() { | |
| 362 | + | let dir = std::env::temp_dir().join(format!("g1t-setup-{}", std::process::id())); | |
| 363 | + | std::fs::create_dir_all(&dir).unwrap(); | |
| 364 | + | let (log, marker) = (dir.join("setup.log"), dir.join(RESTART_FILE)); | |
| 365 | + | let plan = Plan { features: vec!["VirtualMachinePlatform"], wsl: Some(Wsl::Update) }; | |
| 366 | + | let code = super::super::backend::quiet("powershell.exe") | |
| 367 | + | .args(["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", &encoded(&script(&plan, &log, &marker))]) | |
| 368 | + | .status() | |
| 369 | + | .unwrap() | |
| 370 | + | .code() | |
| 371 | + | .unwrap(); | |
| 372 | + | let said = std::fs::read_to_string(&log).unwrap(); | |
| 373 | + | println!("exit {code}\n{said}"); | |
| 374 | + | assert_eq!(outcome(code as u32), Outcome::Failed("Set up stopped (exit code 1).".into())); | |
| 375 | + | assert!(said.contains("g1t setup: Failed:"), "{said}"); | |
| 376 | + | assert!(!marker.exists()); | |
| 377 | + | let _ = std::fs::remove_dir_all(&dir); | |
| 378 | + | } | |
| 379 | + | } |
| 288 | 288 | | **In Docker** | `g1t-runner` on your computer, driving your Docker. Each job is a container of its own. | `container` | Its own container; but the runner controls your Docker, which can reach your files (Docker Desktop shares your home folder with its VM), so this is not a sandbox. | | |
| 289 | 289 | | **Directly on this computer** | `g1t-runner` on your computer, each job a process in a folder of its own. For jobs that need the system itself, such as building a Windows installer. | `host` | Everything your account can: your files, your keys, what an earlier job left. Not a sandbox. Never given a public repository's work. | | |
| 290 | 290 | ||
| 291 | − | What the sandbox needs on each computer: | |
| 292 | − | ||
| 293 | − | | Computer | Linux jobs | Windows jobs | | |
| 294 | − | | --- | --- | --- | | |
| 295 | − | | Windows 11, any edition | WSL, installed once: `wsl --install --no-distribution` in an admin terminal, then a restart. The app's **Copy command** button copies it. | Windows Sandbox, on Pro, Enterprise and Education, version 24H2 or later, turned on in Windows features. Home has none, and Linux jobs still run. | | |
| 296 | − | | macOS 13 or later | [vfkit](https://github.com/crc-org/vfkit): `brew install vfkit` | Not released yet | | |
| 297 | − | | Linux | Sysbox 0.7 or later in Docker, or QEMU with KVM | None: Linux jobs are native | | |
| 291 | + | What the sandbox needs on each computer, and how the app gets it there, | |
| 292 | + | is in [getting the runner running](#getting-the-runner-running). | |
| 298 | 293 | ||
| 299 | − | The sandbox also needs virtualization turned on in your computer's | |
| 300 | − | firmware (Intel VT-x or AMD-V), which most computers have on. | |
| 301 | − | ||
| 302 | 294 | The sandbox keeps jobs from your computer, not your computer from the | |
| 303 | 295 | sandbox: anything running as your account can look inside it while a job | |
| 304 | 296 | runs. So don't choose **Directly on this computer** under the same | |
| ⋯ | |||
| 344 | 336 | it and turn the app's runner off. Uninstalling g1t with **Delete the | |
| 345 | 337 | application data** ticked removes the sandbox too. | |
| 346 | 338 | ||
| 339 | + | ### Getting the runner running | |
| 340 | + | ||
| 341 | + | The sandbox is a small virtual machine, so it needs what any VM needs: | |
| 342 | + | virtualization on in your computer, and on Windows the parts of Windows | |
| 343 | + | that run Linux VMs. You don't need to know what those are. Before it | |
| 344 | + | downloads anything, the app asks `g1t-runner sandbox status` what your | |
| 345 | + | computer still lacks for each kind of job, and each kind's card says it in | |
| 346 | + | plain words: what is missing, why, and one button that fixes it, or the | |
| 347 | + | exact steps when only you can. | |
| 348 | + | ||
| 349 | + | | Computer | Linux jobs need | Windows jobs need | | |
| 350 | + | | --- | --- | --- | | |
| 351 | + | | Windows 11 (any edition), Windows 10 2004 or later | Virtualization on, the Virtual Machine Platform feature, and WSL 2.0 or later. **Set up** does the last two. | Windows 11 Pro, Enterprise or Education, 24H2 or later; virtualization on; the Windows Sandbox feature. **Set up** turns it on. | | |
| 352 | + | | macOS 13 or later, Apple silicon or Intel | [vfkit](https://github.com/crc-org/vfkit), from [Homebrew](https://brew.sh): `brew install vfkit`. | Not released yet. | | |
| 353 | + | | Linux | Sysbox 0.7 or later in Docker; or virtualization on, KVM, and QEMU. | None: Linux jobs are this computer's own. | | |
| 354 | + | ||
| 355 | + | Each kind of job is set up on its own: Windows jobs that can't run on | |
| 356 | + | Windows Home never stop Linux jobs, and Linux jobs waiting on WSL never | |
| 357 | + | stop Windows jobs. What is missing is looked at again within half a minute, so | |
| 358 | + | a card changes by itself once you have fixed it; **Check now** looks at | |
| 359 | + | once. | |
| 360 | + | ||
| 361 | + | **On Windows, Set up does what Windows can do itself, in one go.** It turns | |
| 362 | + | on Virtual Machine Platform and Windows Sandbox, for the kinds of job you | |
| 363 | + | have on, and installs or updates WSL. Windows asks you to allow it, once | |
| 364 | + | (on a standard account, an administrator types their password). The cards | |
| 365 | + | say *Setting up* meanwhile, a few minutes. When what it turned on takes a | |
| 366 | + | restart, they say *Restart needed*, with **Restart now**, which asks first | |
| 367 | + | (save your work), and **Later**. After the restart g1t opens again by | |
| 368 | + | itself and carries on: Linux jobs download their image and start. Set up | |
| 369 | + | is the same as running these in an admin terminal, and its log goes into | |
| 370 | + | the runner's: | |
| 371 | + | ||
| 372 | + | ```powershell | |
| 373 | + | Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform -All -NoRestart | |
| 374 | + | Enable-WindowsOptionalFeature -Online -FeatureName Containers-DisposableClientVM -All -NoRestart | |
| 375 | + | wsl --install --no-distribution # or wsl --update, for an older WSL | |
| 376 | + | ``` | |
| 377 | + | ||
| 378 | + | Whatever is missing, nothing falls back to running jobs in Docker or | |
| 379 | + | directly on your computer: those are only ever [your choice](#how-jobs-run). | |
| 380 | + | ||
| 381 | + | #### On Windows | |
| 382 | + | ||
| 383 | + | | The card says | What is missing | What to do | | |
| 384 | + | | --- | --- | --- | | |
| 385 | + | | *Needs setting up*: *WSL isn't installed*, *Virtual Machine Platform is off*, *WSL needs an update* | WSL 2, which the Linux VM runs on: the Virtual Machine Platform feature, and WSL 2.0 or later from the Microsoft Store (not the one built into older Windows). | **Set up**, and restart if it asks. | | |
| 386 | + | | *Needs setting up*: *Windows Sandbox is off* | The Windows Sandbox feature, which each Windows job runs in. | **Set up**, then restart. | | |
| 387 | + | | *Not available*: *Not in Windows Home* | Windows Sandbox, which Windows Home doesn't have. Linux jobs run all the same. | Nothing, for Linux jobs. For Windows jobs, Windows 11 Pro, Enterprise or Education: **Open Activation** is where Windows upgrades. | | |
| 388 | + | | *Restart needed*: *Restart to finish setting up* | Set up turned on a Windows feature, which takes effect after a restart. | **Restart now**, or **Later** and restart when it suits you. g1t opens again after and carries on. | | |
| 389 | + | | *Virtualization is off* | Intel VT-x or AMD-V, turned off in your computer's firmware, where Windows can't turn it on. | [Turn on virtualization](#turning-on-virtualization), then **Check now**. | | |
| 390 | + | | *This VM can't run the sandbox* | g1t is in a virtual machine whose host doesn't pass virtualization through to it. | [Turn on nested virtualization](#in-a-virtual-machine) on the host. | | |
| 391 | + | | *Windows needs an update*, on Linux jobs | Windows older than Windows 10 version 2004, the first with WSL 2. | **Open Windows Update**, update, and restart. | | |
| 392 | + | | *Windows needs an update*, on Windows jobs | Windows Sandbox's command line, `wsb`, which came with Windows 11 24H2. | **Open Windows Update**, update to 24H2 or later, and restart. | | |
| 393 | + | | *Set up didn't finish* | You said No to Windows' prompt, or a step failed, with its reason. | **Set up** again; **View log** says what happened. | | |
| 394 | + | | *WSL uses mirrored networking* | A setting, not a part: `networkingMode=mirrored` in `%USERPROFILE%\.wslconfig` lets Linux reach your computer's own `localhost`, so the sandbox won't start with it. | Remove it, run `wsl --shutdown`, then **Retry**. | | |
| 395 | + | ||
| 396 | + | #### On Linux | |
| 397 | + | ||
| 398 | + | With Sysbox 0.7 or later in Docker, none of these is needed: the runner's | |
| 399 | + | image runs under Sysbox, without a VM. Without it, Linux jobs run in a VM | |
| 400 | + | under QEMU with KVM, and the card says what is missing for that, with the | |
| 401 | + | command that fixes it on **Copy command**. | |
| 402 | + | ||
| 403 | + | | The card says | What is missing | What to do | | |
| 404 | + | | --- | --- | --- | | |
| 405 | + | | *No access to KVM* | `/dev/kvm` is there, and your account may not use it. | `sudo usermod -aG kvm $USER`, then log out and back in. | | |
| 406 | + | | *KVM isn't loaded* | `/dev/kvm`, though your processor can run VMs. | `sudo modprobe kvm_intel` (Intel) or `sudo modprobe kvm_amd` (AMD). If it says the firmware disabled it, [turn on virtualization](#turning-on-virtualization). | | |
| 407 | + | | *QEMU isn't installed* | QEMU, which runs the VM. | `sudo apt install qemu-system-x86` (`qemu-system-arm` on Arm) on Debian and Ubuntu, `sudo dnf install qemu-kvm` on Fedora, `sudo pacman -S qemu-base` on Arch, `sudo zypper install qemu-kvm` on openSUSE. | | |
| 408 | + | | *Virtualization is off* | Your processor says it can't run VMs: virtualization is off in the firmware, or it has none. | [Turn on virtualization](#turning-on-virtualization), or install Sysbox. | | |
| 409 | + | | *This VM can't run the sandbox* | Linux in a VM whose host doesn't pass virtualization through. | [Turn on nested virtualization](#in-a-virtual-machine), or install Sysbox. | | |
| 410 | + | ||
| 411 | + | #### On macOS | |
| 412 | + | ||
| 413 | + | | The card says | What is missing | What to do | | |
| 414 | + | | --- | --- | --- | | |
| 415 | + | | *vfkit isn't installed*, with **Get Homebrew** | vfkit, and Homebrew, which installs it. | **Get Homebrew** and install it, then `brew install vfkit` in Terminal (**Copy command**). | | |
| 416 | + | | *vfkit isn't installed* | vfkit, which runs the VM with Apple's Virtualization framework. | `brew install vfkit` in Terminal (**Copy command**). | | |
| 417 | + | | *macOS needs an update* | macOS 13 or later. | **Open Software Update**, update, and open g1t again. | | |
| 418 | + | ||
| 419 | + | The VM's image is the one for your Mac's processor, Apple silicon or Intel; | |
| 420 | + | while a release has none for it yet, the card says *Waiting for image*. | |
| 421 | + | Every Mac g1t runs on has virtualization on. | |
| 422 | + | ||
| 423 | + | #### Turning on virtualization | |
| 424 | + | ||
| 425 | + | Virtualization (Intel VT-x, *Intel Virtualization Technology*; AMD-V, | |
| 426 | + | *SVM Mode*) is a setting in your computer's firmware, the BIOS or UEFI | |
| 427 | + | settings, which only you can change. Most computers have it on; some | |
| 428 | + | come with it off. | |
| 429 | + | ||
| 430 | + | 1. Open the firmware settings. On Windows: **Settings**, **System**, | |
| 431 | + | **Recovery**, **Advanced startup**, **Restart now**, then | |
| 432 | + | **Troubleshoot**, **Advanced options**, **UEFI Firmware Settings**, | |
| 433 | + | **Restart**. Or press its key as the computer starts: usually F2 (Dell, | |
| 434 | + | Acer, ASUS, Lenovo IdeaPad), F10 (HP), F1 (Lenovo ThinkPad), or Del | |
| 435 | + | (desktops: MSI, Gigabyte, ASRock). | |
| 436 | + | 2. Find **Intel Virtualization Technology** (sometimes **VT-x** or **Intel | |
| 437 | + | VT**) or **SVM Mode** (AMD), usually under **Advanced**, **CPU | |
| 438 | + | Configuration** or **Security**, and turn it on. | |
| 439 | + | 3. Save and exit, usually F10. Windows starts again, and the card changes | |
| 440 | + | by itself. | |
| 441 | + | ||
| 442 | + | #### In a virtual machine | |
| 443 | + | ||
| 444 | + | g1t inside a VM runs the sandbox as a VM inside that VM, which works only | |
| 445 | + | when the host passes virtualization through: *nested virtualization*. Turn | |
| 446 | + | it on for the VM on its host, with the VM off, then start it again: | |
| 447 | + | ||
| 448 | + | | Host | How | | |
| 449 | + | | --- | --- | | |
| 450 | + | | Hyper-V | `Set-VMProcessor -VMName <name> -ExposeVirtualizationExtensions $true`, in an admin PowerShell on the host. | | |
| 451 | + | | VMware Workstation, Fusion | The VM's settings, **Processors**, **Virtualize Intel VT-x/EPT or AMD-V/RVI**. | | |
| 452 | + | | VirtualBox | The VM's settings, **System**, **Processor**, **Enable Nested VT-x/AMD-V**, or `VBoxManage modifyvm <name> --nested-hw-virt on`. | | |
| 453 | + | | Proxmox, KVM | The VM's processor type `host`. | | |
| 454 | + | | A cloud VM | A machine type with nested virtualization: on Azure, Dv3 and Ev3 or later; on Google Cloud, nested virtualization turned on for the instance. Elsewhere, ask your provider: some offer it only on bare-metal machines. | | |
| 455 | + | ||
| 347 | 456 | ### Registering | |
| 348 | 457 | ||
| 349 | 458 | The first time, and whenever you change the workspace or how jobs run, | |
| ⋯ | |||
| 395 | 504 | ||
| 396 | 505 | | Where | What it shows | | |
| 397 | 506 | | --- | --- | | |
| 398 | − | | The title bar | Its dot, right beside **Settings**, in the colour of what it is doing; hover it for the words (*Runner running*, *Running a job*, *Running 2 jobs*, *Runner starting*, *Runner stopped*, *Runner needs you*), which a wide window shows beside it. It opens the runner menu. | | |
| 507 | + | | The title bar | Its dot, right beside **Settings**, in the colour of what it is doing; hover it for the words (*Runner running*, *Running a job*, *Running 2 jobs*, *Runner starting*, *Runner setting up*, *Runner stopped*, *Runner needs you*), which a wide window shows beside it. It opens the runner menu. | | |
| 399 | 508 | | The tray | A row with what the runner is doing, *Runner: Waiting for jobs*, which opens the runner menu; **Stop running jobs** or **Start running jobs** under it. The icon's hover text says it too. | | |
| 400 | − | | [The runner menu](#the-runner-menu) | At the top, what the whole runner is doing, as the dot says it, with how jobs run, its workspace and version, and its own **Stop** or **Start**. When it needs you, only what is about the whole runner: signed out, no workspace, turned off, stopped, the sandbox down, with the fix's buttons. Sandboxed, a line for each kind of job: its state (*Running a job*, *Waiting for jobs*, *Starting*, *Downloading image* with how far, *Updating*, *Waiting for image*, *Stopped*, *Not available*, *Problem*), the job it is running with a link to it, a newer runner waiting for it (*0.3.2 ready, installs after this job*), and when it isn't running why and the fix, with the fix's buttons and its own **Stop** or **Start**. Then when the app last looked for a newer runner, with **Check for runner updates**; the last lines of its log, following along, with **Open the whole log**; and **Runner settings…**. | | |
| 509 | + | | [The runner menu](#the-runner-menu) | At the top, what the whole runner is doing, as the dot says it, with how jobs run, its workspace and version, and its own **Stop** or **Start**. When it needs you, only what is about the whole runner: signed out, no workspace, turned off, stopped, the sandbox down, with the fix's buttons. Sandboxed, a line for each kind of job: its state (*Running a job*, *Waiting for jobs*, *Starting*, *Downloading image* with how far, *Updating*, *Waiting for image*, *Needs setting up*, *Setting up*, *Restart needed*, *Stopped*, *Not available*, *Problem*), the job it is running with a link to it, a newer runner waiting for it (*0.3.2 ready, installs after this job*), and when it isn't running why and the fix, with the fix's buttons and its own **Stop** or **Start**. Then when the app last looked for a newer runner, with **Check for runner updates**; the last lines of its log, following along, with **Open the whole log**; and **Runner settings…**. | | |
| 401 | 510 | | [The runner's settings](#runner-settings) | The same, with a card for each kind of job side by side, which adds its runner's name on g1t, version, updates and isolation; then **Run jobs on this computer while g1t is open**, how jobs run, the workspace, and your computer's name, the runner's version and when the app last looked for a newer one, with **View on g1t**. In Docker or directly on this computer, one line says what it is doing, or why it isn't running and the fix. | | |
| 402 | 511 | | The workspace's [Runners page](/guides/self-hosted-runners/#the-runners-page) | **Desktops**: every computer with the app in the workspace, with what each is doing, or why it isn't running and the fix, in the same words; a sandboxed one's kinds of job as cards of their own, as in the settings, with a link to the run each is running, its version, when the app last looked for updates and a newer one waiting. Inside the app, this computer comes first, live, with its buttons, each kind's **Stop** or **Start** and **Check for runner updates** among them. A computer whose g1t is closed says *g1t is closed on* its name. | | |
| 403 | 512 | ||
| ⋯ | |||
| 421 | 530 | | *Couldn't register with* a workspace: *only its owners can* | You aren't an owner of it. | Ask an owner to make you one, or **Choose workspace**. | | |
| 422 | 531 | | *Couldn't register with* a workspace, with g1t's reason | g1t refused the registration, such as for too many tokens in an hour. | **Retry**; it also tries again by itself. | | |
| 423 | 532 | | *Not running: Docker isn't running* | Running jobs in Docker needs Docker, and it isn't running or isn't installed. | **Open Docker** (Docker Desktop), or **Get Docker**. It starts by itself once Docker answers. | | |
| 424 | − | | *Not running: WSL isn't installed* | Windows: Linux jobs run in a WSL distribution of the runner's own. | Run `wsl --install --no-distribution` in an admin terminal (**Copy command**), then restart Windows. Every edition of Windows 11 has it, Home included. | | |
| 533 | + | | *Needs setting up*, *Setting up*, *Restart needed* on a card, and *Not running:* what it needs (*WSL isn't installed*, *virtualization is off*, …) | Your computer lacks something the sandbox needs: WSL, a Windows feature, virtualization, KVM, QEMU, vfkit. | **Set up** on Windows, or the card's steps. [Getting the runner running](#getting-the-runner-running) has a row for each, on each system. | | |
| 425 | 534 | | *Not running: WSL uses mirrored networking* | `networkingMode=mirrored` in `%USERPROFILE%\.wslconfig` lets Linux reach your computer's own `localhost`. | Remove it, run `wsl --shutdown`, then **Retry**. | | |
| 426 | − | | *Not running: virtualization is off* | The sandbox is a small VM, and virtualization is off in your computer's firmware, or Virtual Machine Platform is off in Windows features. | Turn them on (**Open Windows features** on Windows), then restart. | | |
| 427 | − | | *Not running: vfkit isn't installed* | macOS: the sandbox's VM runs under vfkit. | Run `brew install vfkit` (**Copy command**). It starts by itself once vfkit is there. | | |
| 428 | − | | *Not running: no Sysbox or KVM* | Linux: the sandbox needs Sysbox in Docker, or QEMU with KVM. | **Install Sysbox**, or turn on virtualization and install QEMU. | | |
| 535 | + | | *Not running: WSL isn't installed*, *virtualization is off*, *vfkit isn't installed*, *no Sysbox or KVM*, with **Copy command** or **Open Windows features** | The same, said by an older `g1t-runner`, from before it named what a computer lacks. | Its fix; the app downloads a newer `g1t-runner` by itself, which brings **Set up**. | | |
| 429 | 536 | | *Not running: the sandbox's image didn't download* | The image couldn't be fetched from g1t, with the reason. | It tries again by itself, waiting longer each time; check that your computer can reach g1t. **Retry** tries now. | | |
| 430 | 537 | | *Not running: the sandbox's image didn't check out* | The image that downloaded isn't the one the release signed (its signature or SHA-256), so it wasn't used. | It downloads it again by itself. If it keeps happening, something between your computer and g1t, such as a proxy or antivirus, is changing downloads: **Report** it. | | |
| 431 | 538 | | *Not running: the sandbox can't run here* | Something else the sandbox needs, in its own words. | What it says. | | |
| 432 | 539 | | *Not running: the sandbox failed* | The sandbox stopped, or didn't start, with its reason. | It starts again by itself, waiting longer each time. **Retry**, or **View log**. | | |
| 433 | 540 | | *Not running: this computer has another sandbox* | A sandbox this copy of g1t has no record of making: one you made with `g1t-runner sandbox up`, or one left from before g1t was installed again. A computer has one. | **Remove it and start again**, which asks first; its old runners stay listed on g1t, offline, until you remove them there. Or keep it, and turn the app's runner off. | | |
| 434 | 541 | | *Not running: this g1t-runner has no sandbox* | The app's `g1t-runner` is from before the sandbox. | It downloads a newer one by itself; **Retry** looks now. | | |
| 435 | − | | *Windows jobs: Not available*: *Windows Sandbox is off*, *Windows needs an update* | Windows Sandbox is off, this Windows is Home, or it is older than 24H2. Linux jobs still run. | On Pro, Enterprise or Education, turn on Windows Sandbox (**Open Windows features**) and restart, or update Windows. | | |
| 542 | + | | *Windows jobs: Not available*: *Not in Windows Home* | Windows Home has no Windows Sandbox. Linux jobs still run. | Nothing, for Linux jobs; Windows jobs need Pro, Enterprise or Education. | | |
| 436 | 543 | | *Windows jobs: Problem*: *Its runner didn't start* or *Its runner stopped* | The runner for Windows jobs didn't start, with why, or stopped by itself, while Linux jobs run. | It is started again by itself in a few minutes; **Retry** now, or **View log**. | | |
| 437 | 544 | | *macOS jobs: Not available*: *Not released yet* | Sandboxed macOS jobs aren't released. | Nothing: they come with a `g1t-runner` update, which the app downloads by itself. | | |
| 438 | 545 | | *Offline:* the server *unreachable* | Your computer can't reach g1t. | Check the connection; it carries on by itself. **Retry** tries now. | | |
| 62 | 62 | //! "resources": { "cpus": 4, "memory_mib": 8192, "disk_gib": 64 }, | |
| 63 | 63 | //! "capabilities": [ | |
| 64 | 64 | //! { "id": "linux", "available": true, "state": "running", "isolation": "nested-container", | |
| 65 | − | //! "runner_version": "0.3.0", "job": null, "cause": null, "fix": null, "turned_off": false }, | |
| 65 | + | //! "runner_version": "0.3.0", "job": null, "cause": null, "fix": null, "turned_off": false, "needs": [] }, | |
| 66 | 66 | //! { "id": "windows", "available": false, "state": "absent", "isolation": "vm", | |
| 67 | 67 | //! "runner_version": null, "job": null, "turned_off": false, | |
| 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, …" } | |
| 68 | + | //! "cause": "Windows Sandbox is not turned on, and Windows jobs each run in one.", | |
| 69 | + | //! "fix": "Turn on Windows Sandbox under Settings, System, Optional features, …", | |
| 70 | + | //! "needs": ["windows_sandbox"] } | |
| 70 | 71 | //! ], | |
| 71 | 72 | //! "state_dir": "C:\\Users\\a\\.g1t-runner\\sandbox", | |
| 72 | 73 | //! "warnings": [] | |
| ⋯ | |||
| 84 | 85 | //! empty when nothing does, and left out by launchers before it. | |
| 85 | 86 | //! `turned_off` is true for a capability someone stopped (`down --only`, | |
| 86 | 87 | //! `--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 | |
| 88 | + | //! failing. `needs` names what the computer still lacks for a capability, | |
| 89 | + | //! first what must come first (`wsl`, `vm_platform`, `virtualization`, | |
| 90 | + | //! `windows_edition`, `restart`, …; needs.rs lists them all), found before | |
| 91 | + | //! anything is downloaded; `cause` and `fix` say the first. It is empty | |
| 92 | + | //! when nothing is missing, and left out by launchers before it. `job` is | |
| 93 | + | //! what the runner is running now, or null. Fields | |
| 88 | 94 | //! are only ever added; `schema` changes if one changes meaning. `up`, | |
| 89 | 95 | //! `down` and `update` print the same object with `--json` when they are | |
| 90 | 96 | //! done, and exit 1 with `error` set when they fail. | |
| ⋯ | |||
| 107 | 113 | use serde::{Deserialize, Serialize}; | |
| 108 | 114 | ||
| 109 | 115 | use super::native::{Availability, MacVm, WsbFound}; | |
| 116 | + | use super::needs::{self, Need, WindowsFacts}; | |
| 110 | 117 | use super::private::{self, write_private}; | |
| 111 | 118 | use super::{JobStatus, Options, RunnerStatus, VERSION, config, log}; | |
| 112 | 119 | use guest::{GuestStatus, Settings}; | |
| ⋯ | |||
| 276 | 283 | /// Someone stopped it (`down --only`, `--no-native`): `stopped` until | |
| 277 | 284 | /// `up --only` turns it on again. Older launchers leave it out. | |
| 278 | 285 | pub turned_off: bool, | |
| 286 | + | /// What the computer still lacks for it, by name, first what must | |
| 287 | + | /// come first (needs.rs): `cause` and `fix` say the first. Empty when | |
| 288 | + | /// nothing is missing; older launchers leave it out. | |
| 289 | + | #[serde(default)] | |
| 290 | + | pub needs: Vec<Need>, | |
| 279 | 291 | } | |
| 280 | 292 | ||
| 281 | 293 | // --- The computer ------------------------------------------------------------- | |
| ⋯ | |||
| 443 | 455 | } | |
| 444 | 456 | ||
| 445 | 457 | /// What this OS's native sandbox is called, and whether it is here. | |
| 446 | − | fn native_capability() -> Option<(&'static str, Availability)> { | |
| 458 | + | fn native_capability(dir: &Path) -> Option<(&'static str, Availability)> { | |
| 447 | 459 | match native_id()? { | |
| 448 | − | "windows" => Some(("windows", WsbFound::look().availability())), | |
| 460 | + | "windows" => { | |
| 461 | + | let found = WsbFound::look(); | |
| 462 | + | let available = found.availability(); | |
| 463 | + | if available.available { | |
| 464 | + | return Some(("windows", available)); | |
| 465 | + | } | |
| 466 | + | // What exactly is missing: asked of Windows only now, as it takes a second. | |
| 467 | + | let windows = WindowsFacts::look(); | |
| 468 | + | let needs = needs::windows_sandbox_needs(&windows, found.feature, found.cli, needs::restart_pending(dir, &windows)); | |
| 469 | + | Some(("windows", if needs.is_empty() { available } else { Availability::needing(needs, "windows") })) | |
| 470 | + | } | |
| 449 | 471 | os => Some((os, MacVm::availability())), | |
| 450 | 472 | } | |
| 451 | 473 | } | |
| ⋯ | |||
| 604 | 626 | if state == "running" || (linux_off && state == "stopped") { | |
| 605 | 627 | error = None; | |
| 606 | 628 | } | |
| 629 | + | let (mut needs, mut cause, mut fix) = (available.needs.clone(), available.cause.clone(), available.fix.clone()); | |
| 630 | + | // What can't be known before a VM starts, its error says: on Windows, | |
| 631 | + | // virtualization off in the firmware, or not passed through to this VM. | |
| 632 | + | if needs.is_empty() | |
| 633 | + | && cfg!(windows) | |
| 634 | + | && state == "error" | |
| 635 | + | && let Some(said) = error.as_deref().filter(|e| needs::about_virtualization(e)) | |
| 636 | + | { | |
| 637 | + | let need = WindowsFacts::look().no_virtualization(); | |
| 638 | + | let (words, to_do) = need.said("linux"); | |
| 639 | + | needs = vec![need]; | |
| 640 | + | cause = Some(format!("{words} ({said})")); | |
| 641 | + | fix = Some(to_do); | |
| 642 | + | } | |
| 607 | 643 | let linux = Capability { | |
| 608 | 644 | id: "linux".into(), | |
| 609 | 645 | available: available.available, | |
| ⋯ | |||
| 611 | 647 | isolation: Some(runner.as_ref().map(|r| r.isolation.clone()).unwrap_or_else(|| "nested-container".into())), | |
| 612 | 648 | runner_version: runner.as_ref().map(|r| r.version.clone()), | |
| 613 | 649 | job: runner.as_ref().and_then(|r| r.job.clone()), | |
| 614 | − | cause: available.cause.clone(), | |
| 615 | − | fix: available.fix.clone(), | |
| 650 | + | cause, | |
| 651 | + | fix, | |
| 616 | 652 | turned_off: linux_off, | |
| 653 | + | needs, | |
| 617 | 654 | }; | |
| 618 | 655 | let mut capabilities = vec![linux.clone()]; | |
| 619 | − | if let Some((os, found)) = native_capability() { | |
| 656 | + | if let Some((os, found)) = native_capability(dir) { | |
| 620 | 657 | let folder = native_dir(dir); | |
| 621 | 658 | let native_status: Option<RunnerStatus> = std::fs::read(super::status_path(&folder)).ok().and_then(|b| serde_json::from_slice(&b).ok()); | |
| 622 | 659 | let registered = config::load(&folder).is_ok(); | |
| ⋯ | |||
| 638 | 675 | cause, | |
| 639 | 676 | fix, | |
| 640 | 677 | turned_off: off, | |
| 678 | + | needs: if found.available { vec![] } else { found.needs }, | |
| 641 | 679 | }); | |
| 642 | 680 | } | |
| 643 | 681 | Status { | |
| ⋯ | |||
| 919 | 957 | Some(only) => only.contains(&kind), | |
| 920 | 958 | None => !saved.is_off(kind), | |
| 921 | 959 | }; | |
| 922 | − | let (want_linux, native) = (wanted("linux"), native_capability().filter(|(os, _)| wanted(os))); | |
| 960 | + | let (want_linux, native) = (wanted("linux"), native_capability(dir).filter(|(os, _)| wanted(os))); | |
| 923 | 961 | if let Some(image) = options.value("runner-image") { | |
| 924 | 962 | saved.runner_image = Some(image.to_owned()); | |
| 925 | 963 | } | |
| ⋯ | |||
| 1225 | 1263 | for field in ["schema", "state", "backend", "runner_version", "launcher_version", "image_version", "isolation", "job", "error", "resources", "capabilities", "state_dir", "warnings"] { | |
| 1226 | 1264 | assert!(value.get(field).is_some(), "{field}"); | |
| 1227 | 1265 | } | |
| 1228 | − | for field in ["id", "available", "state", "isolation", "runner_version", "job", "cause", "fix", "turned_off"] { | |
| 1266 | + | for field in ["id", "available", "state", "isolation", "runner_version", "job", "cause", "fix", "turned_off", "needs"] { | |
| 1229 | 1267 | assert!(value["capabilities"][0].get(field).is_some(), "{field}"); | |
| 1230 | 1268 | } | |
| 1231 | 1269 | assert_eq!(value["resources"]["memory_mib"], 0); | |
| 35 | 35 | use super::image::{self, Local}; | |
| 36 | 36 | use super::{Backend, Resources, StopMode, process_alive}; | |
| 37 | 37 | use crate::selfhosted::native::Availability; | |
| 38 | + | use crate::selfhosted::needs::{self, KvmFacts}; | |
| 38 | 39 | ||
| 39 | 40 | /// The seed disk's size: the settings' JSON, then zeros. | |
| 40 | 41 | pub const SEED_BYTES: usize = 1 << 20; | |
| ⋯ | |||
| 170 | 171 | if image::arch() == "arm64" { "qemu-system-aarch64" } else { "qemu-system-x86_64" } | |
| 171 | 172 | } | |
| 172 | 173 | ||
| 174 | + | /// vfkit: on the PATH, else where Homebrew puts it, since a program the | |
| 175 | + | /// desktop app starts on a Mac has only the system's folders on its PATH. | |
| 176 | + | fn vfkit_program() -> String { | |
| 177 | + | if answers("vfkit") { | |
| 178 | + | return "vfkit".into(); | |
| 179 | + | } | |
| 180 | + | needs::HOMEBREW_BINS | |
| 181 | + | .iter() | |
| 182 | + | .map(|bin| Path::new(bin).join("vfkit")) | |
| 183 | + | .find(|path| path.exists()) | |
| 184 | + | .map_or_else(|| "vfkit".into(), |path| path.display().to_string()) | |
| 185 | + | } | |
| 186 | + | ||
| 173 | 187 | fn answers(program: &str) -> bool { | |
| 174 | 188 | Command::new(program).arg("--version").stdout(Stdio::null()).stderr(Stdio::null()).status().is_ok_and(|s| s.success()) | |
| 175 | 189 | } | |
| ⋯ | |||
| 242 | 256 | } | |
| 243 | 257 | ||
| 244 | 258 | fn availability(&self) -> Availability { | |
| 245 | − | match self.hypervisor { | |
| 246 | − | Hypervisor::Vfkit if !answers("vfkit") => Availability::no( | |
| 247 | − | "vfkit, which runs the sandbox's VM with Apple's Virtualization framework, is not installed.", | |
| 248 | − | "Install it with `brew install vfkit` (macOS 13 or later), then start the sandbox again.", | |
| 249 | − | ), | |
| 250 | − | Hypervisor::Qemu if !Path::new("/dev/kvm").exists() => Availability::no( | |
| 251 | − | "This machine has neither Sysbox nor KVM, so there is nothing to sandbox the runner with.", | |
| 252 | − | "Install Sysbox 0.7 or later (https://github.com/nestybox/sysbox), or turn on virtualization and install QEMU so /dev/kvm exists.", | |
| 253 | − | ), | |
| 254 | − | Hypervisor::Qemu if std::fs::OpenOptions::new().read(true).write(true).open("/dev/kvm").is_err() => { | |
| 255 | − | Availability::no("This user may not use /dev/kvm.", "Add this user to the kvm group, then log in again.") | |
| 259 | + | let needs = match self.hypervisor { | |
| 260 | + | Hypervisor::Vfkit => { | |
| 261 | + | let macos = Command::new("sw_vers").arg("-productVersion").output().ok().and_then(|o| needs::macos_major(&String::from_utf8_lossy(&o.stdout))); | |
| 262 | + | let homebrew = answers("brew") || needs::HOMEBREW_BINS.iter().any(|bin| Path::new(bin).join("brew").exists()); | |
| 263 | + | needs::vfkit_needs(macos, answers(&vfkit_program()), homebrew) | |
| 264 | + | } | |
| 265 | + | Hypervisor::Qemu => { | |
| 266 | + | let device = Path::new("/dev/kvm").exists(); | |
| 267 | + | let (cpu, guest) = needs::cpu_flags(&std::fs::read_to_string("/proc/cpuinfo").unwrap_or_default()); | |
| 268 | + | needs::kvm_needs(&KvmFacts { | |
| 269 | + | device, | |
| 270 | + | usable: device && std::fs::OpenOptions::new().read(true).write(true).open("/dev/kvm").is_ok(), | |
| 271 | + | cpu, | |
| 272 | + | guest, | |
| 273 | + | qemu: answers(qemu_program()), | |
| 274 | + | }) | |
| 256 | 275 | } | |
| 257 | − | Hypervisor::Qemu if !answers(qemu_program()) => Availability::no( | |
| 258 | − | format!("{} is not installed.", qemu_program()), | |
| 259 | − | "Install QEMU (qemu-system on Debian and Ubuntu, qemu-kvm on Fedora), or Sysbox 0.7 or later.", | |
| 260 | − | ), | |
| 261 | − | _ => Availability::yes(), | |
| 262 | − | } | |
| 276 | + | }; | |
| 277 | + | Availability::needing(needs, "linux") | |
| 263 | 278 | } | |
| 264 | 279 | ||
| 265 | 280 | fn exists(&self) -> bool { | |
| ⋯ | |||
| 333 | 348 | let _ = std::fs::remove_file(&paths.control); | |
| 334 | 349 | let _ = std::fs::remove_file(&paths.console); | |
| 335 | 350 | let (program, args) = match self.hypervisor { | |
| 336 | − | Hypervisor::Vfkit => ("vfkit", vfkit_args(&paths, &resources)), | |
| 337 | − | Hypervisor::Qemu => (qemu_program(), qemu_args(image::arch(), &paths, &resources)), | |
| 351 | + | Hypervisor::Vfkit => (vfkit_program(), vfkit_args(&paths, &resources)), | |
| 352 | + | Hypervisor::Qemu => (qemu_program().to_owned(), qemu_args(image::arch(), &paths, &resources)), | |
| 338 | 353 | }; | |
| 339 | 354 | let log = std::fs::File::create(self.dir.join("sandbox.log"))?; | |
| 340 | 355 | let mut command = Command::new(program); | |
| 35 | 35 | use super::image::{self, Local}; | |
| 36 | 36 | use super::{Backend, Resources, StopMode, process_alive}; | |
| 37 | 37 | use crate::selfhosted::native::{Availability, hide_window}; | |
| 38 | + | use crate::selfhosted::needs::{self, WindowsFacts, WslFacts}; | |
| 38 | 39 | ||
| 39 | 40 | /// What the distribution is called. | |
| 40 | 41 | pub const DISTRO: &str = "g1t-sandbox"; | |
| ⋯ | |||
| 65 | 66 | .collect() | |
| 66 | 67 | } | |
| 67 | 68 | ||
| 69 | + | /// Whether `wsl.exe --list --verbose` lists a WSL 2 distribution: WSL 2 | |
| 70 | + | /// works here. | |
| 71 | + | pub fn has_wsl2(text: &str) -> bool { | |
| 72 | + | text.replace('\0', "").lines().skip(1).any(|line| line.trim().trim_start_matches('*').split_whitespace().nth(2) == Some("2")) | |
| 73 | + | } | |
| 74 | + | ||
| 68 | 75 | /// Whether `.wslconfig` puts WSL on mirrored networking, where Linux | |
| 69 | 76 | /// reaches Windows' own `localhost`. | |
| 70 | 77 | pub fn mirrored(wslconfig: &str) -> bool { | |
| ⋯ | |||
| 127 | 134 | ||
| 128 | 135 | fn availability(&self) -> Availability { | |
| 129 | 136 | let installed = wsl().arg("--status").stdout(Stdio::null()).stderr(Stdio::null()).status().is_ok_and(|s| s.success()); | |
| 130 | − | if !installed { | |
| 131 | − | return Availability::no( | |
| 132 | − | "WSL is not installed, and the sandbox for Linux jobs is a WSL distribution of its own.", | |
| 133 | − | "Open a terminal as administrator and run `wsl --install --no-distribution`, then restart Windows. It works on every edition of Windows 11, Home included.", | |
| 134 | − | ); | |
| 135 | − | } | |
| 136 | 137 | let config = std::env::var_os("USERPROFILE").map(|home| PathBuf::from(home).join(".wslconfig")); | |
| 137 | − | if config.and_then(|c| std::fs::read_to_string(c).ok()).is_some_and(|text| mirrored(&text)) { | |
| 138 | + | if installed && config.and_then(|c| std::fs::read_to_string(c).ok()).is_some_and(|text| mirrored(&text)) { | |
| 138 | 139 | return Availability::no( | |
| 139 | 140 | "WSL is set to mirrored networking, in which Linux reaches this computer's own localhost.", | |
| 140 | 141 | "Remove networkingMode=mirrored from %USERPROFILE%\\.wslconfig, run `wsl --shutdown`, and start the sandbox again.", | |
| 141 | 142 | ); | |
| 142 | 143 | } | |
| 143 | − | Availability::yes() | |
| 144 | + | // Made already: WSL works, and what goes wrong from here starting it says. | |
| 145 | + | if installed && self.exists() { | |
| 146 | + | return Availability::yes(); | |
| 147 | + | } | |
| 148 | + | let version = || wsl().arg("--version").stderr(Stdio::null()).output().ok().filter(|o| o.status.success()).and_then(|o| needs::wsl_version(&String::from_utf8_lossy(&o.stdout))); | |
| 149 | + | let listed = || wsl().args(["--list", "--verbose"]).output().map(|o| has_wsl2(&String::from_utf8_lossy(&o.stdout))).unwrap_or(false); | |
| 150 | + | let facts = WslFacts { installed, version: if installed { version() } else { None }, has_wsl2: installed && listed() }; | |
| 151 | + | let windows = WindowsFacts::look(); | |
| 152 | + | Availability::needing(needs::wsl_needs(&windows, &facts, needs::restart_pending(&self.dir, &windows)), "linux") | |
| 144 | 153 | } | |
| 145 | 154 | ||
| 146 | 155 | fn exists(&self) -> bool { | |
| ⋯ | |||
| 269 | 278 | assert_eq!(list[0], ("Ubuntu".to_owned(), "Stopped".to_owned())); | |
| 270 | 279 | assert_eq!(list[1], ("g1t-sandbox".to_owned(), "Running".to_owned())); | |
| 271 | 280 | assert_eq!(list.len(), 3); | |
| 281 | + | assert!(has_wsl2(text)); | |
| 282 | + | assert!(!has_wsl2(" NAME STATE VERSION\n* Ubuntu Stopped 1\n")); | |
| 283 | + | assert!(!has_wsl2("")); | |
| 272 | 284 | // wsl.exe without WSL_UTF8 writes UTF-16: its NULs are dropped. | |
| 273 | 285 | assert_eq!(parse_list("N\0A\0M\0E\0\n\0g\x001\0t\0 \0R\0u\0n\0\n")[0].0, "g1t"); | |
| 274 | 286 | } | |
| 30 | 30 | mod exec; | |
| 31 | 31 | mod launcher; | |
| 32 | 32 | mod native; | |
| 33 | + | mod needs; | |
| 33 | 34 | mod private; | |
| 34 | 35 | mod sandbox; | |
| 35 | 36 | mod service; |
| 34 | 34 | use anyhow::{Context, Result, anyhow, bail}; | |
| 35 | 35 | use serde::Serialize; | |
| 36 | 36 | ||
| 37 | + | use super::needs::Need; | |
| 37 | 38 | use super::sandbox::{Isolation, SandboxDriver}; | |
| 38 | 39 | ||
| 39 | 40 | /// Whether a kind of sandbox can be made here, and if not, why and what | |
| ⋯ | |||
| 46 | 47 | pub cause: Option<String>, | |
| 47 | 48 | /// What the person can do about it, in a sentence. | |
| 48 | 49 | pub fix: Option<String>, | |
| 50 | + | /// What is missing, by name, first what must come first (needs.rs). | |
| 51 | + | pub needs: Vec<Need>, | |
| 49 | 52 | } | |
| 50 | 53 | ||
| 51 | 54 | impl Availability { | |
| 52 | 55 | pub fn yes() -> Availability { | |
| 53 | − | Availability { available: true, cause: None, fix: None } | |
| 56 | + | Availability { available: true, cause: None, fix: None, needs: vec![] } | |
| 54 | 57 | } | |
| 55 | 58 | pub fn no(cause: impl Into<String>, fix: impl Into<String>) -> Availability { | |
| 56 | − | Availability { available: false, cause: Some(cause.into()), fix: Some(fix.into()) } | |
| 59 | + | Availability { available: false, cause: Some(cause.into()), fix: Some(fix.into()), needs: vec![] } | |
| 60 | + | } | |
| 61 | + | /// Not here until `needs` are met, said in the first one's words; here when there are none. | |
| 62 | + | pub fn needing(needs: Vec<Need>, kind: &str) -> Availability { | |
| 63 | + | let Some(first) = needs.first() else { return Availability::yes() }; | |
| 64 | + | let (cause, fix) = first.said(kind); | |
| 65 | + | Availability { needs, ..Availability::no(cause, fix) } | |
| 57 | 66 | } | |
| 58 | 67 | } | |
| 59 | 68 | ||
Loading the diff…
Comments
Sign in to comment.