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