]>
Commit | Line | Data |
---|---|---|
dba905bf CH |
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, | |
091c64a7 | 11 | view::{Nameable, Resizable, ViewWrapper}, |
dba905bf | 12 | views::{Dialog, DummyView, LinearLayout, PaddedView, ProgressBar, TextContent, TextView}, |
c59da6c8 | 13 | CbSink, Cursive, |
dba905bf CH |
14 | }; |
15 | ||
16 | use crate::{abort_install_button, setup::InstallConfig, yes_no_dialog, 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 CH |
89 | |
90 | let writer = Arc::new(Mutex::new(writer)); | |
91 | ||
92 | for line in reader.lines() { | |
93 | let line = match line { | |
94 | Ok(line) => line, | |
c0517345 | 95 | Err(err) => return Err(format!("low-level installer exited early: {err}")), |
c59da6c8 CH |
96 | }; |
97 | ||
98 | let msg = match line.parse::<UiMessage>() { | |
99 | Ok(msg) => msg, | |
100 | Err(stray) => { | |
c0517345 | 101 | // Not a fatal error, so don't abort the installation by returning |
c59da6c8 CH |
102 | eprintln!("low-level installer: {stray}"); |
103 | continue; | |
104 | } | |
105 | }; | |
106 | ||
c0517345 | 107 | let result = match msg.clone() { |
c59da6c8 CH |
108 | UiMessage::Info(s) => cb_sink.send(Box::new(|siv| { |
109 | siv.add_layer(Dialog::info(s).title("Information")); | |
110 | })), | |
111 | UiMessage::Error(s) => cb_sink.send(Box::new(|siv| { | |
112 | siv.add_layer(Dialog::info(s).title("Error")); | |
113 | })), | |
114 | UiMessage::Prompt(s) => cb_sink.send({ | |
115 | let writer = writer.clone(); | |
4c808a1b | 116 | Box::new(move |siv| Self::show_prompt(siv, &s, writer)) |
c59da6c8 CH |
117 | }), |
118 | UiMessage::Progress(ratio, s) => { | |
119 | counter.set(ratio); | |
120 | progress_text.set_content(s); | |
121 | Ok(()) | |
122 | } | |
123 | UiMessage::Finished(success, msg) => { | |
124 | counter.set(100); | |
125 | progress_text.set_content(msg.to_owned()); | |
126 | cb_sink.send(Box::new(move |siv| { | |
7d09f0ab | 127 | Self::prepare_for_reboot(siv, success, &msg) |
c59da6c8 CH |
128 | })) |
129 | } | |
c0517345 CH |
130 | }; |
131 | ||
132 | if let Err(err) = result { | |
133 | eprintln!("error during message handling: {err}"); | |
134 | eprintln!(" message was: '{msg:?}"); | |
c59da6c8 | 135 | } |
c59da6c8 CH |
136 | } |
137 | ||
c0517345 | 138 | Ok(()) |
c59da6c8 CH |
139 | }; |
140 | ||
c0517345 CH |
141 | if let Err(err) = inner() { |
142 | let message = format!("installation failed: {err}"); | |
c59da6c8 CH |
143 | cb_sink |
144 | .send(Box::new(|siv| { | |
145 | siv.add_layer( | |
c0517345 | 146 | Dialog::text(message) |
c59da6c8 CH |
147 | .title("Error") |
148 | .button("Exit", Cursive::quit), | |
149 | ); | |
150 | })) | |
151 | .unwrap(); | |
152 | } | |
153 | } | |
7d09f0ab CH |
154 | |
155 | fn prepare_for_reboot(siv: &mut Cursive, success: bool, msg: &str) { | |
091c64a7 | 156 | const DIALOG_ID: &str = "autoreboot-dialog"; |
7d09f0ab CH |
157 | let title = if success { "Success" } else { "Failure" }; |
158 | ||
091c64a7 CH |
159 | // If the dialog was previously created, just update its content and we're done. |
160 | if let Some(mut dialog) = siv.find_name::<Dialog>(DIALOG_ID) { | |
161 | dialog.set_content(TextView::new(msg)); | |
162 | return; | |
163 | } | |
164 | ||
7d09f0ab CH |
165 | // For rebooting, we just need to quit the installer, |
166 | // our caller does the actual reboot. | |
167 | siv.add_layer( | |
168 | Dialog::text(msg) | |
169 | .title(title) | |
091c64a7 CH |
170 | .button("Reboot now", Cursive::quit) |
171 | .with_name(DIALOG_ID), | |
7d09f0ab CH |
172 | ); |
173 | ||
174 | let autoreboot = siv | |
175 | .user_data::<InstallerState>() | |
176 | .map(|state| state.options.autoreboot) | |
177 | .unwrap_or_default(); | |
178 | ||
179 | if autoreboot && success { | |
180 | let cb_sink = siv.cb_sink(); | |
181 | thread::spawn({ | |
182 | let cb_sink = cb_sink.clone(); | |
183 | move || { | |
184 | thread::sleep(Duration::from_secs(5)); | |
185 | let _ = cb_sink.send(Box::new(Cursive::quit)); | |
186 | } | |
187 | }); | |
188 | } | |
189 | } | |
4c808a1b CH |
190 | |
191 | fn show_prompt<W: Write + 'static>(siv: &mut Cursive, text: &str, writer: Arc<Mutex<W>>) { | |
192 | yes_no_dialog( | |
193 | siv, | |
194 | "Prompt", | |
195 | text, | |
196 | Box::new({ | |
197 | let writer = writer.clone(); | |
198 | move |_| { | |
199 | if let Ok(mut writer) = writer.lock() { | |
200 | let _ = writeln!(writer, "ok"); | |
201 | } | |
202 | } | |
203 | }), | |
204 | Box::new(move |_| { | |
205 | if let Ok(mut writer) = writer.lock() { | |
206 | let _ = writeln!(writer); | |
207 | } | |
208 | }), | |
209 | ); | |
210 | } | |
dba905bf CH |
211 | } |
212 | ||
213 | impl ViewWrapper for InstallProgressView { | |
214 | cursive::wrap_impl!(self.view: PaddedView<LinearLayout>); | |
215 | } | |
216 | ||
c0517345 | 217 | #[derive(Clone, Debug)] |
dba905bf CH |
218 | enum UiMessage { |
219 | Info(String), | |
220 | Error(String), | |
221 | Prompt(String), | |
222 | Finished(bool, String), | |
223 | Progress(usize, String), | |
224 | } | |
225 | ||
226 | impl FromStr for UiMessage { | |
227 | type Err = String; | |
228 | ||
229 | fn from_str(s: &str) -> Result<Self, Self::Err> { | |
230 | let (ty, rest) = s.split_once(": ").ok_or("invalid message: no type")?; | |
231 | ||
232 | match ty { | |
233 | "message" => Ok(UiMessage::Info(rest.to_owned())), | |
234 | "error" => Ok(UiMessage::Error(rest.to_owned())), | |
235 | "prompt" => Ok(UiMessage::Prompt(rest.to_owned())), | |
236 | "finished" => { | |
237 | let (state, rest) = rest.split_once(", ").ok_or("invalid message: no state")?; | |
238 | Ok(UiMessage::Finished(state == "ok", rest.to_owned())) | |
239 | } | |
240 | "progress" => { | |
241 | let (percent, rest) = rest.split_once(' ').ok_or("invalid progress message")?; | |
242 | Ok(UiMessage::Progress( | |
243 | percent | |
244 | .parse::<f64>() | |
245 | .map(|v| (v * 100.).floor() as usize) | |
246 | .map_err(|err| err.to_string())?, | |
247 | rest.to_owned(), | |
248 | )) | |
249 | } | |
250 | unknown => Err(format!("invalid message type {unknown}, rest: {rest}")), | |
251 | } | |
252 | } | |
253 | } |