tui, ui: switch over to JSON-based protocol

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
This commit is contained in:
Christoph Heiss 2023-12-06 12:34:52 +01:00 committed by Thomas Lamprecht
parent 573e3f41fb
commit 8fcdc5b266
2 changed files with 83 additions and 75 deletions

View File

@ -3,10 +3,26 @@ package Proxmox::UI::StdIO;
use strict;
use warnings;
use JSON qw(from_json to_json);
use base qw(Proxmox::UI::Base);
use Proxmox::Log;
my sub send_msg : prototype($$) {
my ($type, %values) = @_;
my $json = to_json({ type => $type, %values }, { utf8 => 1, canonical => 1 });
print STDOUT "$json\n";
}
my sub recv_msg : prototype() {
my $response = <STDIN> // ''; # FIXME: error handling?
chomp($response);
return eval { from_json($response, { utf8 => 1 }) };
}
sub init {
my ($self) = @_;
@ -16,34 +32,33 @@ sub init {
sub message {
my ($self, $msg) = @_;
print STDOUT "message: $msg\n";
&send_msg('message', message => $msg);
}
sub error {
my ($self, $msg) = @_;
log_err("error: $msg\n");
print STDOUT "error: $msg\n";
log_error("error: $msg");
&send_msg('error', message => $msg);
}
sub finished {
my ($self, $success, $msg) = @_;
my $state = $success ? 'ok' : 'err';
log_info("finished: $state, $msg\n");
print STDOUT "finished: $state, $msg\n";
log_info("finished: $state, $msg");
&send_msg('finished', state => $state, message => $msg);
}
sub prompt {
my ($self, $query) = @_;
$query =~ s/\n/ /g; # FIXME: use a better serialisation (e.g., JSON)
print STDOUT "prompt: $query\n";
&send_msg('prompt', query => $query);
my $response = &recv_msg();
my $response = <STDIN> // ''; # FIXME: error handling?
chomp($response);
return lc($response) eq 'ok';
if (defined($response) && $response->{type} eq 'prompt-answer') {
return lc($response->{answer}) eq 'ok';
}
}
sub display_html {
@ -57,7 +72,7 @@ sub progress {
$text = '' if !defined($text);
print STDOUT "progress: $ratio $text\n";
&send_msg('progress', ratio => $ratio, text => $text);
}
sub process_events {

View File

@ -1,17 +1,16 @@
use std::{
io::{BufRead, BufReader, Write},
str::FromStr,
sync::{Arc, Mutex},
thread,
time::Duration,
};
use cursive::{
utils::Counter,
view::{Nameable, Resizable, ViewWrapper},
views::{Dialog, DummyView, LinearLayout, PaddedView, ProgressBar, TextContent, TextView},
CbSink, Cursive,
};
use serde::Deserialize;
use std::{
io::{BufRead, BufReader, Write},
sync::{Arc, Mutex},
thread,
time::Duration,
};
use crate::{abort_install_button, prompt_dialog, setup::InstallConfig, InstallerState};
use proxmox_installer_common::setup::spawn_low_level_installer;
@ -95,7 +94,7 @@ impl InstallProgressView {
Err(err) => return Err(format!("low-level installer exited early: {err}")),
};
let msg = match line.parse::<UiMessage>() {
let msg = match serde_json::from_str::<UiMessage>(&line) {
Ok(msg) => msg,
Err(stray) => {
// Not a fatal error, so don't abort the installation by returning
@ -105,26 +104,26 @@ impl InstallProgressView {
};
let result = match msg.clone() {
UiMessage::Info(s) => cb_sink.send(Box::new(|siv| {
siv.add_layer(Dialog::info(s).title("Information"));
UiMessage::Info { message } => cb_sink.send(Box::new(|siv| {
siv.add_layer(Dialog::info(message).title("Information"));
})),
UiMessage::Error(s) => cb_sink.send(Box::new(|siv| {
siv.add_layer(Dialog::info(s).title("Error"));
UiMessage::Error { message } => cb_sink.send(Box::new(|siv| {
siv.add_layer(Dialog::info(message).title("Error"));
})),
UiMessage::Prompt(s) => cb_sink.send({
UiMessage::Prompt { query } => cb_sink.send({
let writer = writer.clone();
Box::new(move |siv| Self::show_prompt(siv, &s, writer))
Box::new(move |siv| Self::show_prompt(siv, &query, writer))
}),
UiMessage::Progress(ratio, s) => {
counter.set(ratio);
progress_text.set_content(s);
UiMessage::Progress { ratio, text } => {
counter.set((ratio * 100.).floor() as usize);
progress_text.set_content(text);
Ok(())
}
UiMessage::Finished(success, msg) => {
UiMessage::Finished { state, message } => {
counter.set(100);
progress_text.set_content(msg.to_owned());
progress_text.set_content(message.to_owned());
cb_sink.send(Box::new(move |siv| {
Self::prepare_for_reboot(siv, success, &msg)
Self::prepare_for_reboot(siv, state == "ok", &message);
}))
}
};
@ -189,6 +188,19 @@ impl InstallProgressView {
}
fn show_prompt<W: Write + 'static>(siv: &mut Cursive, text: &str, writer: Arc<Mutex<W>>) {
let send_answer = |writer: Arc<Mutex<W>>, answer| {
if let Ok(mut writer) = writer.lock() {
let _ = writeln!(
writer,
"{}",
serde_json::json!({
"type" : "prompt-answer",
"answer" : answer,
})
);
}
};
prompt_dialog(
siv,
"Prompt",
@ -197,16 +209,12 @@ impl InstallProgressView {
Box::new({
let writer = writer.clone();
move |_| {
if let Ok(mut writer) = writer.lock() {
let _ = writeln!(writer, "ok");
}
send_answer(writer.clone(), "ok");
}
}),
"Cancel",
Box::new(move |_| {
if let Ok(mut writer) = writer.lock() {
let _ = writeln!(writer);
}
send_answer(writer.clone(), "cancel");
}),
);
}
@ -216,40 +224,25 @@ impl ViewWrapper for InstallProgressView {
cursive::wrap_impl!(self.view: PaddedView<LinearLayout>);
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "lowercase")]
enum UiMessage {
Info(String),
Error(String),
Prompt(String),
Finished(bool, String),
Progress(usize, String),
}
impl FromStr for UiMessage {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (ty, rest) = s.split_once(": ").ok_or("invalid message: no type")?;
match ty {
"message" => Ok(UiMessage::Info(rest.to_owned())),
"error" => Ok(UiMessage::Error(rest.to_owned())),
"prompt" => Ok(UiMessage::Prompt(rest.to_owned())),
"finished" => {
let (state, rest) = rest.split_once(", ").ok_or("invalid message: no state")?;
Ok(UiMessage::Finished(state == "ok", rest.to_owned()))
}
"progress" => {
let (percent, rest) = rest.split_once(' ').ok_or("invalid progress message")?;
Ok(UiMessage::Progress(
percent
.parse::<f64>()
.map(|v| (v * 100.).floor() as usize)
.map_err(|err| err.to_string())?,
rest.to_owned(),
))
}
unknown => Err(format!("invalid message type {unknown}, rest: {rest}")),
}
}
#[serde(rename = "message")]
Info {
message: String,
},
Error {
message: String,
},
Prompt {
query: String,
},
Finished {
state: String,
message: String,
},
Progress {
ratio: f32,
text: String,
},
}