mirror of
https://git.proxmox.com/git/pve-installer
synced 2025-05-01 01:44:49 +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)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
use std::{
|
use std::{collections::HashMap, env, net::IpAddr};
|
||||||
collections::HashMap,
|
|
||||||
env,
|
|
||||||
io::{BufRead, BufReader, Write},
|
|
||||||
net::IpAddr,
|
|
||||||
str::FromStr,
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
thread,
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use cursive::{
|
use cursive::{
|
||||||
event::Event,
|
event::Event,
|
||||||
theme::{ColorStyle, Effect, PaletteColor, Style},
|
theme::{ColorStyle, Effect, PaletteColor, Style},
|
||||||
utils::Counter,
|
|
||||||
view::{Nameable, Offset, Resizable, ViewWrapper},
|
view::{Nameable, Offset, Resizable, ViewWrapper},
|
||||||
views::{
|
views::{
|
||||||
Button, Checkbox, Dialog, DummyView, EditView, Layer, LinearLayout, PaddedView, Panel,
|
Button, Checkbox, Dialog, DummyView, EditView, Layer, LinearLayout, PaddedView, Panel,
|
||||||
ProgressBar, ResizedView, ScrollView, SelectView, StackView, TextContent, TextView,
|
ResizedView, ScrollView, SelectView, StackView, TextView, ViewRef,
|
||||||
ViewRef,
|
|
||||||
},
|
},
|
||||||
Cursive, CursiveRunnable, ScreenId, View, XY,
|
Cursive, CursiveRunnable, ScreenId, View, XY,
|
||||||
};
|
};
|
||||||
@ -36,14 +25,13 @@ use proxmox_installer_common::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
mod setup;
|
mod setup;
|
||||||
use setup::InstallConfig;
|
|
||||||
|
|
||||||
mod system;
|
mod system;
|
||||||
|
|
||||||
mod views;
|
mod views;
|
||||||
use views::{
|
use views::{
|
||||||
BootdiskOptionsView, CidrAddressEditView, FormView, TableView, TableViewItem,
|
BootdiskOptionsView, CidrAddressEditView, FormView, InstallProgressView, TableView,
|
||||||
TimezoneOptionsView,
|
TableViewItem, TimezoneOptionsView,
|
||||||
};
|
};
|
||||||
|
|
||||||
// TextView::center() seems to garble the first two lines, so fix it manually here.
|
// 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
|
// Ensure the screen is updated independently of keyboard events and such
|
||||||
siv.set_autorefresh(true);
|
siv.set_autorefresh(true);
|
||||||
|
|
||||||
let cb_sink = siv.cb_sink().clone();
|
let state = siv.user_data::<InstallerState>().cloned().unwrap();
|
||||||
let state = siv.user_data::<InstallerState>().unwrap();
|
InstallerView::with_raw(&state, InstallProgressView::new(siv))
|
||||||
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}")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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;
|
mod bootdisk;
|
||||||
pub use bootdisk::*;
|
pub use bootdisk::*;
|
||||||
|
|
||||||
|
mod install_progress;
|
||||||
|
pub use install_progress::*;
|
||||||
|
|
||||||
mod table_view;
|
mod table_view;
|
||||||
pub use table_view::*;
|
pub use table_view::*;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user