]> git.proxmox.com Git - pve-installer.git/blob - proxmox-tui-installer/src/views/install_progress.rs
96e62f83eb0ff418f425a369b350204d2ffba4a5
[pve-installer.git] / proxmox-tui-installer / src / views / install_progress.rs
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,
11 view::{Resizable, ViewWrapper},
12 views::{Dialog, DummyView, LinearLayout, PaddedView, ProgressBar, TextContent, TextView},
13 CbSink, Cursive,
14 };
15
16 use crate::{abort_install_button, setup::InstallConfig, yes_no_dialog, InstallerState};
17 use proxmox_installer_common::setup::spawn_low_level_installer;
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();
32 move |counter: Counter| Self::progress_task(counter, cb_sink, state, progress_text)
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 }
56
57 fn progress_task(
58 counter: Counter,
59 cb_sink: CbSink,
60 state: InstallerState,
61 progress_text: TextContent,
62 ) {
63 let mut child = match spawn_low_level_installer(state.in_test_mode) {
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 = || {
78 let reader = child
79 .stdout
80 .take()
81 .map(BufReader::new)
82 .ok_or("failed to get stdin reader")?;
83
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}"))?;
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,
95 Err(err) => return Err(format!("low-level installer exited early: {err}")),
96 };
97
98 let msg = match line.parse::<UiMessage>() {
99 Ok(msg) => msg,
100 Err(stray) => {
101 // Not a fatal error, so don't abort the installation by returning
102 eprintln!("low-level installer: {stray}");
103 continue;
104 }
105 };
106
107 let result = match msg.clone() {
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();
116 Box::new(move |siv| Self::show_prompt(siv, &s, writer))
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| {
127 Self::prepare_for_reboot(siv, success, &msg)
128 }))
129 }
130 };
131
132 if let Err(err) = result {
133 eprintln!("error during message handling: {err}");
134 eprintln!(" message was: '{msg:?}");
135 }
136 }
137
138 Ok(())
139 };
140
141 if let Err(err) = inner() {
142 let message = format!("installation failed: {err}");
143 cb_sink
144 .send(Box::new(|siv| {
145 siv.add_layer(
146 Dialog::text(message)
147 .title("Error")
148 .button("Exit", Cursive::quit),
149 );
150 }))
151 .unwrap();
152 }
153 }
154
155 fn prepare_for_reboot(siv: &mut Cursive, success: bool, msg: &str) {
156 let title = if success { "Success" } else { "Failure" };
157
158 // For rebooting, we just need to quit the installer,
159 // our caller does the actual reboot.
160 siv.add_layer(
161 Dialog::text(msg)
162 .title(title)
163 .button("Reboot now", Cursive::quit),
164 );
165
166 let autoreboot = siv
167 .user_data::<InstallerState>()
168 .map(|state| state.options.autoreboot)
169 .unwrap_or_default();
170
171 if autoreboot && success {
172 let cb_sink = siv.cb_sink();
173 thread::spawn({
174 let cb_sink = cb_sink.clone();
175 move || {
176 thread::sleep(Duration::from_secs(5));
177 let _ = cb_sink.send(Box::new(Cursive::quit));
178 }
179 });
180 }
181 }
182
183 fn show_prompt<W: Write + 'static>(siv: &mut Cursive, text: &str, writer: Arc<Mutex<W>>) {
184 yes_no_dialog(
185 siv,
186 "Prompt",
187 text,
188 Box::new({
189 let writer = writer.clone();
190 move |_| {
191 if let Ok(mut writer) = writer.lock() {
192 let _ = writeln!(writer, "ok");
193 }
194 }
195 }),
196 Box::new(move |_| {
197 if let Ok(mut writer) = writer.lock() {
198 let _ = writeln!(writer);
199 }
200 }),
201 );
202 }
203 }
204
205 impl ViewWrapper for InstallProgressView {
206 cursive::wrap_impl!(self.view: PaddedView<LinearLayout>);
207 }
208
209 #[derive(Clone, Debug)]
210 enum UiMessage {
211 Info(String),
212 Error(String),
213 Prompt(String),
214 Finished(bool, String),
215 Progress(usize, String),
216 }
217
218 impl FromStr for UiMessage {
219 type Err = String;
220
221 fn from_str(s: &str) -> Result<Self, Self::Err> {
222 let (ty, rest) = s.split_once(": ").ok_or("invalid message: no type")?;
223
224 match ty {
225 "message" => Ok(UiMessage::Info(rest.to_owned())),
226 "error" => Ok(UiMessage::Error(rest.to_owned())),
227 "prompt" => Ok(UiMessage::Prompt(rest.to_owned())),
228 "finished" => {
229 let (state, rest) = rest.split_once(", ").ok_or("invalid message: no state")?;
230 Ok(UiMessage::Finished(state == "ok", rest.to_owned()))
231 }
232 "progress" => {
233 let (percent, rest) = rest.split_once(' ').ok_or("invalid progress message")?;
234 Ok(UiMessage::Progress(
235 percent
236 .parse::<f64>()
237 .map(|v| (v * 100.).floor() as usize)
238 .map_err(|err| err.to_string())?,
239 rest.to_owned(),
240 ))
241 }
242 unknown => Err(format!("invalid message type {unknown}, rest: {rest}")),
243 }
244 }
245 }