]> git.proxmox.com Git - pve-installer.git/blob - proxmox-tui-installer/src/main.rs
tui: don't abort install if min ram requirement is not met
[pve-installer.git] / proxmox-tui-installer / src / main.rs
1 use std::{
2 collections::HashMap,
3 env,
4 io::{BufRead, BufReader, Write},
5 net::IpAddr,
6 path::PathBuf,
7 str::FromStr,
8 sync::{Arc, Mutex},
9 thread,
10 time::Duration,
11 };
12
13 use cursive::{
14 event::Event,
15 theme::{ColorStyle, Effect, PaletteColor, Style},
16 utils::Counter,
17 view::{Nameable, Offset, Resizable, ViewWrapper},
18 views::{
19 Button, Checkbox, Dialog, DummyView, EditView, Layer, LinearLayout, PaddedView, Panel,
20 ProgressBar, ResizedView, ScrollView, SelectView, StackView, TextContent, TextView,
21 ViewRef,
22 },
23 Cursive, CursiveRunnable, ScreenId, View, XY,
24 };
25
26 use regex::Regex;
27
28 mod options;
29 use options::*;
30
31 mod setup;
32 use setup::{InstallConfig, LocaleInfo, ProxmoxProduct, RuntimeInfo, SetupInfo};
33
34 mod system;
35
36 mod utils;
37 use utils::Fqdn;
38
39 mod views;
40 use views::{
41 BootdiskOptionsView, CidrAddressEditView, FormView, TableView, TableViewItem,
42 TimezoneOptionsView,
43 };
44
45 // TextView::center() seems to garble the first two lines, so fix it manually here.
46 const PROXMOX_LOGO: &str = r#"
47 ____
48 | _ \ _ __ _____ ___ __ ___ _____ __
49 | |_) | '__/ _ \ \/ / '_ ` _ \ / _ \ \/ /
50 | __/| | | (_) > <| | | | | | (_) > <
51 |_| |_| \___/_/\_\_| |_| |_|\___/_/\_\ "#;
52
53 /// ISO information is available globally.
54 static mut SETUP_INFO: Option<SetupInfo> = None;
55
56 pub fn setup_info() -> &'static SetupInfo {
57 unsafe { SETUP_INFO.as_ref().unwrap() }
58 }
59
60 fn init_setup_info(info: SetupInfo) {
61 unsafe {
62 SETUP_INFO = Some(info);
63 }
64 }
65
66 #[inline]
67 pub fn current_product() -> setup::ProxmoxProduct {
68 setup_info().config.product
69 }
70
71 struct InstallerView {
72 view: ResizedView<Dialog>,
73 }
74
75 impl InstallerView {
76 pub fn new<T: View>(
77 state: &InstallerState,
78 view: T,
79 next_cb: Box<dyn Fn(&mut Cursive)>,
80 focus_next: bool,
81 ) -> Self {
82 let mut bbar = LinearLayout::horizontal()
83 .child(abort_install_button())
84 .child(DummyView.full_width())
85 .child(Button::new("Previous", switch_to_prev_screen))
86 .child(DummyView)
87 .child(Button::new("Next", next_cb));
88 let _ = bbar.set_focus_index(4); // ignore errors
89 let mut inner = LinearLayout::vertical()
90 .child(PaddedView::lrtb(0, 0, 1, 1, view))
91 .child(PaddedView::lrtb(1, 1, 0, 0, bbar));
92 if focus_next {
93 let _ = inner.set_focus_index(1); // ignore errors
94 }
95
96 Self::with_raw(state, inner)
97 }
98
99 pub fn with_raw(state: &InstallerState, view: impl View) -> Self {
100 let setup = &state.setup_info;
101
102 let title = format!(
103 "{} ({}-{}) Installer",
104 setup.config.fullname, setup.iso_info.release, setup.iso_info.isorelease
105 );
106
107 let inner = Dialog::around(view).title(title);
108
109 Self {
110 // Limit the maximum to something reasonable, such that it won't get spread out much
111 // depending on the screen.
112 view: ResizedView::with_max_size((120, 40), inner),
113 }
114 }
115 }
116
117 impl ViewWrapper for InstallerView {
118 cursive::wrap_impl!(self.view: ResizedView<Dialog>);
119 }
120
121 struct InstallerBackgroundView {
122 view: StackView,
123 }
124
125 impl InstallerBackgroundView {
126 pub fn new() -> Self {
127 let style = Style {
128 effects: Effect::Bold.into(),
129 color: ColorStyle::back(PaletteColor::View),
130 };
131
132 let mut view = StackView::new();
133 view.add_fullscreen_layer(Layer::with_color(
134 DummyView
135 .full_width()
136 .fixed_height(PROXMOX_LOGO.lines().count() + 1),
137 ColorStyle::back(PaletteColor::View),
138 ));
139 view.add_transparent_layer_at(
140 XY {
141 x: Offset::Center,
142 y: Offset::Absolute(0),
143 },
144 TextView::new(PROXMOX_LOGO).style(style),
145 );
146
147 Self { view }
148 }
149 }
150
151 impl ViewWrapper for InstallerBackgroundView {
152 cursive::wrap_impl!(self.view: StackView);
153 }
154
155 #[derive(Clone, Eq, Hash, PartialEq)]
156 enum InstallerStep {
157 Licence,
158 Bootdisk,
159 Timezone,
160 Password,
161 Network,
162 Summary,
163 Install,
164 }
165
166 #[derive(Clone)]
167 struct InstallerState {
168 options: InstallerOptions,
169 /// FIXME: Remove:
170 setup_info: SetupInfo,
171 runtime_info: RuntimeInfo,
172 locales: LocaleInfo,
173 steps: HashMap<InstallerStep, ScreenId>,
174 in_test_mode: bool,
175 }
176
177 fn main() {
178 let mut siv = cursive::termion();
179
180 let in_test_mode = match env::args().nth(1).as_deref() {
181 Some("-t") => true,
182
183 // Always force the test directory in debug builds
184 _ => cfg!(debug_assertions),
185 };
186
187 let (locales, runtime_info) = match installer_setup(in_test_mode) {
188 Ok(result) => result,
189 Err(err) => initial_setup_error(&mut siv, &err),
190 };
191
192 siv.clear_global_callbacks(Event::CtrlChar('c'));
193 siv.set_on_pre_event(Event::CtrlChar('c'), trigger_abort_install_dialog);
194
195 siv.set_user_data(InstallerState {
196 options: InstallerOptions {
197 bootdisk: BootdiskOptions::defaults_from(&runtime_info.disks[0]),
198 timezone: TimezoneOptions::defaults_from(&runtime_info, &locales),
199 password: Default::default(),
200 network: NetworkOptions::from(&runtime_info.network),
201 autoreboot: false,
202 },
203 setup_info: setup_info().clone(), // FIXME: REMOVE
204 runtime_info,
205 locales,
206 steps: HashMap::new(),
207 in_test_mode,
208 });
209
210 switch_to_next_screen(&mut siv, InstallerStep::Licence, &license_dialog);
211 siv.run();
212 }
213
214 fn installer_setup(in_test_mode: bool) -> Result<(LocaleInfo, RuntimeInfo), String> {
215 let base_path = if in_test_mode { "./testdir" } else { "/" };
216 let mut path = PathBuf::from(base_path);
217
218 path.push("run");
219 path.push("proxmox-installer");
220
221 let installer_info = {
222 let mut path = path.clone();
223 path.push("iso-info.json");
224
225 setup::read_json(&path).map_err(|err| format!("Failed to retrieve setup info: {err}"))?
226 };
227 init_setup_info(installer_info);
228
229 let locale_info = {
230 let mut path = path.clone();
231 path.push("locales.json");
232
233 setup::read_json(&path).map_err(|err| format!("Failed to retrieve locale info: {err}"))?
234 };
235
236 let mut runtime_info: RuntimeInfo = {
237 let mut path = path.clone();
238 path.push("run-env-info.json");
239
240 setup::read_json(&path)
241 .map_err(|err| format!("Failed to retrieve runtime environment info: {err}"))?
242 };
243
244 runtime_info.disks.sort();
245 if runtime_info.disks.is_empty() {
246 Err("The installer could not find any supported hard disks.".to_owned())
247 } else {
248 Ok((locale_info, runtime_info))
249 }
250 }
251
252 /// Anything that can be done late in the setup and will not result in fatal errors.
253 fn installer_setup_late(siv: &mut Cursive) {
254 let state = siv.user_data::<InstallerState>().cloned().unwrap();
255
256 if !state.in_test_mode {
257 let kmap_id = &state.options.timezone.kb_layout;
258 if let Some(kmap) = state.locales.kmap.get(kmap_id) {
259 if let Err(err) = system::set_keyboard_layout(kmap) {
260 display_setup_warning(siv, &format!("Failed to apply keyboard layout: {err}"));
261 }
262 }
263 }
264
265 if state.runtime_info.total_memory < 1024 {
266 display_setup_warning(
267 siv,
268 concat!(
269 "Less than 1 GiB of usable memory detected, installation will probably fail.\n\n",
270 "See 'System Requirements' in the documentation."
271 ),
272 );
273 }
274
275 if state.setup_info.config.product == ProxmoxProduct::PVE && !state.runtime_info.hvm_supported {
276 display_setup_warning(
277 siv,
278 concat!(
279 "No support for hardware-accelerated KVM virtualization detected.\n\n",
280 "Check BIOS settings for Intel VT / AMD-V / SVM."
281 ),
282 );
283 }
284 }
285
286 fn initial_setup_error(siv: &mut CursiveRunnable, message: &str) -> ! {
287 siv.add_layer(
288 Dialog::around(TextView::new(message))
289 .title("Installer setup error")
290 .button("Ok", Cursive::quit),
291 );
292 siv.run();
293
294 std::process::exit(1);
295 }
296
297 fn display_setup_warning(siv: &mut Cursive, message: &str) {
298 siv.add_layer(Dialog::info(message).title("Warning"));
299 }
300
301 fn switch_to_next_screen(
302 siv: &mut Cursive,
303 step: InstallerStep,
304 constructor: &dyn Fn(&mut Cursive) -> InstallerView,
305 ) {
306 let state = siv.user_data::<InstallerState>().cloned().unwrap();
307 let is_first_screen = state.steps.is_empty();
308
309 // Check if the screen already exists; if yes, then simply switch to it.
310 if let Some(screen_id) = state.steps.get(&step) {
311 siv.set_screen(*screen_id);
312
313 // The summary view cannot be cached (otherwise it would display stale values). Thus
314 // replace it if the screen is switched to.
315 // TODO: Could be done by e.g. having all the main dialog views implement some sort of
316 // .refresh(), which can be called if the view is switched to.
317 if step == InstallerStep::Summary {
318 let view = constructor(siv);
319 siv.screen_mut().pop_layer();
320 siv.screen_mut().add_layer(view);
321 }
322
323 return;
324 }
325
326 let v = constructor(siv);
327 let screen = siv.add_active_screen();
328 siv.with_user_data(|state: &mut InstallerState| state.steps.insert(step, screen));
329
330 siv.screen_mut().add_transparent_layer_at(
331 XY {
332 x: Offset::Parent(0),
333 y: Offset::Parent(0),
334 },
335 InstallerBackgroundView::new(),
336 );
337
338 siv.screen_mut().add_layer(v);
339
340 // If this is the first screen to be added, execute our late setup first.
341 // Needs to be done here, at the end, to ensure that any potential layers get added to
342 // the right screen and are on top.
343 if is_first_screen {
344 installer_setup_late(siv);
345 }
346 }
347
348 fn switch_to_prev_screen(siv: &mut Cursive) {
349 let id = siv.active_screen().saturating_sub(1);
350 siv.set_screen(id);
351 }
352
353 fn yes_no_dialog(
354 siv: &mut Cursive,
355 title: &str,
356 text: &str,
357 // callback_yes: &'static dyn Fn(&mut Cursive),
358 // callback_no: &'static dyn Fn(&mut Cursive),
359 callback_yes: Box<dyn Fn(&mut Cursive)>,
360 callback_no: Box<dyn Fn(&mut Cursive)>,
361 ) {
362 siv.add_layer(
363 Dialog::around(TextView::new(text))
364 .title(title)
365 .button("No", move |siv| {
366 siv.pop_layer();
367 callback_no(siv);
368 })
369 .button("Yes", move |siv| {
370 siv.pop_layer();
371 callback_yes(siv);
372 }),
373 )
374 }
375
376 fn trigger_abort_install_dialog(siv: &mut Cursive) {
377 #[cfg(debug_assertions)]
378 siv.quit();
379
380 #[cfg(not(debug_assertions))]
381 yes_no_dialog(
382 siv,
383 "Abort installation?",
384 "Are you sure you want to abort the installation?",
385 Box::new(Cursive::quit),
386 Box::new(|_| {}),
387 )
388 }
389
390 fn abort_install_button() -> Button {
391 Button::new("Abort", trigger_abort_install_dialog)
392 }
393
394 fn get_eula() -> String {
395 // TODO: properly using info from Proxmox::Install::Env::setup()
396 std::fs::read_to_string("/cdrom/EULA")
397 .unwrap_or_else(|_| "< Debug build - ignoring non-existing EULA >".to_owned())
398 }
399
400 fn license_dialog(siv: &mut Cursive) -> InstallerView {
401 let state = siv.user_data::<InstallerState>().unwrap();
402
403 let mut bbar = LinearLayout::horizontal()
404 .child(abort_install_button())
405 .child(DummyView.full_width())
406 .child(Button::new("I agree", |siv| {
407 switch_to_next_screen(siv, InstallerStep::Bootdisk, &bootdisk_dialog)
408 }));
409 let _ = bbar.set_focus_index(2); // ignore errors
410
411 let mut inner = LinearLayout::vertical()
412 .child(PaddedView::lrtb(
413 0,
414 0,
415 1,
416 0,
417 TextView::new("END USER LICENSE AGREEMENT (EULA)").center(),
418 ))
419 .child(Panel::new(ScrollView::new(
420 TextView::new(get_eula()).center(),
421 )))
422 .child(PaddedView::lrtb(1, 1, 1, 0, bbar));
423
424 let _ = inner.set_focus_index(2); // ignore errors
425
426 InstallerView::with_raw(state, inner)
427 }
428
429 fn bootdisk_dialog(siv: &mut Cursive) -> InstallerView {
430 let state = siv.user_data::<InstallerState>().cloned().unwrap();
431
432 InstallerView::new(
433 &state,
434 BootdiskOptionsView::new(&state.runtime_info.disks, &state.options.bootdisk)
435 .with_name("bootdisk-options"),
436 Box::new(|siv| {
437 let options = siv.call_on_name("bootdisk-options", BootdiskOptionsView::get_values);
438
439 match options {
440 Some(Ok(options)) => {
441 siv.with_user_data(|state: &mut InstallerState| {
442 state.options.bootdisk = options;
443 });
444
445 switch_to_next_screen(siv, InstallerStep::Timezone, &timezone_dialog);
446 }
447
448 Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))),
449 _ => siv.add_layer(Dialog::info("Invalid values")),
450 }
451 }),
452 true,
453 )
454 }
455
456 fn timezone_dialog(siv: &mut Cursive) -> InstallerView {
457 let state = siv.user_data::<InstallerState>().unwrap();
458 let options = &state.options.timezone;
459
460 InstallerView::new(
461 state,
462 TimezoneOptionsView::new(&state.locales, options).with_name("timezone-options"),
463 Box::new(|siv| {
464 let options = siv.call_on_name("timezone-options", TimezoneOptionsView::get_values);
465
466 match options {
467 Some(Ok(options)) => {
468 siv.with_user_data(|state: &mut InstallerState| {
469 state.options.timezone = options;
470 });
471
472 switch_to_next_screen(siv, InstallerStep::Password, &password_dialog);
473 }
474 Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))),
475 _ => siv.add_layer(Dialog::info("Invalid values")),
476 }
477 }),
478 true,
479 )
480 }
481
482 fn password_dialog(siv: &mut Cursive) -> InstallerView {
483 let state = siv.user_data::<InstallerState>().unwrap();
484 let options = &state.options.password;
485
486 let inner = FormView::new()
487 .child("Root password", EditView::new().secret())
488 .child("Confirm root password", EditView::new().secret())
489 .child(
490 "Administator email",
491 EditView::new().content(&options.email),
492 )
493 .with_name("password-options");
494
495 InstallerView::new(
496 state,
497 inner,
498 Box::new(|siv| {
499 let options = siv.call_on_name("password-options", |view: &mut FormView| {
500 let root_password = view
501 .get_value::<EditView, _>(0)
502 .ok_or("failed to retrieve password")?;
503
504 let confirm_password = view
505 .get_value::<EditView, _>(1)
506 .ok_or("failed to retrieve password confirmation")?;
507
508 let email = view
509 .get_value::<EditView, _>(2)
510 .ok_or("failed to retrieve email")?;
511
512 let email_regex =
513 Regex::new(r"^[\w\+\-\~]+(\.[\w\+\-\~]+)*@[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*$")
514 .unwrap();
515
516 if root_password.len() < 5 {
517 Err("password too short")
518 } else if root_password != confirm_password {
519 Err("passwords do not match")
520 } else if email == "mail@example.invalid" {
521 Err("invalid email address")
522 } else if !email_regex.is_match(&email) {
523 Err("Email does not look like a valid address (user@domain.tld)")
524 } else {
525 Ok(PasswordOptions {
526 root_password,
527 email,
528 })
529 }
530 });
531
532 match options {
533 Some(Ok(options)) => {
534 siv.with_user_data(|state: &mut InstallerState| {
535 state.options.password = options;
536 });
537
538 switch_to_next_screen(siv, InstallerStep::Network, &network_dialog);
539 }
540 Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))),
541 _ => siv.add_layer(Dialog::info("Invalid values")),
542 }
543 }),
544 false,
545 )
546 }
547
548 fn network_dialog(siv: &mut Cursive) -> InstallerView {
549 let state = siv.user_data::<InstallerState>().unwrap();
550 let options = &state.options.network;
551
552 let inner = FormView::new()
553 .child(
554 "Management interface",
555 SelectView::new()
556 .popup()
557 .with_all_str(state.runtime_info.network.interfaces.keys()),
558 )
559 .child(
560 "Hostname (FQDN)",
561 EditView::new().content(options.fqdn.to_string()),
562 )
563 .child(
564 "IP address (CIDR)",
565 CidrAddressEditView::new().content(options.address.clone()),
566 )
567 .child(
568 "Gateway address",
569 EditView::new().content(options.gateway.to_string()),
570 )
571 .child(
572 "DNS server address",
573 EditView::new().content(options.dns_server.to_string()),
574 )
575 .with_name("network-options");
576
577 InstallerView::new(
578 state,
579 inner,
580 Box::new(|siv| {
581 let options = siv.call_on_name("network-options", |view: &mut FormView| {
582 let ifname = view
583 .get_value::<SelectView, _>(0)
584 .ok_or("failed to retrieve management interface name")?;
585
586 let fqdn = view
587 .get_value::<EditView, _>(1)
588 .ok_or("failed to retrieve host FQDN")?
589 .parse::<Fqdn>()
590 .map_err(|_| "failed to parse hostname".to_owned())?;
591
592 let address = view
593 .get_value::<CidrAddressEditView, _>(2)
594 .ok_or("failed to retrieve host address")?;
595
596 let gateway = view
597 .get_value::<EditView, _>(3)
598 .ok_or("failed to retrieve gateway address")?
599 .parse::<IpAddr>()
600 .map_err(|err| err.to_string())?;
601
602 let dns_server = view
603 .get_value::<EditView, _>(4)
604 .ok_or("failed to retrieve DNS server address")?
605 .parse::<IpAddr>()
606 .map_err(|err| err.to_string())?;
607
608 if address.addr().is_ipv4() != gateway.is_ipv4() {
609 Err("host and gateway IP address version must not differ".to_owned())
610 } else if address.addr().is_ipv4() != dns_server.is_ipv4() {
611 Err("host and DNS IP address version must not differ".to_owned())
612 } else if fqdn.to_string().chars().all(|c| c.is_ascii_digit()) {
613 // Not supported/allowed on Debian
614 Err("hostname cannot be purely numeric".to_owned())
615 } else if fqdn.to_string().ends_with(".invalid") {
616 Err("hostname does not look valid".to_owned())
617 } else {
618 Ok(NetworkOptions {
619 ifname,
620 fqdn,
621 address,
622 gateway,
623 dns_server,
624 })
625 }
626 });
627
628 match options {
629 Some(Ok(options)) => {
630 siv.with_user_data(|state: &mut InstallerState| {
631 state.options.network = options;
632 });
633
634 switch_to_next_screen(siv, InstallerStep::Summary, &summary_dialog);
635 }
636 Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))),
637 _ => siv.add_layer(Dialog::info("Invalid values")),
638 }
639 }),
640 true,
641 )
642 }
643
644 pub struct SummaryOption {
645 name: &'static str,
646 value: String,
647 }
648
649 impl SummaryOption {
650 pub fn new<S: Into<String>>(name: &'static str, value: S) -> Self {
651 Self {
652 name,
653 value: value.into(),
654 }
655 }
656 }
657
658 impl TableViewItem for SummaryOption {
659 fn get_column(&self, name: &str) -> String {
660 match name {
661 "name" => self.name.to_owned(),
662 "value" => self.value.clone(),
663 _ => unreachable!(),
664 }
665 }
666 }
667
668 fn summary_dialog(siv: &mut Cursive) -> InstallerView {
669 let state = siv.user_data::<InstallerState>().unwrap();
670
671 let mut bbar = LinearLayout::horizontal()
672 .child(abort_install_button())
673 .child(DummyView.full_width())
674 .child(Button::new("Previous", switch_to_prev_screen))
675 .child(DummyView)
676 .child(Button::new("Install", |siv| {
677 let autoreboot = siv
678 .find_name("reboot-after-install")
679 .map(|v: ViewRef<Checkbox>| v.is_checked())
680 .unwrap_or_default();
681
682 siv.with_user_data(|state: &mut InstallerState| {
683 state.options.autoreboot = autoreboot;
684 });
685
686 switch_to_next_screen(siv, InstallerStep::Install, &install_progress_dialog);
687 }));
688
689 let _ = bbar.set_focus_index(2); // ignore errors
690
691 let mut inner = LinearLayout::vertical()
692 .child(PaddedView::lrtb(
693 0,
694 0,
695 1,
696 2,
697 TableView::new()
698 .columns(&[
699 ("name".to_owned(), "Option".to_owned()),
700 ("value".to_owned(), "Selected value".to_owned()),
701 ])
702 .items(state.options.to_summary(&state.locales)),
703 ))
704 .child(
705 LinearLayout::horizontal()
706 .child(DummyView.full_width())
707 .child(Checkbox::new().checked().with_name("reboot-after-install"))
708 .child(
709 TextView::new(" Automatically reboot after successful installation").no_wrap(),
710 )
711 .child(DummyView.full_width()),
712 )
713 .child(PaddedView::lrtb(1, 1, 1, 0, bbar));
714
715 let _ = inner.set_focus_index(2); // ignore errors
716
717 InstallerView::with_raw(state, inner)
718 }
719
720 fn install_progress_dialog(siv: &mut Cursive) -> InstallerView {
721 // Ensure the screen is updated independently of keyboard events and such
722 siv.set_autorefresh(true);
723
724 let cb_sink = siv.cb_sink().clone();
725 let state = siv.user_data::<InstallerState>().unwrap();
726 let progress_text = TextContent::new("starting the installation ..");
727
728 let progress_task = {
729 let progress_text = progress_text.clone();
730 let options = state.options.clone();
731 move |counter: Counter| {
732 let child = {
733 use std::process::{Command, Stdio};
734
735 #[cfg(not(debug_assertions))]
736 let (path, args, envs): (&str, [&str; 1], [(&str, &str); 0]) =
737 ("proxmox-low-level-installer", ["start-session"], []);
738
739 #[cfg(debug_assertions)]
740 let (path, args, envs) = (
741 PathBuf::from("./proxmox-low-level-installer"),
742 ["-t", "start-session-test"],
743 [("PERL5LIB", ".")],
744 );
745
746 Command::new(path)
747 .args(args)
748 .envs(envs)
749 .stdin(Stdio::piped())
750 .stdout(Stdio::piped())
751 .spawn()
752 };
753
754 let mut child = match child {
755 Ok(child) => child,
756 Err(err) => {
757 let _ = cb_sink.send(Box::new(move |siv| {
758 siv.add_layer(
759 Dialog::text(err.to_string())
760 .title("Error")
761 .button("Ok", Cursive::quit),
762 );
763 }));
764 return;
765 }
766 };
767
768 let inner = || {
769 let reader = child.stdout.take().map(BufReader::new)?;
770 let mut writer = child.stdin.take()?;
771
772 serde_json::to_writer(&mut writer, &InstallConfig::from(options)).unwrap();
773 writeln!(writer).unwrap();
774
775 let writer = Arc::new(Mutex::new(writer));
776
777 for line in reader.lines() {
778 let line = match line {
779 Ok(line) => line,
780 Err(_) => break,
781 };
782
783 let msg = match line.parse::<UiMessage>() {
784 Ok(msg) => msg,
785 Err(stray) => {
786 eprintln!("low-level installer: {stray}");
787 continue;
788 }
789 };
790
791 match msg {
792 UiMessage::Info(s) => cb_sink.send(Box::new(|siv| {
793 siv.add_layer(Dialog::info(s).title("Information"));
794 })),
795 UiMessage::Error(s) => cb_sink.send(Box::new(|siv| {
796 siv.add_layer(Dialog::info(s).title("Error"));
797 })),
798 UiMessage::Prompt(s) => cb_sink.send({
799 let writer = writer.clone();
800 Box::new(move |siv| {
801 yes_no_dialog(
802 siv,
803 "Prompt",
804 &s,
805 Box::new({
806 let writer = writer.clone();
807 move |_| {
808 if let Ok(mut writer) = writer.lock() {
809 let _ = writeln!(writer, "ok");
810 }
811 }
812 }),
813 Box::new(move |_| {
814 if let Ok(mut writer) = writer.lock() {
815 let _ = writeln!(writer);
816 }
817 }),
818 );
819 })
820 }),
821 UiMessage::Progress(ratio, s) => {
822 counter.set(ratio);
823 progress_text.set_content(s);
824 Ok(())
825 }
826 UiMessage::Finished(success, msg) => {
827 counter.set(100);
828 progress_text.set_content(msg.to_owned());
829 cb_sink.send(Box::new(move |siv| {
830 let title = if success { "Success" } else { "Failure" };
831
832 // For rebooting, we just need to quit the installer,
833 // our caller does the actual reboot.
834 siv.add_layer(
835 Dialog::text(msg)
836 .title(title)
837 .button("Reboot now", Cursive::quit),
838 );
839
840 let autoreboot = siv
841 .user_data::<InstallerState>()
842 .map(|state| state.options.autoreboot)
843 .unwrap_or_default();
844
845 if autoreboot && success {
846 let cb_sink = siv.cb_sink();
847 thread::spawn({
848 let cb_sink = cb_sink.clone();
849 move || {
850 thread::sleep(Duration::from_secs(5));
851 let _ = cb_sink.send(Box::new(Cursive::quit));
852 }
853 });
854 }
855 }))
856 }
857 }
858 .unwrap();
859 }
860
861 Some(())
862 };
863
864 if inner().is_none() {
865 cb_sink
866 .send(Box::new(|siv| {
867 siv.add_layer(
868 Dialog::text("low-level installer exited early")
869 .title("Error")
870 .button("Exit", Cursive::quit),
871 );
872 }))
873 .unwrap();
874 }
875 }
876 };
877
878 let progress_bar = ProgressBar::new().with_task(progress_task).full_width();
879 let inner = PaddedView::lrtb(
880 1,
881 1,
882 1,
883 1,
884 LinearLayout::vertical()
885 .child(PaddedView::lrtb(1, 1, 0, 0, progress_bar))
886 .child(DummyView)
887 .child(TextView::new_with_content(progress_text).center())
888 .child(PaddedView::lrtb(
889 1,
890 1,
891 1,
892 0,
893 LinearLayout::horizontal().child(abort_install_button()),
894 )),
895 );
896
897 InstallerView::with_raw(state, inner)
898 }
899
900 enum UiMessage {
901 Info(String),
902 Error(String),
903 Prompt(String),
904 Finished(bool, String),
905 Progress(usize, String),
906 }
907
908 impl FromStr for UiMessage {
909 type Err = String;
910
911 fn from_str(s: &str) -> Result<Self, Self::Err> {
912 let (ty, rest) = s.split_once(": ").ok_or("invalid message: no type")?;
913
914 match ty {
915 "message" => Ok(UiMessage::Info(rest.to_owned())),
916 "error" => Ok(UiMessage::Error(rest.to_owned())),
917 "prompt" => Ok(UiMessage::Prompt(rest.to_owned())),
918 "finished" => {
919 let (state, rest) = rest.split_once(", ").ok_or("invalid message: no state")?;
920 Ok(UiMessage::Finished(state == "ok", rest.to_owned()))
921 }
922 "progress" => {
923 let (percent, rest) = rest.split_once(' ').ok_or("invalid progress message")?;
924 Ok(UiMessage::Progress(
925 percent
926 .parse::<f64>()
927 .map(|v| (v * 100.).floor() as usize)
928 .map_err(|err| err.to_string())?,
929 rest.to_owned(),
930 ))
931 }
932 unknown => Err(format!("invalid message type {unknown}, rest: {rest}")),
933 }
934 }
935 }