mirror of
https://git.proxmox.com/git/pve-installer
synced 2025-04-28 16:34:37 +00:00
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 <c.heiss@proxmox.com>
This commit is contained in:
parent
eda9fa0c6c
commit
dba905bfa5
@ -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::<InstallerState>().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::<UiMessage>() {
|
||||
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::<InstallerState>()
|
||||
.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<Self, Self::Err> {
|
||||
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::<f64>()
|
||||
.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::<InstallerState>().cloned().unwrap();
|
||||
InstallerView::with_raw(&state, InstallProgressView::new(siv))
|
||||
}
|
||||
|
241
proxmox-tui-installer/src/views/install_progress.rs
Normal file
241
proxmox-tui-installer/src/views/install_progress.rs
Normal file
@ -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<LinearLayout>,
|
||||
}
|
||||
|
||||
impl InstallProgressView {
|
||||
pub fn new(siv: &mut Cursive) -> Self {
|
||||
let cb_sink = siv.cb_sink().clone();
|
||||
let state = siv.user_data::<InstallerState>().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::<UiMessage>() {
|
||||
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::<InstallerState>()
|
||||
.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<LinearLayout>);
|
||||
}
|
||||
|
||||
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<Self, Self::Err> {
|
||||
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::<f64>()
|
||||
.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}")),
|
||||
}
|
||||
}
|
||||
}
|
@ -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::*;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user