]> git.proxmox.com Git - pve-installer.git/blob - proxmox-tui-installer/src/views/install_progress.rs
tui: move install progress dialog into own view module
[pve-installer.git] / proxmox-tui-installer / src / views / install_progress.rs
1 use std::{
2 io::{BufRead, BufReader, Write},
3 str::FromStr,
4 sync::{Arc, Mutex},
5 thread,
6 time::Duration,
7 };
8
9 use cursive::{
10 utils::Counter,
11 view::{Resizable, ViewWrapper},
12 views::{Dialog, DummyView, LinearLayout, PaddedView, ProgressBar, TextContent, TextView},
13 Cursive,
14 };
15
16 use crate::{abort_install_button, setup::InstallConfig, yes_no_dialog, InstallerState};
17
18 pub struct InstallProgressView {
19 view: PaddedView<LinearLayout>,
20 }
21
22 impl InstallProgressView {
23 pub fn new(siv: &mut Cursive) -> Self {
24 let cb_sink = siv.cb_sink().clone();
25 let state = siv.user_data::<InstallerState>().unwrap();
26 let progress_text = TextContent::new("starting the installation ..");
27
28 let progress_task = {
29 let progress_text = progress_text.clone();
30 let state = state.clone();
31 move |counter: Counter| {
32 let child = {
33 use std::process::{Command, Stdio};
34
35 let (path, args, envs): (&str, &[&str], Vec<(&str, &str)>) =
36 if state.in_test_mode {
37 (
38 "./proxmox-low-level-installer",
39 &["-t", "start-session-test"],
40 vec![("PERL5LIB", ".")],
41 )
42 } else {
43 ("proxmox-low-level-installer", &["start-session"], vec![])
44 };
45
46 Command::new(path)
47 .args(args)
48 .envs(envs)
49 .stdin(Stdio::piped())
50 .stdout(Stdio::piped())
51 .spawn()
52 };
53
54 let mut child = match child {
55 Ok(child) => child,
56 Err(err) => {
57 let _ = cb_sink.send(Box::new(move |siv| {
58 siv.add_layer(
59 Dialog::text(err.to_string())
60 .title("Error")
61 .button("Ok", Cursive::quit),
62 );
63 }));
64 return;
65 }
66 };
67
68 let inner = || {
69 let reader = child.stdout.take().map(BufReader::new)?;
70 let mut writer = child.stdin.take()?;
71
72 serde_json::to_writer(&mut writer, &InstallConfig::from(state.options))
73 .unwrap();
74 writeln!(writer).unwrap();
75
76 let writer = Arc::new(Mutex::new(writer));
77
78 for line in reader.lines() {
79 let line = match line {
80 Ok(line) => line,
81 Err(_) => break,
82 };
83
84 let msg = match line.parse::<UiMessage>() {
85 Ok(msg) => msg,
86 Err(stray) => {
87 eprintln!("low-level installer: {stray}");
88 continue;
89 }
90 };
91
92 match msg {
93 UiMessage::Info(s) => cb_sink.send(Box::new(|siv| {
94 siv.add_layer(Dialog::info(s).title("Information"));
95 })),
96 UiMessage::Error(s) => cb_sink.send(Box::new(|siv| {
97 siv.add_layer(Dialog::info(s).title("Error"));
98 })),
99 UiMessage::Prompt(s) => cb_sink.send({
100 let writer = writer.clone();
101 Box::new(move |siv| {
102 yes_no_dialog(
103 siv,
104 "Prompt",
105 &s,
106 Box::new({
107 let writer = writer.clone();
108 move |_| {
109 if let Ok(mut writer) = writer.lock() {
110 let _ = writeln!(writer, "ok");
111 }
112 }
113 }),
114 Box::new(move |_| {
115 if let Ok(mut writer) = writer.lock() {
116 let _ = writeln!(writer);
117 }
118 }),
119 );
120 })
121 }),
122 UiMessage::Progress(ratio, s) => {
123 counter.set(ratio);
124 progress_text.set_content(s);
125 Ok(())
126 }
127 UiMessage::Finished(success, msg) => {
128 counter.set(100);
129 progress_text.set_content(msg.to_owned());
130 cb_sink.send(Box::new(move |siv| {
131 let title = if success { "Success" } else { "Failure" };
132
133 // For rebooting, we just need to quit the installer,
134 // our caller does the actual reboot.
135 siv.add_layer(
136 Dialog::text(msg)
137 .title(title)
138 .button("Reboot now", Cursive::quit),
139 );
140
141 let autoreboot = siv
142 .user_data::<InstallerState>()
143 .map(|state| state.options.autoreboot)
144 .unwrap_or_default();
145
146 if autoreboot && success {
147 let cb_sink = siv.cb_sink();
148 thread::spawn({
149 let cb_sink = cb_sink.clone();
150 move || {
151 thread::sleep(Duration::from_secs(5));
152 let _ = cb_sink.send(Box::new(Cursive::quit));
153 }
154 });
155 }
156 }))
157 }
158 }
159 .unwrap();
160 }
161
162 Some(())
163 };
164
165 if inner().is_none() {
166 cb_sink
167 .send(Box::new(|siv| {
168 siv.add_layer(
169 Dialog::text("low-level installer exited early")
170 .title("Error")
171 .button("Exit", Cursive::quit),
172 );
173 }))
174 .unwrap();
175 }
176 }
177 };
178
179 let progress_bar = ProgressBar::new().with_task(progress_task).full_width();
180 let view = PaddedView::lrtb(
181 1,
182 1,
183 1,
184 1,
185 LinearLayout::vertical()
186 .child(PaddedView::lrtb(1, 1, 0, 0, progress_bar))
187 .child(DummyView)
188 .child(TextView::new_with_content(progress_text).center())
189 .child(PaddedView::lrtb(
190 1,
191 1,
192 1,
193 0,
194 LinearLayout::horizontal().child(abort_install_button()),
195 )),
196 );
197
198 Self { view }
199 }
200 }
201
202 impl ViewWrapper for InstallProgressView {
203 cursive::wrap_impl!(self.view: PaddedView<LinearLayout>);
204 }
205
206 enum UiMessage {
207 Info(String),
208 Error(String),
209 Prompt(String),
210 Finished(bool, String),
211 Progress(usize, String),
212 }
213
214 impl FromStr for UiMessage {
215 type Err = String;
216
217 fn from_str(s: &str) -> Result<Self, Self::Err> {
218 let (ty, rest) = s.split_once(": ").ok_or("invalid message: no type")?;
219
220 match ty {
221 "message" => Ok(UiMessage::Info(rest.to_owned())),
222 "error" => Ok(UiMessage::Error(rest.to_owned())),
223 "prompt" => Ok(UiMessage::Prompt(rest.to_owned())),
224 "finished" => {
225 let (state, rest) = rest.split_once(", ").ok_or("invalid message: no state")?;
226 Ok(UiMessage::Finished(state == "ok", rest.to_owned()))
227 }
228 "progress" => {
229 let (percent, rest) = rest.split_once(' ').ok_or("invalid progress message")?;
230 Ok(UiMessage::Progress(
231 percent
232 .parse::<f64>()
233 .map(|v| (v * 100.).floor() as usize)
234 .map_err(|err| err.to_string())?,
235 rest.to_owned(),
236 ))
237 }
238 unknown => Err(format!("invalid message type {unknown}, rest: {rest}")),
239 }
240 }
241 }