]> git.proxmox.com Git - pve-installer.git/blob - proxmox-tui-installer/src/views/install_progress.rs
a70b6cbbb0114b6bebf320d66eba91442cbe99bd
[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.stdout.take().map(BufReader::new)?;
79 let mut writer = child.stdin.take()?;
80
81 serde_json::to_writer(&mut writer, &InstallConfig::from(state.options)).unwrap();
82 writeln!(writer).unwrap();
83
84 let writer = Arc::new(Mutex::new(writer));
85
86 for line in reader.lines() {
87 let line = match line {
88 Ok(line) => line,
89 Err(_) => break,
90 };
91
92 let msg = match line.parse::<UiMessage>() {
93 Ok(msg) => msg,
94 Err(stray) => {
95 eprintln!("low-level installer: {stray}");
96 continue;
97 }
98 };
99
100 match msg {
101 UiMessage::Info(s) => cb_sink.send(Box::new(|siv| {
102 siv.add_layer(Dialog::info(s).title("Information"));
103 })),
104 UiMessage::Error(s) => cb_sink.send(Box::new(|siv| {
105 siv.add_layer(Dialog::info(s).title("Error"));
106 })),
107 UiMessage::Prompt(s) => cb_sink.send({
108 let writer = writer.clone();
109 Box::new(move |siv| {
110 yes_no_dialog(
111 siv,
112 "Prompt",
113 &s,
114 Box::new({
115 let writer = writer.clone();
116 move |_| {
117 if let Ok(mut writer) = writer.lock() {
118 let _ = writeln!(writer, "ok");
119 }
120 }
121 }),
122 Box::new(move |_| {
123 if let Ok(mut writer) = writer.lock() {
124 let _ = writeln!(writer);
125 }
126 }),
127 );
128 })
129 }),
130 UiMessage::Progress(ratio, s) => {
131 counter.set(ratio);
132 progress_text.set_content(s);
133 Ok(())
134 }
135 UiMessage::Finished(success, msg) => {
136 counter.set(100);
137 progress_text.set_content(msg.to_owned());
138 cb_sink.send(Box::new(move |siv| {
139 let title = if success { "Success" } else { "Failure" };
140
141 // For rebooting, we just need to quit the installer,
142 // our caller does the actual reboot.
143 siv.add_layer(
144 Dialog::text(msg)
145 .title(title)
146 .button("Reboot now", Cursive::quit),
147 );
148
149 let autoreboot = siv
150 .user_data::<InstallerState>()
151 .map(|state| state.options.autoreboot)
152 .unwrap_or_default();
153
154 if autoreboot && success {
155 let cb_sink = siv.cb_sink();
156 thread::spawn({
157 let cb_sink = cb_sink.clone();
158 move || {
159 thread::sleep(Duration::from_secs(5));
160 let _ = cb_sink.send(Box::new(Cursive::quit));
161 }
162 });
163 }
164 }))
165 }
166 }
167 .unwrap();
168 }
169
170 Some(())
171 };
172
173 if inner().is_none() {
174 cb_sink
175 .send(Box::new(|siv| {
176 siv.add_layer(
177 Dialog::text("low-level installer exited early")
178 .title("Error")
179 .button("Exit", Cursive::quit),
180 );
181 }))
182 .unwrap();
183 }
184 }
185 }
186
187 impl ViewWrapper for InstallProgressView {
188 cursive::wrap_impl!(self.view: PaddedView<LinearLayout>);
189 }
190
191 enum UiMessage {
192 Info(String),
193 Error(String),
194 Prompt(String),
195 Finished(bool, String),
196 Progress(usize, String),
197 }
198
199 impl FromStr for UiMessage {
200 type Err = String;
201
202 fn from_str(s: &str) -> Result<Self, Self::Err> {
203 let (ty, rest) = s.split_once(": ").ok_or("invalid message: no type")?;
204
205 match ty {
206 "message" => Ok(UiMessage::Info(rest.to_owned())),
207 "error" => Ok(UiMessage::Error(rest.to_owned())),
208 "prompt" => Ok(UiMessage::Prompt(rest.to_owned())),
209 "finished" => {
210 let (state, rest) = rest.split_once(", ").ok_or("invalid message: no state")?;
211 Ok(UiMessage::Finished(state == "ok", rest.to_owned()))
212 }
213 "progress" => {
214 let (percent, rest) = rest.split_once(' ').ok_or("invalid progress message")?;
215 Ok(UiMessage::Progress(
216 percent
217 .parse::<f64>()
218 .map(|v| (v * 100.).floor() as usize)
219 .map_err(|err| err.to_string())?,
220 rest.to_owned(),
221 ))
222 }
223 unknown => Err(format!("invalid message type {unknown}, rest: {rest}")),
224 }
225 }
226 }