]> git.proxmox.com Git - pve-installer.git/blame - proxmox-tui-installer/src/views/install_progress.rs
low-level, tui: count down auto-reboot timeout
[pve-installer.git] / proxmox-tui-installer / src / views / install_progress.rs
CommitLineData
dba905bf
CH
1use std::{
2 io::{BufRead, BufReader, Write},
3 str::FromStr,
4 sync::{Arc, Mutex},
5 thread,
6 time::Duration,
7};
8
9use 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
16use crate::{abort_install_button, setup::InstallConfig, yes_no_dialog, 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
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
213impl ViewWrapper for InstallProgressView {
214 cursive::wrap_impl!(self.view: PaddedView<LinearLayout>);
215}
216
c0517345 217#[derive(Clone, Debug)]
dba905bf
CH
218enum UiMessage {
219 Info(String),
220 Error(String),
221 Prompt(String),
222 Finished(bool, String),
223 Progress(usize, String),
224}
225
226impl 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}