]>
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::{ | |
22de6e5f | 9 | fs::File, |
8fcdc5b2 CH |
10 | io::{BufRead, BufReader, Write}, |
11 | sync::{Arc, Mutex}, | |
12 | thread, | |
13 | time::Duration, | |
14 | }; | |
dba905bf | 15 | |
28a55aea | 16 | use crate::{abort_install_button, prompt_dialog, setup::InstallConfig, InstallerState}; |
923be763 | 17 | use proxmox_installer_common::setup::spawn_low_level_installer; |
dba905bf CH |
18 | |
19 | pub struct InstallProgressView { | |
20 | view: PaddedView<LinearLayout>, | |
21 | } | |
22 | ||
23 | impl 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 | ||
236 | impl 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 | 242 | enum 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)] | |
264 | mod 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 | } |