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