| 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 | function Restart-Pending {{ |
| 141 | Set-Content -LiteralPath {marker} -Value (Get-CimInstance Win32_OperatingSystem).LastBootUpTime.ToUniversalTime().ToString('o') |
| 142 | }} |
| 143 | try {{ |
| 144 | foreach ($name in @({features})) {{ |
| 145 | $feature = Get-WindowsOptionalFeature -Online -FeatureName $name |
| 146 | if ($feature.State -ne 'Enabled') {{ |
| 147 | Say "Turning on $name." |
| 148 | $result = Enable-WindowsOptionalFeature -Online -FeatureName $name -All -NoRestart -WarningAction SilentlyContinue |
| 149 | if ($result.RestartNeeded) {{ $restart = $true }} |
| 150 | }} |
| 151 | }} |
| 152 | {wsl} |
| 153 | if ($restart) {{ |
| 154 | Restart-Pending |
| 155 | Say 'Done; Windows needs a restart to finish.' |
| 156 | exit {NEEDS_RESTART} |
| 157 | }} |
| 158 | Say 'Done.' |
| 159 | exit 0 |
| 160 | }} catch {{ |
| 161 | Say ('Failed: ' + $_.Exception.Message) |
| 162 | # What was turned on still takes the restart; after it, Set up offers what is left. |
| 163 | if ($restart) {{ |
| 164 | Restart-Pending |
| 165 | Say 'Windows needs a restart for what was turned on; Set up does the rest after it.' |
| 166 | exit {NEEDS_RESTART} |
| 167 | }} |
| 168 | exit 1 |
| 169 | }} |
| 170 | "#, |
| 171 | log = quoted(log), |
| 172 | marker = quoted(marker), |
| 173 | ) |
| 174 | } |
| 175 | |
| 176 | /// PowerShell's `-EncodedCommand`: UTF-16LE in base64. |
| 177 | pub fn encoded(script: &str) -> String { |
| 178 | use base64::Engine; |
| 179 | let bytes: Vec<u8> = script.encode_utf16().flat_map(u16::to_le_bytes).collect(); |
| 180 | base64::engine::general_purpose::STANDARD.encode(bytes) |
| 181 | } |
| 182 | |
| 183 | /// How Set up went. |
| 184 | #[derive(Clone, Debug, PartialEq, Eq)] |
| 185 | pub enum Outcome { |
| 186 | Done, |
| 187 | /// Done, and Windows needs a restart to finish. |
| 188 | Restart, |
| 189 | /// The person said No to Windows' prompt. |
| 190 | Declined, |
| 191 | Failed(String), |
| 192 | } |
| 193 | |
| 194 | /// What an exit code means. |
| 195 | pub fn outcome(code: u32) -> Outcome { |
| 196 | match code { |
| 197 | 0 => Outcome::Done, |
| 198 | NEEDS_RESTART => Outcome::Restart, |
| 199 | CANCELLED => Outcome::Declined, |
| 200 | other => Outcome::Failed(format!("Set up stopped (exit code {other}).")), |
| 201 | } |
| 202 | } |
| 203 | |
| 204 | /// Runs the script as administrator, asking Windows first (its prompt, |
| 205 | /// in front of `window`), and waits for it. |
| 206 | #[cfg(windows)] |
| 207 | pub fn run_elevated(script: &str, window: isize) -> Outcome { |
| 208 | use std::ffi::OsStr; |
| 209 | use std::os::windows::ffi::OsStrExt; |
| 210 | |
| 211 | #[repr(C)] |
| 212 | struct ShellExecuteInfo { |
| 213 | size: u32, |
| 214 | mask: u32, |
| 215 | window: isize, |
| 216 | verb: *const u16, |
| 217 | file: *const u16, |
| 218 | parameters: *const u16, |
| 219 | directory: *const u16, |
| 220 | show: i32, |
| 221 | instance: isize, |
| 222 | id_list: *mut std::ffi::c_void, |
| 223 | class: *const u16, |
| 224 | class_key: isize, |
| 225 | hot_key: u32, |
| 226 | icon_or_monitor: isize, |
| 227 | process: isize, |
| 228 | } |
| 229 | #[link(name = "shell32")] |
| 230 | unsafe extern "system" { |
| 231 | fn ShellExecuteExW(info: *mut ShellExecuteInfo) -> i32; |
| 232 | } |
| 233 | unsafe extern "system" { |
| 234 | fn WaitForSingleObject(handle: isize, millis: u32) -> u32; |
| 235 | fn GetExitCodeProcess(handle: isize, code: *mut u32) -> i32; |
| 236 | fn CloseHandle(handle: isize) -> i32; |
| 237 | fn GetLastError() -> u32; |
| 238 | } |
| 239 | const SEE_MASK_NOCLOSEPROCESS: u32 = 0x40; |
| 240 | const SEE_MASK_NOASYNC: u32 = 0x100; |
| 241 | const SW_HIDE: i32 = 0; |
| 242 | const INFINITE: u32 = u32::MAX; |
| 243 | |
| 244 | let wide = |text: &str| OsStr::new(text).encode_wide().chain(std::iter::once(0)).collect::<Vec<u16>>(); |
| 245 | let verb = wide("runas"); |
| 246 | let file = wide("powershell.exe"); |
| 247 | let parameters = wide(&format!("-NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand {}", encoded(script))); |
| 248 | let mut info = ShellExecuteInfo { |
| 249 | size: std::mem::size_of::<ShellExecuteInfo>() as u32, |
| 250 | mask: SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NOASYNC, |
| 251 | window, |
| 252 | verb: verb.as_ptr(), |
| 253 | file: file.as_ptr(), |
| 254 | parameters: parameters.as_ptr(), |
| 255 | directory: std::ptr::null(), |
| 256 | show: SW_HIDE, |
| 257 | instance: 0, |
| 258 | id_list: std::ptr::null_mut(), |
| 259 | class: std::ptr::null(), |
| 260 | class_key: 0, |
| 261 | hot_key: 0, |
| 262 | icon_or_monitor: 0, |
| 263 | process: 0, |
| 264 | }; |
| 265 | // SAFETY: `info` and the strings it points to live until the call returns. |
| 266 | if unsafe { ShellExecuteExW(&mut info) } == 0 { |
| 267 | let error = unsafe { GetLastError() }; |
| 268 | return if error == CANCELLED { Outcome::Declined } else { Outcome::Failed(format!("Windows couldn't start Set up (error {error}).")) }; |
| 269 | } |
| 270 | if info.process == 0 { |
| 271 | return Outcome::Failed("Windows started Set up without a process to wait for.".into()); |
| 272 | } |
| 273 | let mut code = 0u32; |
| 274 | // SAFETY: the process handle is ours until CloseHandle. |
| 275 | unsafe { |
| 276 | WaitForSingleObject(info.process, INFINITE); |
| 277 | GetExitCodeProcess(info.process, &mut code); |
| 278 | CloseHandle(info.process); |
| 279 | } |
| 280 | outcome(code) |
| 281 | } |
| 282 | |
| 283 | #[cfg(not(windows))] |
| 284 | pub fn run_elevated(_script: &str, _window: isize) -> Outcome { |
| 285 | Outcome::Failed("Set up is for Windows; the runner's card says what to do here.".into()) |
| 286 | } |
| 287 | |
| 288 | /// What Windows' own dialog asks before g1t restarts it. |
| 289 | pub const RESTART_QUESTION: &str = "Restart Windows now to finish setting up the runner?\n\nSave your work first. g1t opens again after the restart and carries on by itself."; |
| 290 | |
| 291 | /// Asks with Windows' own dialog, which no page can answer for the |
| 292 | /// person, and restarts Windows on Yes: on a thread of its own, as the |
| 293 | /// dialog waits for them. Restarting is asked this way wherever the button |
| 294 | /// is, the site's pages included. |
| 295 | pub fn ask_and_restart(app_exe: std::path::PathBuf) -> Result<(), String> { |
| 296 | if !cfg!(windows) { |
| 297 | return Err("Only Windows is restarted from g1t.".into()); |
| 298 | } |
| 299 | std::thread::Builder::new() |
| 300 | .name("g1t-restart-windows".into()) |
| 301 | .spawn(move || { |
| 302 | if asked_yes(RESTART_QUESTION) { |
| 303 | let _ = restart_windows(&app_exe); |
| 304 | } |
| 305 | }) |
| 306 | .map(|_| ()) |
| 307 | .map_err(|e| format!("Windows couldn't be asked: {e}")) |
| 308 | } |
| 309 | |
| 310 | #[cfg(windows)] |
| 311 | fn asked_yes(question: &str) -> bool { |
| 312 | use std::ffi::OsStr; |
| 313 | use std::os::windows::ffi::OsStrExt; |
| 314 | #[link(name = "user32")] |
| 315 | unsafe extern "system" { |
| 316 | fn MessageBoxW(window: isize, text: *const u16, caption: *const u16, kind: u32) -> i32; |
| 317 | } |
| 318 | const MB_YESNO: u32 = 0x4; |
| 319 | const MB_ICONQUESTION: u32 = 0x20; |
| 320 | const MB_DEFBUTTON2: u32 = 0x100; |
| 321 | const MB_SETFOREGROUND: u32 = 0x1_0000; |
| 322 | const MB_TOPMOST: u32 = 0x4_0000; |
| 323 | const IDYES: i32 = 6; |
| 324 | let wide = |text: &str| OsStr::new(text).encode_wide().chain(std::iter::once(0)).collect::<Vec<u16>>(); |
| 325 | let (text, caption) = (wide(question), wide("g1t")); |
| 326 | // SAFETY: both strings live until the call returns. No owner window: this |
| 327 | // isn't the window's thread, and the dialog stays on top by itself. |
| 328 | unsafe { MessageBoxW(0, text.as_ptr(), caption.as_ptr(), MB_YESNO | MB_ICONQUESTION | MB_DEFBUTTON2 | MB_SETFOREGROUND | MB_TOPMOST) == IDYES } |
| 329 | } |
| 330 | |
| 331 | #[cfg(not(windows))] |
| 332 | fn asked_yes(_question: &str) -> bool { |
| 333 | false |
| 334 | } |
| 335 | |
| 336 | /// Restarts Windows now. g1t opens again after, once (RunOnce), and |
| 337 | /// carries on setting up. |
| 338 | fn restart_windows(app_exe: &Path) -> Result<(), String> { |
| 339 | open_after_restart(app_exe); |
| 340 | let status = super::backend::quiet("shutdown.exe") |
| 341 | .args(["/r", "/t", "0", "/c", "g1t is restarting Windows to finish setting up the runner."]) |
| 342 | .status() |
| 343 | .map_err(|e| format!("Windows couldn't be restarted: {e}"))?; |
| 344 | if status.success() { Ok(()) } else { Err("Windows didn't restart: restart it from the Start menu.".into()) } |
| 345 | } |
| 346 | |
| 347 | /// g1t opens once when the person next signs in to Windows, after the |
| 348 | /// restart (`RunOnce`, which Windows clears as it runs it), whether or not |
| 349 | /// it opens at sign-in anyway: setup carries on from there. |
| 350 | pub fn open_after_restart(app_exe: &Path) { |
| 351 | if cfg!(windows) { |
| 352 | let _ = super::backend::quiet("reg") |
| 353 | .args(["add", r"HKCU\Software\Microsoft\Windows\CurrentVersion\RunOnce", "/v", "g1t", "/t", "REG_SZ", "/d", &format!("\"{}\"", app_exe.display()), "/f"]) |
| 354 | .stdout(std::process::Stdio::null()) |
| 355 | .stderr(std::process::Stdio::null()) |
| 356 | .status(); |
| 357 | } |
| 358 | } |
| 359 | |
| 360 | #[cfg(test)] |
| 361 | mod tests { |
| 362 | use super::*; |
| 363 | |
| 364 | fn status(json: &str) -> Status { |
| 365 | super::super::launcher::parse(json).unwrap() |
| 366 | } |
| 367 | |
| 368 | #[test] |
| 369 | fn set_up_does_what_the_kinds_that_are_on_need() { |
| 370 | let clean = status( |
| 371 | r#"{"schema":1,"state":"error","capabilities":[ |
| 372 | {"id":"linux","available":false,"state":"error","needs":["vm_platform","wsl"]}, |
| 373 | {"id":"windows","available":false,"state":"absent","needs":["windows_sandbox"]}]}"#, |
| 374 | ); |
| 375 | let both = plan(&clean, |_| true); |
| 376 | assert_eq!(both.features, ["VirtualMachinePlatform", "Containers-DisposableClientVM"]); |
| 377 | assert_eq!(both.wsl, Some(Wsl::Install)); |
| 378 | assert_eq!(both.said(), "turns on Virtual Machine Platform and Windows Sandbox and installs WSL"); |
| 379 | // Windows jobs stopped by the person: their feature is left alone. |
| 380 | let linux = plan(&clean, |kind| kind == "linux"); |
| 381 | assert_eq!(linux.features, ["VirtualMachinePlatform"]); |
| 382 | assert_eq!(linux.said(), "turns on Virtual Machine Platform and installs WSL"); |
| 383 | let update = status(r#"{"schema":1,"state":"error","capabilities":[{"id":"linux","available":false,"needs":["wsl_update"]}]}"#); |
| 384 | assert_eq!(plan(&update, |_| true).said(), "updates WSL"); |
| 385 | // Home: nothing Set up can do for Windows jobs. |
| 386 | let home = status(r#"{"schema":1,"state":"running","capabilities":[{"id":"linux","available":true,"state":"running"},{"id":"windows","available":false,"needs":["windows_edition"]}]}"#); |
| 387 | assert!(plan(&home, |_| true).is_empty()); |
| 388 | assert!(fixable(&["vm_platform".into(), "wsl".into()])); |
| 389 | assert!(!fixable(&["virtualization".into(), "vm_platform".into()])); |
| 390 | assert!(!fixable(&[])); |
| 391 | } |
| 392 | |
| 393 | #[test] |
| 394 | fn the_script_is_fixed_and_says_a_restart_by_its_exit_code() { |
| 395 | let plan = Plan { features: vec!["VirtualMachinePlatform"], wsl: Some(Wsl::Install) }; |
| 396 | 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")); |
| 397 | assert!(script.contains(r"$log = 'C:\Users\o''neil\AppData\Local\sh.g1t\runner\setup.log'")); |
| 398 | assert!(script.contains("foreach ($name in @('VirtualMachinePlatform'))")); |
| 399 | assert!(script.contains("Install-Wsl '--install', '--no-distribution'")); |
| 400 | assert!(script.contains("exit 3010")); |
| 401 | assert!(!script.contains("{features}") && !script.contains("{wsl}")); |
| 402 | assert_eq!(outcome(0), Outcome::Done); |
| 403 | assert_eq!(outcome(3010), Outcome::Restart); |
| 404 | assert_eq!(outcome(1223), Outcome::Declined); |
| 405 | assert!(matches!(outcome(1), Outcome::Failed(_))); |
| 406 | // UTF-16LE in base64, as PowerShell reads it: "a" is 61 00. |
| 407 | assert_eq!(encoded("a"), "YQA="); |
| 408 | } |
| 409 | |
| 410 | /// Runs the script's WSL step as this user with `--version`, which |
| 411 | /// changes nothing: wsl.exe started hidden, what it said logged line by |
| 412 | /// line, and its exit code read. `cargo test -- --ignored the_wsl_step --nocapture`. |
| 413 | #[test] |
| 414 | #[ignore] |
| 415 | fn the_wsl_step_runs_wsl_and_logs_what_it_said() { |
| 416 | let dir = std::env::temp_dir().join(format!("g1t-setup-wsl-{}", std::process::id())); |
| 417 | std::fs::create_dir_all(&dir).unwrap(); |
| 418 | let (log, marker) = (dir.join("setup.log"), dir.join(RESTART_FILE)); |
| 419 | let script = script(&Plan { features: vec![], wsl: Some(Wsl::Update) }, &log, &marker).replace("Install-Wsl '--update'", "Install-Wsl '--version'"); |
| 420 | let code = super::super::backend::quiet("powershell.exe") |
| 421 | .args(["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", &encoded(&script)]) |
| 422 | .status() |
| 423 | .unwrap() |
| 424 | .code() |
| 425 | .unwrap(); |
| 426 | let said = std::fs::read_to_string(&log).unwrap(); |
| 427 | println!("exit {code}\n{said}"); |
| 428 | assert_eq!(outcome(code as u32), Outcome::Done, "{said}"); |
| 429 | assert!(said.contains("g1t setup: wsl: "), "{said}"); |
| 430 | assert!(said.trim_end().ends_with("g1t setup: Done."), "{said}"); |
| 431 | let _ = std::fs::remove_dir_all(&dir); |
| 432 | } |
| 433 | |
| 434 | /// Runs the script as this user, not as administrator, so it changes |
| 435 | /// nothing: it must parse, log, and fail plainly. Windows only: |
| 436 | /// `cargo test -- --ignored the_script_runs --nocapture`. |
| 437 | #[test] |
| 438 | #[ignore] |
| 439 | fn the_script_runs_and_without_an_administrator_says_why() { |
| 440 | let dir = std::env::temp_dir().join(format!("g1t-setup-{}", std::process::id())); |
| 441 | std::fs::create_dir_all(&dir).unwrap(); |
| 442 | let (log, marker) = (dir.join("setup.log"), dir.join(RESTART_FILE)); |
| 443 | let plan = Plan { features: vec!["VirtualMachinePlatform"], wsl: Some(Wsl::Update) }; |
| 444 | let code = super::super::backend::quiet("powershell.exe") |
| 445 | .args(["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", &encoded(&script(&plan, &log, &marker))]) |
| 446 | .status() |
| 447 | .unwrap() |
| 448 | .code() |
| 449 | .unwrap(); |
| 450 | let said = std::fs::read_to_string(&log).unwrap(); |
| 451 | println!("exit {code}\n{said}"); |
| 452 | assert_eq!(outcome(code as u32), Outcome::Failed("Set up stopped (exit code 1).".into())); |
| 453 | assert!(said.contains("g1t setup: Failed:"), "{said}"); |
| 454 | assert!(!marker.exists()); |
| 455 | let _ = std::fs::remove_dir_all(&dir); |
| 456 | } |
| 457 | } |