From dba905bfa5a80c752f7b98eded5120f97c8e6979 Mon Sep 17 00:00:00 2001 From: Christoph Heiss Date: Fri, 10 Nov 2023 15:17:19 +0100 Subject: [PATCH] tui: move install progress dialog into own view module While at it, convert it to a proper `View`-impl, instead of a functional component. No functional changes. Signed-off-by: Christoph Heiss --- proxmox-tui-installer/src/main.rs | 233 +---------------- .../src/views/install_progress.rs | 241 ++++++++++++++++++ proxmox-tui-installer/src/views/mod.rs | 3 + 3 files changed, 250 insertions(+), 227 deletions(-) create mode 100644 proxmox-tui-installer/src/views/install_progress.rs diff --git a/proxmox-tui-installer/src/main.rs b/proxmox-tui-installer/src/main.rs index a7b466f..e1411c6 100644 --- a/proxmox-tui-installer/src/main.rs +++ b/proxmox-tui-installer/src/main.rs @@ -1,25 +1,14 @@ #![forbid(unsafe_code)] -use std::{ - collections::HashMap, - env, - io::{BufRead, BufReader, Write}, - net::IpAddr, - str::FromStr, - sync::{Arc, Mutex}, - thread, - time::Duration, -}; +use std::{collections::HashMap, env, net::IpAddr}; use cursive::{ event::Event, theme::{ColorStyle, Effect, PaletteColor, Style}, - utils::Counter, view::{Nameable, Offset, Resizable, ViewWrapper}, views::{ Button, Checkbox, Dialog, DummyView, EditView, Layer, LinearLayout, PaddedView, Panel, - ProgressBar, ResizedView, ScrollView, SelectView, StackView, TextContent, TextView, - ViewRef, + ResizedView, ScrollView, SelectView, StackView, TextView, ViewRef, }, Cursive, CursiveRunnable, ScreenId, View, XY, }; @@ -36,14 +25,13 @@ use proxmox_installer_common::{ }; mod setup; -use setup::InstallConfig; mod system; mod views; use views::{ - BootdiskOptionsView, CidrAddressEditView, FormView, TableView, TableViewItem, - TimezoneOptionsView, + BootdiskOptionsView, CidrAddressEditView, FormView, InstallProgressView, TableView, + TableViewItem, TimezoneOptionsView, }; // TextView::center() seems to garble the first two lines, so fix it manually here. @@ -673,215 +661,6 @@ fn install_progress_dialog(siv: &mut Cursive) -> InstallerView { // Ensure the screen is updated independently of keyboard events and such siv.set_autorefresh(true); - let cb_sink = siv.cb_sink().clone(); - let state = siv.user_data::().unwrap(); - let in_test_mode = state.in_test_mode; - let progress_text = TextContent::new("starting the installation .."); - - let progress_task = { - let progress_text = progress_text.clone(); - let options = state.options.clone(); - move |counter: Counter| { - let child = { - use std::process::{Command, Stdio}; - - let (path, args, envs): (&str, &[&str], Vec<(&str, &str)>) = if in_test_mode { - ( - "./proxmox-low-level-installer", - &["-t", "start-session-test"], - vec![("PERL5LIB", ".")], - ) - } else { - ("proxmox-low-level-installer", &["start-session"], vec![]) - }; - - Command::new(path) - .args(args) - .envs(envs) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - }; - - let mut child = match child { - Ok(child) => child, - Err(err) => { - let _ = cb_sink.send(Box::new(move |siv| { - siv.add_layer( - Dialog::text(err.to_string()) - .title("Error") - .button("Ok", Cursive::quit), - ); - })); - return; - } - }; - - let inner = || { - let reader = child.stdout.take().map(BufReader::new)?; - let mut writer = child.stdin.take()?; - - serde_json::to_writer(&mut writer, &InstallConfig::from(options)).unwrap(); - writeln!(writer).unwrap(); - - let writer = Arc::new(Mutex::new(writer)); - - for line in reader.lines() { - let line = match line { - Ok(line) => line, - Err(_) => break, - }; - - let msg = match line.parse::() { - Ok(msg) => msg, - Err(stray) => { - eprintln!("low-level installer: {stray}"); - continue; - } - }; - - match msg { - UiMessage::Info(s) => cb_sink.send(Box::new(|siv| { - siv.add_layer(Dialog::info(s).title("Information")); - })), - UiMessage::Error(s) => cb_sink.send(Box::new(|siv| { - siv.add_layer(Dialog::info(s).title("Error")); - })), - UiMessage::Prompt(s) => cb_sink.send({ - let writer = writer.clone(); - Box::new(move |siv| { - yes_no_dialog( - siv, - "Prompt", - &s, - Box::new({ - let writer = writer.clone(); - move |_| { - if let Ok(mut writer) = writer.lock() { - let _ = writeln!(writer, "ok"); - } - } - }), - Box::new(move |_| { - if let Ok(mut writer) = writer.lock() { - let _ = writeln!(writer); - } - }), - ); - }) - }), - UiMessage::Progress(ratio, s) => { - counter.set(ratio); - progress_text.set_content(s); - Ok(()) - } - UiMessage::Finished(success, msg) => { - counter.set(100); - progress_text.set_content(msg.to_owned()); - cb_sink.send(Box::new(move |siv| { - let title = if success { "Success" } else { "Failure" }; - - // For rebooting, we just need to quit the installer, - // our caller does the actual reboot. - siv.add_layer( - Dialog::text(msg) - .title(title) - .button("Reboot now", Cursive::quit), - ); - - let autoreboot = siv - .user_data::() - .map(|state| state.options.autoreboot) - .unwrap_or_default(); - - if autoreboot && success { - let cb_sink = siv.cb_sink(); - thread::spawn({ - let cb_sink = cb_sink.clone(); - move || { - thread::sleep(Duration::from_secs(5)); - let _ = cb_sink.send(Box::new(Cursive::quit)); - } - }); - } - })) - } - } - .unwrap(); - } - - Some(()) - }; - - if inner().is_none() { - cb_sink - .send(Box::new(|siv| { - siv.add_layer( - Dialog::text("low-level installer exited early") - .title("Error") - .button("Exit", Cursive::quit), - ); - })) - .unwrap(); - } - } - }; - - let progress_bar = ProgressBar::new().with_task(progress_task).full_width(); - let inner = PaddedView::lrtb( - 1, - 1, - 1, - 1, - LinearLayout::vertical() - .child(PaddedView::lrtb(1, 1, 0, 0, progress_bar)) - .child(DummyView) - .child(TextView::new_with_content(progress_text).center()) - .child(PaddedView::lrtb( - 1, - 1, - 1, - 0, - LinearLayout::horizontal().child(abort_install_button()), - )), - ); - - InstallerView::with_raw(state, inner) -} - -enum UiMessage { - Info(String), - Error(String), - Prompt(String), - Finished(bool, String), - Progress(usize, String), -} - -impl FromStr for UiMessage { - type Err = String; - - fn from_str(s: &str) -> Result { - let (ty, rest) = s.split_once(": ").ok_or("invalid message: no type")?; - - match ty { - "message" => Ok(UiMessage::Info(rest.to_owned())), - "error" => Ok(UiMessage::Error(rest.to_owned())), - "prompt" => Ok(UiMessage::Prompt(rest.to_owned())), - "finished" => { - let (state, rest) = rest.split_once(", ").ok_or("invalid message: no state")?; - Ok(UiMessage::Finished(state == "ok", rest.to_owned())) - } - "progress" => { - let (percent, rest) = rest.split_once(' ').ok_or("invalid progress message")?; - Ok(UiMessage::Progress( - percent - .parse::() - .map(|v| (v * 100.).floor() as usize) - .map_err(|err| err.to_string())?, - rest.to_owned(), - )) - } - unknown => Err(format!("invalid message type {unknown}, rest: {rest}")), - } - } + let state = siv.user_data::().cloned().unwrap(); + InstallerView::with_raw(&state, InstallProgressView::new(siv)) } diff --git a/proxmox-tui-installer/src/views/install_progress.rs b/proxmox-tui-installer/src/views/install_progress.rs new file mode 100644 index 0000000..4dca81b --- /dev/null +++ b/proxmox-tui-installer/src/views/install_progress.rs @@ -0,0 +1,241 @@ +use std::{ + io::{BufRead, BufReader, Write}, + str::FromStr, + sync::{Arc, Mutex}, + thread, + time::Duration, +}; + +use cursive::{ + utils::Counter, + view::{Resizable, ViewWrapper}, + views::{Dialog, DummyView, LinearLayout, PaddedView, ProgressBar, TextContent, TextView}, + Cursive, +}; + +use crate::{abort_install_button, setup::InstallConfig, yes_no_dialog, InstallerState}; + +pub struct InstallProgressView { + view: PaddedView, +} + +impl InstallProgressView { + pub fn new(siv: &mut Cursive) -> Self { + let cb_sink = siv.cb_sink().clone(); + let state = siv.user_data::().unwrap(); + let progress_text = TextContent::new("starting the installation .."); + + let progress_task = { + let progress_text = progress_text.clone(); + let state = state.clone(); + move |counter: Counter| { + let child = { + use std::process::{Command, Stdio}; + + let (path, args, envs): (&str, &[&str], Vec<(&str, &str)>) = + if state.in_test_mode { + ( + "./proxmox-low-level-installer", + &["-t", "start-session-test"], + vec![("PERL5LIB", ".")], + ) + } else { + ("proxmox-low-level-installer", &["start-session"], vec![]) + }; + + Command::new(path) + .args(args) + .envs(envs) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + }; + + let mut child = match child { + Ok(child) => child, + Err(err) => { + let _ = cb_sink.send(Box::new(move |siv| { + siv.add_layer( + Dialog::text(err.to_string()) + .title("Error") + .button("Ok", Cursive::quit), + ); + })); + return; + } + }; + + let inner = || { + let reader = child.stdout.take().map(BufReader::new)?; + let mut writer = child.stdin.take()?; + + serde_json::to_writer(&mut writer, &InstallConfig::from(state.options)) + .unwrap(); + writeln!(writer).unwrap(); + + let writer = Arc::new(Mutex::new(writer)); + + for line in reader.lines() { + let line = match line { + Ok(line) => line, + Err(_) => break, + }; + + let msg = match line.parse::() { + Ok(msg) => msg, + Err(stray) => { + eprintln!("low-level installer: {stray}"); + continue; + } + }; + + match msg { + UiMessage::Info(s) => cb_sink.send(Box::new(|siv| { + siv.add_layer(Dialog::info(s).title("Information")); + })), + UiMessage::Error(s) => cb_sink.send(Box::new(|siv| { + siv.add_layer(Dialog::info(s).title("Error")); + })), + UiMessage::Prompt(s) => cb_sink.send({ + let writer = writer.clone(); + Box::new(move |siv| { + yes_no_dialog( + siv, + "Prompt", + &s, + Box::new({ + let writer = writer.clone(); + move |_| { + if let Ok(mut writer) = writer.lock() { + let _ = writeln!(writer, "ok"); + } + } + }), + Box::new(move |_| { + if let Ok(mut writer) = writer.lock() { + let _ = writeln!(writer); + } + }), + ); + }) + }), + UiMessage::Progress(ratio, s) => { + counter.set(ratio); + progress_text.set_content(s); + Ok(()) + } + UiMessage::Finished(success, msg) => { + counter.set(100); + progress_text.set_content(msg.to_owned()); + cb_sink.send(Box::new(move |siv| { + let title = if success { "Success" } else { "Failure" }; + + // For rebooting, we just need to quit the installer, + // our caller does the actual reboot. + siv.add_layer( + Dialog::text(msg) + .title(title) + .button("Reboot now", Cursive::quit), + ); + + let autoreboot = siv + .user_data::() + .map(|state| state.options.autoreboot) + .unwrap_or_default(); + + if autoreboot && success { + let cb_sink = siv.cb_sink(); + thread::spawn({ + let cb_sink = cb_sink.clone(); + move || { + thread::sleep(Duration::from_secs(5)); + let _ = cb_sink.send(Box::new(Cursive::quit)); + } + }); + } + })) + } + } + .unwrap(); + } + + Some(()) + }; + + if inner().is_none() { + cb_sink + .send(Box::new(|siv| { + siv.add_layer( + Dialog::text("low-level installer exited early") + .title("Error") + .button("Exit", Cursive::quit), + ); + })) + .unwrap(); + } + } + }; + + let progress_bar = ProgressBar::new().with_task(progress_task).full_width(); + let view = PaddedView::lrtb( + 1, + 1, + 1, + 1, + LinearLayout::vertical() + .child(PaddedView::lrtb(1, 1, 0, 0, progress_bar)) + .child(DummyView) + .child(TextView::new_with_content(progress_text).center()) + .child(PaddedView::lrtb( + 1, + 1, + 1, + 0, + LinearLayout::horizontal().child(abort_install_button()), + )), + ); + + Self { view } + } +} + +impl ViewWrapper for InstallProgressView { + cursive::wrap_impl!(self.view: PaddedView); +} + +enum UiMessage { + Info(String), + Error(String), + Prompt(String), + Finished(bool, String), + Progress(usize, String), +} + +impl FromStr for UiMessage { + type Err = String; + + fn from_str(s: &str) -> Result { + let (ty, rest) = s.split_once(": ").ok_or("invalid message: no type")?; + + match ty { + "message" => Ok(UiMessage::Info(rest.to_owned())), + "error" => Ok(UiMessage::Error(rest.to_owned())), + "prompt" => Ok(UiMessage::Prompt(rest.to_owned())), + "finished" => { + let (state, rest) = rest.split_once(", ").ok_or("invalid message: no state")?; + Ok(UiMessage::Finished(state == "ok", rest.to_owned())) + } + "progress" => { + let (percent, rest) = rest.split_once(' ').ok_or("invalid progress message")?; + Ok(UiMessage::Progress( + percent + .parse::() + .map(|v| (v * 100.).floor() as usize) + .map_err(|err| err.to_string())?, + rest.to_owned(), + )) + } + unknown => Err(format!("invalid message type {unknown}, rest: {rest}")), + } + } +} diff --git a/proxmox-tui-installer/src/views/mod.rs b/proxmox-tui-installer/src/views/mod.rs index 4d27532..3244e76 100644 --- a/proxmox-tui-installer/src/views/mod.rs +++ b/proxmox-tui-installer/src/views/mod.rs @@ -12,6 +12,9 @@ use proxmox_installer_common::utils::CidrAddress; mod bootdisk; pub use bootdisk::*; +mod install_progress; +pub use install_progress::*; + mod table_view; pub use table_view::*; -- 2.39.2