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 strict;
use warnings; use warnings;
use JSON qw(from_json to_json);
use base qw(Proxmox::UI::Base); use base qw(Proxmox::UI::Base);
use Proxmox::Log; 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 { sub init {
my ($self) = @_; my ($self) = @_;
@ -16,34 +32,33 @@ sub init {
sub message { sub message {
my ($self, $msg) = @_; my ($self, $msg) = @_;
print STDOUT "message: $msg\n"; &send_msg('message', message => $msg);
} }
sub error { sub error {
my ($self, $msg) = @_; my ($self, $msg) = @_;
log_err("error: $msg\n");
print STDOUT "error: $msg\n"; log_error("error: $msg");
&send_msg('error', message => $msg);
} }
sub finished { sub finished {
my ($self, $success, $msg) = @_; my ($self, $success, $msg) = @_;
my $state = $success ? 'ok' : 'err'; my $state = $success ? 'ok' : 'err';
log_info("finished: $state, $msg\n"); log_info("finished: $state, $msg");
print STDOUT "finished: $state, $msg\n"; &send_msg('finished', state => $state, message => $msg);
} }
sub prompt { sub prompt {
my ($self, $query) = @_; my ($self, $query) = @_;
$query =~ s/\n/ /g; # FIXME: use a better serialisation (e.g., JSON) &send_msg('prompt', query => $query);
print STDOUT "prompt: $query\n"; my $response = &recv_msg();
my $response = <STDIN> // ''; # FIXME: error handling? if (defined($response) && $response->{type} eq 'prompt-answer') {
return lc($response->{answer}) eq 'ok';
chomp($response); }
return lc($response) eq 'ok';
} }
sub display_html { sub display_html {
@ -57,7 +72,7 @@ sub progress {
$text = '' if !defined($text); $text = '' if !defined($text);
print STDOUT "progress: $ratio $text\n"; &send_msg('progress', ratio => $ratio, text => $text);
} }
sub process_events { 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::{ use cursive::{
utils::Counter, utils::Counter,
view::{Nameable, Resizable, ViewWrapper}, view::{Nameable, Resizable, ViewWrapper},
views::{Dialog, DummyView, LinearLayout, PaddedView, ProgressBar, TextContent, TextView}, views::{Dialog, DummyView, LinearLayout, PaddedView, ProgressBar, TextContent, TextView},
CbSink, Cursive, 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 crate::{abort_install_button, prompt_dialog, setup::InstallConfig, InstallerState};
use proxmox_installer_common::setup::spawn_low_level_installer; 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}")), 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, Ok(msg) => msg,
Err(stray) => { Err(stray) => {
// Not a fatal error, so don't abort the installation by returning // Not a fatal error, so don't abort the installation by returning
@ -105,26 +104,26 @@ impl InstallProgressView {
}; };
let result = match msg.clone() { let result = match msg.clone() {
UiMessage::Info(s) => cb_sink.send(Box::new(|siv| { UiMessage::Info { message } => cb_sink.send(Box::new(|siv| {
siv.add_layer(Dialog::info(s).title("Information")); siv.add_layer(Dialog::info(message).title("Information"));
})), })),
UiMessage::Error(s) => cb_sink.send(Box::new(|siv| { UiMessage::Error { message } => cb_sink.send(Box::new(|siv| {
siv.add_layer(Dialog::info(s).title("Error")); siv.add_layer(Dialog::info(message).title("Error"));
})), })),
UiMessage::Prompt(s) => cb_sink.send({ UiMessage::Prompt { query } => cb_sink.send({
let writer = writer.clone(); 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) => { UiMessage::Progress { ratio, text } => {
counter.set(ratio); counter.set((ratio * 100.).floor() as usize);
progress_text.set_content(s); progress_text.set_content(text);
Ok(()) Ok(())
} }
UiMessage::Finished(success, msg) => { UiMessage::Finished { state, message } => {
counter.set(100); counter.set(100);
progress_text.set_content(msg.to_owned()); progress_text.set_content(message.to_owned());
cb_sink.send(Box::new(move |siv| { 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>>) { 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( prompt_dialog(
siv, siv,
"Prompt", "Prompt",
@ -197,16 +209,12 @@ impl InstallProgressView {
Box::new({ Box::new({
let writer = writer.clone(); let writer = writer.clone();
move |_| { move |_| {
if let Ok(mut writer) = writer.lock() { send_answer(writer.clone(), "ok");
let _ = writeln!(writer, "ok");
}
} }
}), }),
"Cancel", "Cancel",
Box::new(move |_| { Box::new(move |_| {
if let Ok(mut writer) = writer.lock() { send_answer(writer.clone(), "cancel");
let _ = writeln!(writer);
}
}), }),
); );
} }
@ -216,40 +224,25 @@ impl ViewWrapper for InstallProgressView {
cursive::wrap_impl!(self.view: PaddedView<LinearLayout>); cursive::wrap_impl!(self.view: PaddedView<LinearLayout>);
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "lowercase")]
enum UiMessage { enum UiMessage {
Info(String), #[serde(rename = "message")]
Error(String), Info {
Prompt(String), message: String,
Finished(bool, String), },
Progress(usize, String), Error {
} message: String,
},
impl FromStr for UiMessage { Prompt {
type Err = String; query: String,
},
fn from_str(s: &str) -> Result<Self, Self::Err> { Finished {
let (ty, rest) = s.split_once(": ").ok_or("invalid message: no type")?; state: String,
message: String,
match ty { },
"message" => Ok(UiMessage::Info(rest.to_owned())), Progress {
"error" => Ok(UiMessage::Error(rest.to_owned())), ratio: f32,
"prompt" => Ok(UiMessage::Prompt(rest.to_owned())), text: String,
"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}")),
}
}
} }