]> git.proxmox.com Git - pve-installer.git/blame - proxmox-tui-installer/src/views/install_progress.rs
tui: install_progress: write low-level non-JSON messages to separate file
[pve-installer.git] / proxmox-tui-installer / src / views / install_progress.rs
CommitLineData
dba905bf
CH
1use cursive::{
2 utils::Counter,
091c64a7 3 view::{Nameable, Resizable, ViewWrapper},
dba905bf 4 views::{Dialog, DummyView, LinearLayout, PaddedView, ProgressBar, TextContent, TextView},
c59da6c8 5 CbSink, Cursive,
dba905bf 6};
8fcdc5b2
CH
7use serde::Deserialize;
8use std::{
22de6e5f 9 fs::File,
8fcdc5b2
CH
10 io::{BufRead, BufReader, Write},
11 sync::{Arc, Mutex},
12 thread,
13 time::Duration,
14};
dba905bf 15
28a55aea 16use crate::{abort_install_button, prompt_dialog, setup::InstallConfig, InstallerState};
923be763 17use proxmox_installer_common::setup::spawn_low_level_installer;
dba905bf
CH
18
19pub struct InstallProgressView {
20 view: PaddedView<LinearLayout>,
21}
22
23impl InstallProgressView {
24 pub fn new(siv: &mut Cursive) -> Self {
25 let cb_sink = siv.cb_sink().clone();
26 let state = siv.user_data::<InstallerState>().unwrap();
27 let progress_text = TextContent::new("starting the installation ..");
28
29 let progress_task = {
30 let progress_text = progress_text.clone();
31 let state = state.clone();
c59da6c8 32 move |counter: Counter| Self::progress_task(counter, cb_sink, state, progress_text)
dba905bf
CH
33 };
34
35 let progress_bar = ProgressBar::new().with_task(progress_task).full_width();
36 let view = PaddedView::lrtb(
37 1,
38 1,
39 1,
40 1,
41 LinearLayout::vertical()
42 .child(PaddedView::lrtb(1, 1, 0, 0, progress_bar))
43 .child(DummyView)
44 .child(TextView::new_with_content(progress_text).center())
45 .child(PaddedView::lrtb(
46 1,
47 1,
48 1,
49 0,
50 LinearLayout::horizontal().child(abort_install_button()),
51 )),
52 );
53
54 Self { view }
55 }
c59da6c8
CH
56
57 fn progress_task(
58 counter: Counter,
59 cb_sink: CbSink,
60 state: InstallerState,
61 progress_text: TextContent,
62 ) {
923be763 63 let mut child = match spawn_low_level_installer(state.in_test_mode) {
c59da6c8
CH
64 Ok(child) => child,
65 Err(err) => {
66 let _ = cb_sink.send(Box::new(move |siv| {
67 siv.add_layer(
68 Dialog::text(err.to_string())
69 .title("Error")
70 .button("Ok", Cursive::quit),
71 );
72 }));
73 return;
74 }
75 };
76
77 let inner = || {
c0517345
CH
78 let reader = child
79 .stdout
80 .take()
81 .map(BufReader::new)
82 .ok_or("failed to get stdin reader")?;
c59da6c8 83
c0517345
CH
84 let mut writer = child.stdin.take().ok_or("failed to get stdin writer")?;
85
86 serde_json::to_writer(&mut writer, &InstallConfig::from(state.options))
87 .map_err(|err| format!("failed to serialize install config: {err}"))?;
88 writeln!(writer).map_err(|err| format!("failed to write install config: {err}"))?;
c59da6c8 89
22de6e5f
CH
90 let mut lowlevel_log = File::create("/tmp/install-low-level.log")
91 .map_err(|err| format!("failed to open low-level installer logfile: {err}"))?;
92
c59da6c8
CH
93 let writer = Arc::new(Mutex::new(writer));
94
95 for line in reader.lines() {
96 let line = match line {
97 Ok(line) => line,
c0517345 98 Err(err) => return Err(format!("low-level installer exited early: {err}")),
c59da6c8
CH
99 };
100
22de6e5f
CH
101 // The low-level installer also spews the output of any command it runs on its
102 // stdout. Use a very simple heuricstic to determine whether it is actually JSON
103 // or not.
104 if !line.starts_with('{') || !line.ends_with('}') {
105 let _ = writeln!(lowlevel_log, "{}", line);
106 continue;
107 }
108
8fcdc5b2 109 let msg = match serde_json::from_str::<UiMessage>(&line) {
c59da6c8 110 Ok(msg) => msg,
22de6e5f 111 Err(err) => {
c0517345 112 // Not a fatal error, so don't abort the installation by returning
22de6e5f
CH
113 eprintln!("low-level installer: error while parsing message: '{err}'");
114 eprintln!(" original message was: '{line}'");
c59da6c8
CH
115 continue;
116 }
117 };
118
c0517345 119 let result = match msg.clone() {
8fcdc5b2
CH
120 UiMessage::Info { message } => cb_sink.send(Box::new(|siv| {
121 siv.add_layer(Dialog::info(message).title("Information"));
c59da6c8 122 })),
8fcdc5b2
CH
123 UiMessage::Error { message } => cb_sink.send(Box::new(|siv| {
124 siv.add_layer(Dialog::info(message).title("Error"));
c59da6c8 125 })),
8fcdc5b2 126 UiMessage::Prompt { query } => cb_sink.send({
c59da6c8 127 let writer = writer.clone();
8fcdc5b2 128 Box::new(move |siv| Self::show_prompt(siv, &query, writer))
c59da6c8 129 }),
8fcdc5b2
CH
130 UiMessage::Progress { ratio, text } => {
131 counter.set((ratio * 100.).floor() as usize);
132 progress_text.set_content(text);
c59da6c8
CH
133 Ok(())
134 }
8fcdc5b2 135 UiMessage::Finished { state, message } => {
c59da6c8 136 counter.set(100);
8fcdc5b2 137 progress_text.set_content(message.to_owned());
c59da6c8 138 cb_sink.send(Box::new(move |siv| {
8fcdc5b2 139 Self::prepare_for_reboot(siv, state == "ok", &message);
c59da6c8
CH
140 }))
141 }
c0517345
CH
142 };
143
144 if let Err(err) = result {
145 eprintln!("error during message handling: {err}");
146 eprintln!(" message was: '{msg:?}");
c59da6c8 147 }
c59da6c8
CH
148 }
149
c0517345 150 Ok(())
c59da6c8
CH
151 };
152
c0517345
CH
153 if let Err(err) = inner() {
154 let message = format!("installation failed: {err}");
c59da6c8
CH
155 cb_sink
156 .send(Box::new(|siv| {
157 siv.add_layer(
c0517345 158 Dialog::text(message)
c59da6c8
CH
159 .title("Error")
160 .button("Exit", Cursive::quit),
161 );
162 }))
163 .unwrap();
164 }
165 }
7d09f0ab
CH
166
167 fn prepare_for_reboot(siv: &mut Cursive, success: bool, msg: &str) {
091c64a7 168 const DIALOG_ID: &str = "autoreboot-dialog";
7d09f0ab
CH
169 let title = if success { "Success" } else { "Failure" };
170
091c64a7
CH
171 // If the dialog was previously created, just update its content and we're done.
172 if let Some(mut dialog) = siv.find_name::<Dialog>(DIALOG_ID) {
173 dialog.set_content(TextView::new(msg));
174 return;
175 }
176
7d09f0ab
CH
177 // For rebooting, we just need to quit the installer,
178 // our caller does the actual reboot.
179 siv.add_layer(
180 Dialog::text(msg)
181 .title(title)
091c64a7
CH
182 .button("Reboot now", Cursive::quit)
183 .with_name(DIALOG_ID),
7d09f0ab
CH
184 );
185
186 let autoreboot = siv
187 .user_data::<InstallerState>()
188 .map(|state| state.options.autoreboot)
189 .unwrap_or_default();
190
191 if autoreboot && success {
192 let cb_sink = siv.cb_sink();
193 thread::spawn({
194 let cb_sink = cb_sink.clone();
195 move || {
196 thread::sleep(Duration::from_secs(5));
197 let _ = cb_sink.send(Box::new(Cursive::quit));
198 }
199 });
200 }
201 }
4c808a1b
CH
202
203 fn show_prompt<W: Write + 'static>(siv: &mut Cursive, text: &str, writer: Arc<Mutex<W>>) {
8fcdc5b2
CH
204 let send_answer = |writer: Arc<Mutex<W>>, answer| {
205 if let Ok(mut writer) = writer.lock() {
206 let _ = writeln!(
207 writer,
208 "{}",
209 serde_json::json!({
210 "type" : "prompt-answer",
211 "answer" : answer,
212 })
213 );
214 }
215 };
216
28a55aea 217 prompt_dialog(
4c808a1b
CH
218 siv,
219 "Prompt",
220 text,
28a55aea 221 "OK",
4c808a1b
CH
222 Box::new({
223 let writer = writer.clone();
224 move |_| {
8fcdc5b2 225 send_answer(writer.clone(), "ok");
4c808a1b
CH
226 }
227 }),
28a55aea 228 "Cancel",
4c808a1b 229 Box::new(move |_| {
8fcdc5b2 230 send_answer(writer.clone(), "cancel");
4c808a1b
CH
231 }),
232 );
233 }
dba905bf
CH
234}
235
236impl ViewWrapper for InstallProgressView {
237 cursive::wrap_impl!(self.view: PaddedView<LinearLayout>);
238}
239
8fcdc5b2
CH
240#[derive(Clone, Debug, Deserialize, PartialEq)]
241#[serde(tag = "type", rename_all = "lowercase")]
dba905bf 242enum UiMessage {
8fcdc5b2
CH
243 #[serde(rename = "message")]
244 Info {
245 message: String,
246 },
247 Error {
248 message: String,
249 },
250 Prompt {
251 query: String,
252 },
253 Finished {
254 state: String,
255 message: String,
256 },
257 Progress {
258 ratio: f32,
259 text: String,
260 },
dba905bf 261}
240f1f00
CH
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266 use std::env;
267
268 #[test]
269 fn run_low_level_installer_test_session() {
270 env::set_current_dir("..").expect("failed to change working directory");
271 let mut child = spawn_low_level_installer(true)
272 .expect("failed to run low-level installer test session");
273
274 let mut reader = child
275 .stdout
276 .take()
277 .map(BufReader::new)
278 .expect("failed to get stdin reader");
279
280 let mut writer = child.stdin.take().expect("failed to get stdin writer");
281
282 serde_json::to_writer(&mut writer, &serde_json::json!({ "autoreboot": false }))
283 .expect("failed to serialize install config");
284
285 writeln!(writer).expect("failed to write install config: {err}");
286
287 let mut next_msg = || {
288 let mut line = String::new();
289 reader.read_line(&mut line).expect("a line");
290
291 match serde_json::from_str::<UiMessage>(&line) {
292 Ok(msg) => Some(msg),
293 Err(err) => panic!("unexpected error: '{err}'"),
294 }
295 };
296
297 assert_eq!(
298 next_msg(),
299 Some(UiMessage::Prompt {
300 query: "Reply anything?".to_owned()
301 }),
302 );
303
304 serde_json::to_writer(
305 &mut writer,
306 &serde_json::json!({"type": "prompt-answer", "answer": "ok"}),
307 )
308 .expect("failed to write prompt answer");
309 writeln!(writer).expect("failed to write prompt answer");
310
311 assert_eq!(
312 next_msg(),
313 Some(UiMessage::Info {
314 message: "Test Message - got ok".to_owned()
315 }),
316 );
317
318 for i in (1..=1000).step_by(3) {
319 assert_eq!(
320 next_msg(),
321 Some(UiMessage::Progress {
322 ratio: (i as f32) / 1000.,
323 text: format!("foo {i}"),
324 }),
325 );
326 }
327
328 assert_eq!(
329 next_msg(),
330 Some(UiMessage::Finished {
331 state: "ok".to_owned(),
332 message: "Installation finished - reboot now?".to_owned(),
333 }),
334 );
335
336 // Should be nothing left to read now
337 let mut line = String::new();
338 assert_eq!(reader.read_line(&mut line).expect("success"), 0);
339
340 // Give the low-level installer some time to exit properly
341 std::thread::sleep(Duration::new(1, 0));
342
343 match child.try_wait() {
344 Ok(Some(status)) => assert!(
345 status.success(),
346 "low-level installer did not exit successfully"
347 ),
348 Ok(None) => {
349 child.kill().expect("could not kill low-level installer");
350 panic!("low-level install was not successful");
351 }
352 Err(err) => panic!("failed to wait for low-level installer: {err}"),
353 }
354 }
355}