From 8fcdc5b2666b71a995925f8c20aa3d0dccd75d2d Mon Sep 17 00:00:00 2001 From: Christoph Heiss Date: Wed, 6 Dec 2023 12:34:52 +0100 Subject: [PATCH] tui, ui: switch over to JSON-based protocol Signed-off-by: Christoph Heiss --- Proxmox/UI/StdIO.pm | 41 ++++-- .../src/views/install_progress.rs | 117 ++++++++---------- 2 files changed, 83 insertions(+), 75 deletions(-) diff --git a/Proxmox/UI/StdIO.pm b/Proxmox/UI/StdIO.pm index a97245c..25d1c82 100644 --- a/Proxmox/UI/StdIO.pm +++ b/Proxmox/UI/StdIO.pm @@ -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 = // ''; # 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"; - - my $response = // ''; # FIXME: error handling? - - chomp($response); + &send_msg('prompt', query => $query); + my $response = &recv_msg(); - 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 { diff --git a/proxmox-tui-installer/src/views/install_progress.rs b/proxmox-tui-installer/src/views/install_progress.rs index 01c9941..741529f 100644 --- a/proxmox-tui-installer/src/views/install_progress.rs +++ b/proxmox-tui-installer/src/views/install_progress.rs @@ -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::() { + let msg = match serde_json::from_str::(&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(siv: &mut Cursive, text: &str, writer: Arc>) { + let send_answer = |writer: Arc>, 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); } -#[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 { - 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::() - .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, + }, } -- 2.39.2