]> git.proxmox.com Git - pve-installer.git/blob - proxmox-tui-installer/src/views/install_progress.rs
741529f8a7bb2eaae2aa515aae68317e89ef4eda
[pve-installer.git] / proxmox-tui-installer / src / views / install_progress.rs
1 use cursive::{
2 utils::Counter,
3 view::{Nameable, Resizable, ViewWrapper},
4 views::{Dialog, DummyView, LinearLayout, PaddedView, ProgressBar, TextContent, TextView},
5 CbSink, Cursive,
6 };
7 use serde::Deserialize;
8 use std::{
9 io::{BufRead, BufReader, Write},
10 sync::{Arc, Mutex},
11 thread,
12 time::Duration,
13 };
14
15 use crate::{abort_install_button, prompt_dialog, setup::InstallConfig, InstallerState};
16 use proxmox_installer_common::setup::spawn_low_level_installer;
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();
31 move |counter: Counter| Self::progress_task(counter, cb_sink, state, progress_text)
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 }
55
56 fn progress_task(
57 counter: Counter,
58 cb_sink: CbSink,
59 state: InstallerState,
60 progress_text: TextContent,
61 ) {
62 let mut child = match spawn_low_level_installer(state.in_test_mode) {
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 = || {
77 let reader = child
78 .stdout
79 .take()
80 .map(BufReader::new)
81 .ok_or("failed to get stdin reader")?;
82
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}"))?;
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,
94 Err(err) => return Err(format!("low-level installer exited early: {err}")),
95 };
96
97 let msg = match serde_json::from_str::<UiMessage>(&line) {
98 Ok(msg) => msg,
99 Err(stray) => {
100 // Not a fatal error, so don't abort the installation by returning
101 eprintln!("low-level installer: {stray}");
102 continue;
103 }
104 };
105
106 let result = match msg.clone() {
107 UiMessage::Info { message } => cb_sink.send(Box::new(|siv| {
108 siv.add_layer(Dialog::info(message).title("Information"));
109 })),
110 UiMessage::Error { message } => cb_sink.send(Box::new(|siv| {
111 siv.add_layer(Dialog::info(message).title("Error"));
112 })),
113 UiMessage::Prompt { query } => cb_sink.send({
114 let writer = writer.clone();
115 Box::new(move |siv| Self::show_prompt(siv, &query, writer))
116 }),
117 UiMessage::Progress { ratio, text } => {
118 counter.set((ratio * 100.).floor() as usize);
119 progress_text.set_content(text);
120 Ok(())
121 }
122 UiMessage::Finished { state, message } => {
123 counter.set(100);
124 progress_text.set_content(message.to_owned());
125 cb_sink.send(Box::new(move |siv| {
126 Self::prepare_for_reboot(siv, state == "ok", &message);
127 }))
128 }
129 };
130
131 if let Err(err) = result {
132 eprintln!("error during message handling: {err}");
133 eprintln!(" message was: '{msg:?}");
134 }
135 }
136
137 Ok(())
138 };
139
140 if let Err(err) = inner() {
141 let message = format!("installation failed: {err}");
142 cb_sink
143 .send(Box::new(|siv| {
144 siv.add_layer(
145 Dialog::text(message)
146 .title("Error")
147 .button("Exit", Cursive::quit),
148 );
149 }))
150 .unwrap();
151 }
152 }
153
154 fn prepare_for_reboot(siv: &mut Cursive, success: bool, msg: &str) {
155 const DIALOG_ID: &str = "autoreboot-dialog";
156 let title = if success { "Success" } else { "Failure" };
157
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
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)
169 .button("Reboot now", Cursive::quit)
170 .with_name(DIALOG_ID),
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 }
189
190 fn show_prompt<W: Write + 'static>(siv: &mut Cursive, text: &str, writer: Arc<Mutex<W>>) {
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
204 prompt_dialog(
205 siv,
206 "Prompt",
207 text,
208 "OK",
209 Box::new({
210 let writer = writer.clone();
211 move |_| {
212 send_answer(writer.clone(), "ok");
213 }
214 }),
215 "Cancel",
216 Box::new(move |_| {
217 send_answer(writer.clone(), "cancel");
218 }),
219 );
220 }
221 }
222
223 impl ViewWrapper for InstallProgressView {
224 cursive::wrap_impl!(self.view: PaddedView<LinearLayout>);
225 }
226
227 #[derive(Clone, Debug, Deserialize, PartialEq)]
228 #[serde(tag = "type", rename_all = "lowercase")]
229 enum UiMessage {
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 },
248 }