Migrate tauri from portmaster-ui to desktop/tauri. Update build system

This commit is contained in:
Patrick Pacher
2024-03-22 11:45:18 +01:00
parent ac23ce32a1
commit d524bce166
35 changed files with 10960 additions and 42 deletions

View File

@@ -0,0 +1,191 @@
use futures_util::{SinkExt, StreamExt};
use http::Uri;
use log::{debug, error, warn};
use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::sync::mpsc::{channel, Receiver, Sender};
use tokio::sync::RwLock;
use tokio_websockets::{ClientBuilder, Error};
use super::message::*;
use super::types::*;
/// An internal representation of a Command that
/// contains the PortAPI message as well as a response
/// channel that will receive all responses sent from the
/// server.
///
/// Users should normally not need to use the Command struct
/// directly since `PortAPI` already abstracts the creation of
/// mpsc channels.
struct Command {
msg: Message,
response: Sender<Response>,
}
/// The client implementation for PortAPI.
#[derive(Clone)]
pub struct PortAPI {
dispatch: Sender<Command>,
}
/// The map type used to store message subscribers.
type SubscriberMap = RwLock<HashMap<usize, Sender<Response>>>;
/// Connect to PortAPI at the specified URI.
///
/// This method will launch a new async thread on the `tauri::async_runtime`
/// that will handle message to transmit and also multiplex server responses
/// to the appropriate subscriber.
pub async fn connect(uri: &str) -> Result<PortAPI, Error> {
let parsed = match uri.parse::<Uri>() {
Ok(u) => u,
Err(_e) => {
return Err(Error::NoUriConfigured); // TODO(ppacher): fix the return error type.
}
};
let (mut client, _) = ClientBuilder::from_uri(parsed).connect().await?;
let (tx, mut dispatch) = channel::<Command>(64);
tauri::async_runtime::spawn(async move {
let subscribers: SubscriberMap = RwLock::new(HashMap::new());
let next_id = AtomicUsize::new(0);
loop {
tokio::select! {
msg = client.next() => {
let msg = match msg {
Some(msg) => msg,
None => {
warn!("websocket connection lost");
dispatch.close();
return;
}
};
match msg {
Err(err) => {
error!("failed to receive frame from websocket: {}", err);
dispatch.close();
return;
},
Ok(msg) => {
let text = unsafe {
std::str::from_utf8_unchecked(msg.as_payload())
};
match text.parse::<Message>() {
Ok(msg) => {
let id = msg.id;
let map = subscribers
.read()
.await;
if let Some(sub) = map.get(&id) {
let res: Result<Response, MessageError> = msg.try_into();
match res {
Ok(response) => {
if let Err(err) = sub.send(response).await {
// The receiver side has been closed already,
// drop the read lock and remove the subscriber
// from our hashmap
drop(map);
subscribers
.write()
.await
.remove(&id);
debug!("subscriber for command {} closed read side: {}", id, err);
}
},
Err(err) => {
error!("invalid command: {}", err);
}
}
}
},
Err(err) => {
error!("failed to deserialize message: {}", err)
}
}
}
}
},
Some(mut cmd) = dispatch.recv() => {
let id = next_id.fetch_add(1, Ordering::Relaxed);
cmd.msg.id = id;
let blob: String = cmd.msg.into();
debug!("Sending websocket frame: {}", blob);
match client.send(tokio_websockets::Message::text(blob)).await {
Ok(_) => {
subscribers
.write()
.await
.insert(id, cmd.response);
},
Err(err) => {
error!("failed to dispatch command: {}", err);
// TODO(ppacher): we should send some error to cmd.response here.
// Otherwise, the sender of cmd might get stuck waiting for responses
// if they don't check for PortAPI.is_closed().
return
}
}
}
}
}
});
Ok(PortAPI { dispatch: tx })
}
impl PortAPI {
/// `request` sends a PortAPI `portapi::types::Request` to the server and returns a mpsc receiver channel
/// where all server responses are forwarded.
///
/// If the caller does not intend to read any responses the returned receiver may be closed or
/// dropped. As soon as the async-thread launched in `connect` detects a closed receiver it is remove
/// from the subscription map.
///
/// The default buffer size for the channel is 64. Use `request_with_buffer_size` to specify a dedicated buffer size.
pub async fn request(
&self,
r: Request,
) -> std::result::Result<Receiver<Response>, MessageError> {
self.request_with_buffer_size(r, 64).await
}
// Like `request` but supports explicitly specifying a channel buffer size.
pub async fn request_with_buffer_size(
&self,
r: Request,
buffer: usize,
) -> std::result::Result<Receiver<Response>, MessageError> {
let (tx, rx) = channel(buffer);
let msg: Message = r.try_into()?;
let _ = self.dispatch.send(Command { response: tx, msg }).await;
Ok(rx)
}
/// Reports whether or not the websocket connection to the Portmaster Database API has been closed
/// due to errors.
///
/// Users are expected to check this field on a regular interval to detect any issues and perform
/// a clean re-connect by calling `connect` again.
pub fn is_closed(&self) -> bool {
self.dispatch.is_closed()
}
}

View File

@@ -0,0 +1,258 @@
use thiserror::Error;
/// MessageError describes any error that is encountered when parsing
/// PortAPI messages or when converting between the Request/Response types.
#[derive(Debug, Error)]
pub enum MessageError {
#[error("missing command id")]
MissingID,
#[error("invalid command id")]
InvalidID,
#[error("missing command")]
MissingCommand,
#[error("missing key")]
MissingKey,
#[error("missing payload")]
MissingPayload,
#[error("unknown or unsupported command: {0}")]
UnknownCommand(String),
#[error(transparent)]
InvalidPayload(#[from] serde_json::Error),
}
/// Payload defines the payload type and content of a PortAPI message.
///
/// For the time being, only JSON payloads (indicated by a prefixed 'J' of the payload content)
/// is directly supported in `Payload::parse()`.
///
/// For other payload types (like CBOR, BSON, ...) it's the user responsibility to figure out
/// appropriate decoding from the `Payload::UNKNOWN` variant.
#[derive(PartialEq, Debug, Clone)]
pub enum Payload {
JSON(String),
UNKNOWN(String),
}
/// ParseError is returned from `Payload::parse()`.
#[derive(Debug, Error)]
pub enum ParseError {
#[error(transparent)]
JSON(#[from] serde_json::Error),
#[error("unknown error while parsing")]
UNKNOWN
}
impl Payload {
/// Parse the payload into T.
///
/// Only JSON parsing is supported for now. See [Payload] for more information.
pub fn parse<'a, T>(self: &'a Self) -> std::result::Result<T, ParseError>
where
T: serde::de::Deserialize<'a> {
match self {
Payload::JSON(blob) => Ok(serde_json::from_str::<T>(blob.as_str())?),
Payload::UNKNOWN(_) => Err(ParseError::UNKNOWN),
}
}
}
/// Supports creating a Payload instance from a String.
///
/// See [Payload] for more information.
impl std::convert::From<String> for Payload {
fn from(value: String) -> Payload {
let mut chars = value.chars();
let first = chars.next();
let rest = chars.as_str().to_string();
match first {
Some(c) => match c {
'J' => Payload::JSON(rest),
_ => Payload::UNKNOWN(value),
},
None => Payload::UNKNOWN("".to_string())
}
}
}
/// Display implementation for Payload that just displays the raw payload.
impl std::fmt::Display for Payload {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Payload::JSON(payload) => {
write!(f, "J{}", payload)
},
Payload::UNKNOWN(payload) => {
write!(f, "{}", payload)
}
}
}
}
/// Message is an internal representation of a PortAPI message.
/// Users should more likely use `portapi::types::Request` and `portapi::types::Response`
/// instead of directly using `Message`.
///
/// The struct is still public since it might be useful for debugging or to implement new
/// commands not yet supported by the `portapi::types` crate.
#[derive(PartialEq, Debug, Clone)]
pub struct Message {
pub id: usize,
pub cmd: String,
pub key: Option<String>,
pub payload: Option<Payload>,
}
/// Implementation to marshal a PortAPI message into it's wire-format representation
/// (which is a string).
///
/// Note that this conversion does not check for invalid messages!
impl std::convert::From<Message> for String {
fn from(value: Message) -> Self {
let mut result = "".to_owned();
result.push_str(value.id.to_string().as_str());
result.push_str("|");
result.push_str(&value.cmd);
if let Some(key) = value.key {
result.push_str("|");
result.push_str(key.as_str());
}
if let Some(payload) = value.payload {
result.push_str("|");
result.push_str(payload.to_string().as_str())
}
result
}
}
/// An implementation for `String::parse()` to convert a wire-format representation
/// of a PortAPI message to a Message instance.
///
/// Any errors returned from `String::parse()` will be of type `MessageError`
impl std::str::FromStr for Message {
type Err = MessageError;
fn from_str(line: &str) -> Result<Self, Self::Err> {
let parts = line.split("|").collect::<Vec<&str>>();
let id = match parts.get(0) {
Some(s) => match (*s).parse::<usize>() {
Ok(id) => Ok(id),
Err(_) => Err(MessageError::InvalidID),
},
None => Err(MessageError::MissingID),
}?;
let cmd = match parts.get(1) {
Some(s) => Ok(*s),
None => Err(MessageError::MissingCommand),
}?
.to_string();
let key = parts.get(2)
.and_then(|key| Some(key.to_string()));
let payload : Option<Payload> = parts.get(3)
.and_then(|p| Some(p.to_string().into()));
return Ok(Message {
id,
cmd,
key,
payload: payload
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Deserialize;
#[derive(Debug, PartialEq, Deserialize)]
struct Test {
a: i64,
s: String,
}
#[test]
fn payload_to_string() {
let p = Payload::JSON("{}".to_string());
assert_eq!(p.to_string(), "J{}");
let p = Payload::UNKNOWN("some unknown content".to_string());
assert_eq!(p.to_string(), "some unknown content");
}
#[test]
fn payload_from_string() {
let p: Payload = "J{}".to_string().into();
assert_eq!(p, Payload::JSON("{}".to_string()));
let p: Payload = "some unknown content".to_string().into();
assert_eq!(p, Payload::UNKNOWN("some unknown content".to_string()));
}
#[test]
fn payload_parse() {
let p: Payload = "J{\"a\": 100, \"s\": \"string\"}".to_string().into();
let t: Test = p.parse()
.expect("Expected payload parsing to work");
assert_eq!(t, Test{
a: 100,
s: "string".to_string(),
});
}
#[test]
fn parse_message() {
let m = "10|insert|some:key|J{}".parse::<Message>()
.expect("Expected message to parse");
assert_eq!(m, Message{
id: 10,
cmd: "insert".to_string(),
key: Some("some:key".to_string()),
payload: Some(Payload::JSON("{}".to_string())),
});
let m = "1|done".parse::<Message>()
.expect("Expected message to parse");
assert_eq!(m, Message{
id: 1,
cmd: "done".to_string(),
key: None,
payload: None
});
let m = "".parse::<Message>()
.expect_err("Expected parsing to fail");
if let MessageError::InvalidID = m {} else {
panic!("unexpected error value: {}", m)
}
let m = "1".parse::<Message>()
.expect_err("Expected parsing to fail");
if let MessageError::MissingCommand = m {} else {
panic!("unexpected error value: {}", m)
}
}
}

View File

@@ -0,0 +1,4 @@
pub mod client;
pub mod message;
pub mod types;
pub mod models;

View File

@@ -0,0 +1,18 @@
use serde::*;
use super::super::message::Payload;
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct BooleanValue {
#[serde(rename = "Value")]
pub value: Option<bool>,
}
impl TryInto<Payload> for BooleanValue {
type Error = serde_json::Error;
fn try_into(self) -> Result<Payload, Self::Error> {
let str = serde_json::to_string(&self)?;
Ok(Payload::JSON(str))
}
}

View File

@@ -0,0 +1,4 @@
pub mod config;
pub mod spn;
pub mod notification;
pub mod subsystem;

View File

@@ -0,0 +1,70 @@
use serde::*;
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct Notification {
#[serde(rename = "EventID")]
pub event_id: String,
#[serde(rename = "GUID")]
pub guid: String,
#[serde(rename = "Type")]
pub notification_type: NotificationType,
#[serde(rename = "Message")]
pub message: String,
#[serde(rename = "Title")]
pub title: String,
#[serde(rename = "Category")]
pub category: String,
#[serde(rename = "EventData")]
pub data: serde_json::Value,
#[serde(rename = "Expires")]
pub expires: u64,
#[serde(rename = "State")]
pub state: String,
#[serde(rename = "AvailableActions")]
pub actions: Vec<Action>,
#[serde(rename = "SelectedActionID")]
pub selected_action_id: String,
#[serde(rename = "ShowOnSystem")]
pub show_on_system: bool,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct Action {
#[serde(rename = "ID")]
pub id: String,
#[serde(rename = "Text")]
pub text: String,
#[serde(rename = "Type")]
pub action_type: String,
#[serde(rename = "Payload")]
pub payload: serde_json::Value,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct NotificationType(i32);
#[allow(dead_code)]
pub const INFO: NotificationType = NotificationType(0);
#[allow(dead_code)]
pub const WARN: NotificationType = NotificationType(1);
#[allow(dead_code)]
pub const PROMPT: NotificationType = NotificationType(2);
#[allow(dead_code)]
pub const ERROR: NotificationType = NotificationType(3);

View File

@@ -0,0 +1,8 @@
use serde::*;
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct SPNStatus {
#[serde(rename = "Status")]
pub status: String,
}

View File

@@ -0,0 +1,45 @@
#![allow(dead_code)]
use serde::*;
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct ModuleStatus {
#[serde(rename = "Name")]
pub name: String,
#[serde(rename = "Enabled")]
pub enabled: bool,
#[serde(rename = "Status")]
pub status: u8,
#[serde(rename = "FailureStatus")]
pub failure_status: u8,
#[serde(rename = "FailureID")]
pub failure_id: String,
#[serde(rename = "FailureMsg")]
pub failure_msg: String,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct Subsystem {
#[serde(rename = "ID")]
pub id: String,
#[serde(rename = "Name")]
pub name: String,
#[serde(rename = "Description")]
pub description: String,
#[serde(rename = "Modules")]
pub module_status: Vec<ModuleStatus>,
#[serde(rename = "FailureStatus")]
pub failure_status: u8,
}
pub const FAILURE_NONE: u8 = 0;
pub const FAILURE_HINT: u8 = 1;
pub const FAILURE_WARNING: u8 = 2;
pub const FAILURE_ERROR: u8 = 3;

View File

@@ -0,0 +1,199 @@
use super::message::*;
/// Request is a strongly typed request message
/// that can be converted to a `portapi::message::Message`
/// object for further use by the client (`portapi::client::PortAPI`).
#[derive(PartialEq, Debug)]
pub enum Request {
Get(String),
Query(String),
Subscribe(String),
QuerySubscribe(String),
Create(String, Payload),
Update(String, Payload),
Insert(String, Payload),
Delete(String),
Cancel,
}
/// Implementation to convert a internal `portapi::message::Message` to a valid
/// `Request` variant.
///
/// Any error returned will be of type `portapi::message::MessageError`.
impl std::convert::TryFrom<Message> for Request {
type Error = MessageError;
fn try_from(value: Message) -> Result<Self, Self::Error> {
match value.cmd.as_str() {
"get" => {
let key = value.key.ok_or(MessageError::MissingKey)?;
Ok(Request::Get(key))
},
"query" => {
let key = value.key.ok_or(MessageError::MissingKey)?;
Ok(Request::Query(key))
},
"sub" => {
let key = value.key.ok_or(MessageError::MissingKey)?;
Ok(Request::Subscribe(key))
},
"qsub" => {
let key = value.key.ok_or(MessageError::MissingKey)?;
Ok(Request::QuerySubscribe(key))
},
"create" => {
let key = value.key.ok_or(MessageError::MissingKey)?;
let payload = value.payload.ok_or(MessageError::MissingPayload)?;
Ok(Request::Create(key, payload))
},
"update" => {
let key = value.key.ok_or(MessageError::MissingKey)?;
let payload = value.payload.ok_or(MessageError::MissingPayload)?;
Ok(Request::Update(key, payload))
},
"insert" => {
let key = value.key.ok_or(MessageError::MissingKey)?;
let payload = value.payload.ok_or(MessageError::MissingPayload)?;
Ok(Request::Insert(key, payload))
},
"delete" => {
let key = value.key.ok_or(MessageError::MissingKey)?;
Ok(Request::Delete(key))
},
"cancel" => {
Ok(Request::Cancel)
},
cmd => {
Err(MessageError::UnknownCommand(cmd.to_string()))
}
}
}
}
/// An implementation to try to convert a `Request` variant into a valid
/// `portapi::message::Message` struct.
///
/// While this implementation does not yet return any errors, it's expected that
/// additional validation will be added in the future so users should already expect
/// to receive `portapi::message::MessageError`s.
impl std::convert::TryFrom<Request> for Message {
type Error = MessageError;
fn try_from(value: Request) -> Result<Self, Self::Error> {
match value {
Request::Get(key) => Ok(Message { id: 0, cmd: "get".to_string(), key: Some(key), payload: None }),
Request::Query(key) => Ok(Message { id: 0, cmd: "query".to_string(), key: Some(key), payload: None }),
Request::Subscribe(key) => Ok(Message { id: 0, cmd: "sub".to_string(), key: Some(key), payload: None }),
Request::QuerySubscribe(key) => Ok(Message { id: 0, cmd: "qsub".to_string(), key: Some(key), payload: None }),
Request::Create(key, value) => Ok(Message{ id: 0, cmd: "create".to_string(), key: Some(key), payload: Some(value)}),
Request::Update(key, value) => Ok(Message{ id: 0, cmd: "update".to_string(), key: Some(key), payload: Some(value)}),
Request::Insert(key, value) => Ok(Message{ id: 0, cmd: "insert".to_string(), key: Some(key), payload: Some(value)}),
Request::Delete(key) => Ok(Message { id: 0, cmd: "delete".to_string(), key: Some(key), payload: None }),
Request::Cancel => Ok(Message { id: 0, cmd: "cancel".to_string(), key: None, payload: None }),
}
}
}
/// Response is strongly types PortAPI response message.
/// that can be converted to a `portapi::message::Message`
/// object for further use by the client (`portapi::client::PortAPI`).
#[derive(PartialEq, Debug)]
pub enum Response {
Ok(String, Payload),
Update(String, Payload),
New(String, Payload),
Delete(String),
Success,
Error(String),
Warning(String),
Done
}
/// Implementation to convert a internal `portapi::message::Message` to a valid
/// `Response` variant.
///
/// Any error returned will be of type `portapi::message::MessageError`.
impl std::convert::TryFrom<Message> for Response {
type Error = MessageError;
fn try_from(value: Message) -> Result<Self, MessageError> {
match value.cmd.as_str() {
"ok" => {
let key = value.key.ok_or(MessageError::MissingKey)?;
let payload = value.payload.ok_or(MessageError::MissingPayload)?;
Ok(Response::Ok(key, payload))
},
"upd" => {
let key = value.key.ok_or(MessageError::MissingKey)?;
let payload = value.payload.ok_or(MessageError::MissingPayload)?;
Ok(Response::Update(key, payload))
},
"new" => {
let key = value.key.ok_or(MessageError::MissingKey)?;
let payload = value.payload.ok_or(MessageError::MissingPayload)?;
Ok(Response::New(key, payload))
},
"del" => {
let key = value.key.ok_or(MessageError::MissingKey)?;
Ok(Response::Delete(key))
},
"success" => {
Ok(Response::Success)
},
"error" => {
let key = value.key.ok_or(MessageError::MissingKey)?;
Ok(Response::Error(key))
},
"warning" => {
let key = value.key.ok_or(MessageError::MissingKey)?;
Ok(Response::Warning(key))
},
"done" => {
Ok(Response::Done)
},
cmd => Err(MessageError::UnknownCommand(cmd.to_string()))
}
}
}
/// An implementation to try to convert a `Response` variant into a valid
/// `portapi::message::Message` struct.
///
/// While this implementation does not yet return any errors, it's expected that
/// additional validation will be added in the future so users should already expect
/// to receive `portapi::message::MessageError`s.
impl std::convert::TryFrom<Response> for Message {
type Error = MessageError;
fn try_from(value: Response) -> Result<Self, Self::Error> {
match value {
Response::Ok(key, payload) => Ok(Message{id: 0, cmd: "ok".to_string(), key: Some(key), payload: Some(payload)}),
Response::Update(key, payload) => Ok(Message{id: 0, cmd: "upd".to_string(), key: Some(key), payload: Some(payload)}),
Response::New(key, payload) => Ok(Message{id: 0, cmd: "new".to_string(), key: Some(key), payload: Some(payload)}),
Response::Delete(key ) => Ok(Message{id: 0, cmd: "del".to_string(), key: Some(key), payload: None}),
Response::Success => Ok(Message{id: 0, cmd: "success".to_string(), key: None, payload: None}),
Response::Warning(key) => Ok(Message{id: 0, cmd: "warning".to_string(), key: Some(key), payload: None}),
Response::Error(key) => Ok(Message{id: 0, cmd: "error".to_string(), key: Some(key), payload: None}),
Response::Done => Ok(Message{id: 0, cmd: "done".to_string(), key: None, payload: None}),
}
}
}
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub struct Record {
pub created: u64,
pub deleted: u64,
pub expires: u64,
pub modified: u64,
pub key: String,
}