Skip to content

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

Commit

Getting the runner running, a pass for quality (#50). What Windows says of itself is kept in the sandbox's folder until Windows restarts, since none of it changes before then: a status while something is missing no longer starts PowerShell every few seconds (2.8 s the first time, 1 ms after). Windows jobs with Windows Sandbox on but virtualization off in the firmware now say so before a job, not as it fails. Set up that turned a feature on and then couldn't install WSL still asks for the restart, and offers WSL again after it. Restart now is asked with Windows' own dialog, wherever the button is, so no page can restart a computer by itself.

Checked:
cargo test -p g1t-runner (259 and 5), the app's tests (55), clippy on both; on this computer the facts read live and are read back from the folder in 1 ms, the script's WSL step runs wsl.exe unelevated and logs what it said, and unelevated the script changes nothing and says it needs an administrator.
G1t-Session:
ses_01m4yfkg80ev2t3yyd05fak46z
syntaqxcommitted Parent901359cBrowse files
6 files+171−140/6 viewed
+6−3
386386 /// What Windows can do itself for the sandbox, at once, behind its own
387387 /// administrator prompt (setup.rs): busy until the card says it is setting up.
388388 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.");
389+/// Asked with Windows' own dialog (setup.rs), which no page can answer for
390+/// the person, rather than a page's: done once the app has it.
391+const RESTART_WINDOWS: Action = Action::now("restart_windows", "Restart now");
391392 const RESTART_LATER: Action = Action::waits("restart_later", "Later");
392393 const WINDOWS_UPDATE: Action = Action::now("windows_update", "Open Windows Update");
393394 const WINDOWS_ACTIVATION: Action = Action::now("windows_activation", "Open Activation");
16511652 let linux = &restart.capabilities[0];
16521653 assert_eq!((linux.title.as_str(), linux.tone), ("Restart needed", Tone::Warning));
16531654 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+ // Windows' own dialog asks, not a page's, which a page could skip.
1656+ assert!(RESTART_WINDOWS.confirm.is_none() && !RESTART_WINDOWS.waits);
1657+ assert!(super::super::setup::RESTART_QUESTION.starts_with("Restart Windows now"));
16551658 assert_eq!(linux.fix.as_deref(), Some("Restart now, or later: g1t opens again after the restart and carries on by itself."));
16561659 let later = shown_for(needing(Some(&["restart"]), None), Facts { setup: SetUp { later: true, ..SetUp::default() }, ..facts() });
16571660 screen("windows-restart-later", &later);
+1−1
699699 "firmware_help" => browse("https://docs.g1t.sh/guides/desktop/#turning-on-virtualization")?,
700700 "nested_help" => browse("https://docs.g1t.sh/guides/desktop/#in-a-virtual-machine")?,
701701 "set_up" => set_up(app)?,
702− "restart_windows" => setup::restart_windows(&std::env::current_exe().map_err(|e| e.to_string())?)?,
702+ "restart_windows" => setup::ask_and_restart(std::env::current_exe().map_err(|e| e.to_string())?)?,
703703 "restart_later" => {
704704 if let Ok(exe) = std::env::current_exe() {
705705 setup::open_after_restart(&exe);
+82−4
137137 throw 'WSL could not be installed or updated.'
138138 }}
139139 $restart = $false
140+function Restart-Pending {{
141+ Set-Content -LiteralPath {marker} -Value (Get-CimInstance Win32_OperatingSystem).LastBootUpTime.ToUniversalTime().ToString('o')
142+}}
140143 try {{
141144 foreach ($name in @({features})) {{
142145 $feature = Get-WindowsOptionalFeature -Online -FeatureName $name
148151 }}
149152 {wsl}
150153 if ($restart) {{
151− Set-Content -LiteralPath {marker} -Value (Get-CimInstance Win32_OperatingSystem).LastBootUpTime.ToUniversalTime().ToString('o')
154+ Restart-Pending
152155 Say 'Done; Windows needs a restart to finish.'
153156 exit {NEEDS_RESTART}
154157 }}
156159 exit 0
157160 }} catch {{
158161 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+ }}
159168 exit 1
160169 }}
161170 "#,
276285 Outcome::Failed("Set up is for Windows; the runner's card says what to do here.".into())
277286 }
278287
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> {
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> {
282296 if !cfg!(windows) {
283297 return Err("Only Windows is restarted from g1t.".into());
284298 }
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> {
285339 open_after_restart(app_exe);
286340 let status = super::backend::quiet("shutdown.exe")
287341 .args(["/r", "/t", "0", "/c", "g1t is restarting Windows to finish setting up the runner."])
353407 assert_eq!(encoded("a"), "YQA=");
354408 }
355409
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+
356434 /// Runs the script as this user, not as administrator, so it changes
357435 /// nothing: it must parse, log, and fail plainly. Windows only:
358436 /// `cargo test -- --ignored the_script_runs --nocapture`.
+6−4
460460 "windows" => {
461461 let found = WsbFound::look();
462462 let available = found.availability();
463+ // What exactly is missing, from what Windows says of itself (kept until it restarts).
464+ let windows = WindowsFacts::look_in(dir);
463465 if available.available {
464− return Some(("windows", available));
466+ // Windows Sandbox is on, but with virtualization off in the firmware no
467+ // hypervisor runs, and it would fail only as a job starts.
468+ return Some(("windows", if windows.firmware_off() { Availability::needing(vec![Need::Virtualization], "windows") } else { available }));
465469 }
466− // What exactly is missing: asked of Windows only now, as it takes a second.
467− let windows = WindowsFacts::look();
468470 let needs = needs::windows_sandbox_needs(&windows, found.feature, found.cli, needs::restart_pending(dir, &windows));
469471 Some(("windows", if needs.is_empty() { available } else { Availability::needing(needs, "windows") }))
470472 }
634636 && state == "error"
635637 && let Some(said) = error.as_deref().filter(|e| needs::about_virtualization(e))
636638 {
637− let need = WindowsFacts::look().no_virtualization();
639+ let need = WindowsFacts::look_in(dir).no_virtualization();
638640 let (words, to_do) = need.said("linux");
639641 needs = vec![need];
640642 cause = Some(format!("{words} ({said})"));
+1−1
148148 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)));
149149 let listed = || wsl().args(["--list", "--verbose"]).output().map(|o| has_wsl2(&String::from_utf8_lossy(&o.stdout))).unwrap_or(false);
150150 let facts = WslFacts { installed, version: if installed { version() } else { None }, has_wsl2: installed && listed() };
151− let windows = WindowsFacts::look();
151+ let windows = WindowsFacts::look_in(&self.dir);
152152 Availability::needing(needs::wsl_needs(&windows, &facts, needs::restart_pending(&self.dir, &windows)), "linux")
153153 }
154154
+75−1
141141
142142 /// What Windows says of itself, read without an administrator. Each is
143143 /// None (or empty) when it couldn't be read, and nothing is concluded from it.
144−#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
144+#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
145145 #[serde(default)]
146146 pub struct WindowsFacts {
147147 /// `EditionID`: `Core` (Home), `Professional`, `Enterprise`, …
206206 .clone()
207207 }
208208
209+ /// [`look`](Self::look), kept in the launcher's state folder until
210+ /// Windows restarts: what it says (the edition, the processor, a
211+ /// hypervisor running, a feature turned on) changes only across a
212+ /// restart, and asking takes a second or two, which a status every
213+ /// few seconds shouldn't.
214+ pub fn look_in(dir: &Path) -> WindowsFacts {
215+ let path = dir.join(FACTS_FILE);
216+ let Some(boot) = booted_at() else { return WindowsFacts::look() };
217+ let kept: Option<Kept> = std::fs::read(&path).ok().and_then(|bytes| serde_json::from_slice(&bytes).ok());
218+ if let Some(kept) = kept.filter(|kept| same_boot(kept.boot, boot)) {
219+ return kept.facts;
220+ }
221+ let facts = WindowsFacts::look();
222+ // Kept only when Windows answered.
223+ if facts.booted.is_some() {
224+ let _ = std::fs::create_dir_all(dir);
225+ let _ = std::fs::write(&path, serde_json::to_vec(&Kept { boot, facts: facts.clone() }).unwrap_or_default());
226+ }
227+ facts
228+ }
229+
209230 /// Windows Home (`Core`, `CoreN`, `CoreSingleLanguage`, `CoreCountrySpecific`).
210231 pub fn home(&self) -> bool {
211232 self.edition.as_deref().is_some_and(|e| e.starts_with("Core"))
235256 }
236257 }
237258
259+/// The file [`WindowsFacts::look_in`] keeps them in.
260+pub const FACTS_FILE: &str = "windows-facts.json";
261+
262+#[derive(Serialize, Deserialize)]
263+struct Kept {
264+ /// When Windows started, in seconds since 1970.
265+ boot: u64,
266+ facts: WindowsFacts,
267+}
268+
269+/// Two readings of when Windows started are the same boot: they are
270+/// worked out from the time now and the time since, so differ by a second or so.
271+fn same_boot(a: u64, b: u64) -> bool {
272+ a.abs_diff(b) <= 30
273+}
274+
275+/// When Windows started, in seconds since 1970: now, less how long it has been running.
276+fn booted_at() -> Option<u64> {
277+ #[cfg(windows)]
278+ {
279+ unsafe extern "system" {
280+ fn GetTickCount64() -> u64;
281+ }
282+ // SAFETY: GetTickCount64 takes nothing and only reads a counter.
283+ let up = unsafe { GetTickCount64() } / 1000;
284+ let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).ok()?.as_secs();
285+ Some(now.saturating_sub(up))
286+ }
287+ #[cfg(not(windows))]
288+ None
289+}
290+
238291 /// Whether setup left a restart to do, this boot: the file setup wrote
239292 /// names the boot it ran in. One from an earlier boot is removed.
240293 pub fn restart_pending(dir: &Path, facts: &WindowsFacts) -> bool {
507560 println!("{facts:#?}");
508561 assert!(facts.edition.is_some() && facts.build.is_some() && facts.booted.is_some(), "{facts:?}");
509562 assert!(facts.features.contains_key(VM_PLATFORM), "{facts:?}");
563+ // Kept until Windows restarts: asked once, then read back at once.
564+ let dir = std::env::temp_dir().join(format!("g1t-facts-{}", std::process::id()));
565+ let first = WindowsFacts::look_in(&dir);
566+ let started = std::time::Instant::now();
567+ assert_eq!(WindowsFacts::look_in(&dir), first);
568+ println!("read back in {:?}", started.elapsed());
569+ assert!(started.elapsed() < std::time::Duration::from_millis(100));
570+ let _ = std::fs::remove_dir_all(&dir);
571+ }
572+
573+ #[test]
574+ fn what_windows_says_is_kept_until_it_restarts() {
575+ assert!(same_boot(1_760_000_000, 1_760_000_002));
576+ assert!(!same_boot(1_760_000_000, 1_760_003_600));
577+ let dir = std::env::temp_dir().join(format!("g1t-facts-kept-{}", std::process::id()));
578+ std::fs::create_dir_all(&dir).unwrap();
579+ let Some(boot) = booted_at() else { return };
580+ let kept = Kept { boot, facts: clean_home() };
581+ std::fs::write(dir.join(FACTS_FILE), serde_json::to_vec(&kept).unwrap()).unwrap();
582+ assert_eq!(WindowsFacts::look_in(&dir), clean_home());
583+ let _ = std::fs::remove_dir_all(&dir);
510584 }
511585
512586 #[test]

Comments

Sign in to comment.