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