From 96ede2a407645e8d5bf910ddcd083499838ca6a5 Mon Sep 17 00:00:00 2001 From: "Dustin C. Hatch" Date: Sun, 9 Jan 2022 12:58:45 -0600 Subject: [PATCH] backend: web: Begin JSON-RPC client impl Starting work on the JSON-RPC client in the web server. The `Context` structure, which is stored in Rocket's managed state and available to route functions through the `State` request guard, provides a method to get an RPC client. Each request that needs to communicate with the daemon will have its own RPC connection. This ensures that a valid connection is always available, even if the daemon has restarted between web requests. I had considered storing the connection in the context, testing it each time it was needed, and reconnecting if the connection was broken. This proved very difficult, since the context is passed to request handlers as an immutable reference. Mutating its state would require locking, and I could not make that work easily. Besides, the overhead of "pinging" the server for every request is probably greater than just reconnecting every time, so it would have been a waste. The *GET /status* operation returns a document that indicates the status of the daemon and the web server. --- backend/Cargo.lock | 3 +++ backend/Cargo.toml | 5 ++++- backend/src/daemon/rpc.rs | 7 ++++--- backend/src/main.rs | 1 + backend/src/models/mod.rs | 1 + backend/src/models/status.rs | 18 +++++++++++++++++ backend/src/rpc.rs | 10 ++-------- backend/src/server/config.rs | 2 ++ backend/src/server/context.rs | 31 +++++++++++++++++++++++++++++ backend/src/server/error.rs | 18 +++++++++++++++++ backend/src/server/mod.rs | 21 ++++++++++++++++++- backend/src/server/routes/hello.rs | 6 ------ backend/src/server/routes/mod.rs | 2 +- backend/src/server/routes/status.rs | 18 +++++++++++++++++ 14 files changed, 123 insertions(+), 20 deletions(-) create mode 100644 backend/src/models/mod.rs create mode 100644 backend/src/models/status.rs create mode 100644 backend/src/server/context.rs create mode 100644 backend/src/server/error.rs delete mode 100644 backend/src/server/routes/hello.rs diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 80323f7..11a1466 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -807,9 +807,12 @@ dependencies = [ "futures 0.3.19", "jsonrpc-core", "jsonrpc-pubsub", + "jsonrpc-server-utils", "log", + "parity-tokio-ipc", "serde", "serde_json", + "tokio", "url", ] diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 1a85e52..5058485 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -10,9 +10,12 @@ figment = "^0.10" jsonrpc-core = "~18.0" jsonrpc-derive = "~18.0" jsonrpc-ipc-server = "~18.0" -jsonrpc-core-client = "~18.0" serde = "^1.0" +[dependencies.jsonrpc-core-client] +version = "~18.0" +features = ["ipc"] + [dependencies.procfs] version = "^0.12" features = ["chrono"] diff --git a/backend/src/daemon/rpc.rs b/backend/src/daemon/rpc.rs index 1023071..0bae1ec 100644 --- a/backend/src/daemon/rpc.rs +++ b/backend/src/daemon/rpc.rs @@ -1,4 +1,5 @@ -use crate::rpc::{Status, WeywotRpc}; +use crate::models::status::DaemonStatus; +use crate::rpc::WeywotRpc; use chrono::Local; use jsonrpc_core::Result as JsonRpcResult; use procfs::process::Process; @@ -9,8 +10,8 @@ use std::process; pub struct RpcDaemon; impl WeywotRpc for RpcDaemon { - fn status(&self) -> JsonRpcResult { - Ok(Status { + fn status(&self) -> JsonRpcResult { + Ok(DaemonStatus { version: env!("CARGO_PKG_VERSION").into(), runtime: proc_runtime().unwrap_or(0), }) diff --git a/backend/src/main.rs b/backend/src/main.rs index e008ab6..cf0a755 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,4 +1,5 @@ mod daemon; +mod models; mod rpc; mod server; diff --git a/backend/src/models/mod.rs b/backend/src/models/mod.rs new file mode 100644 index 0000000..16a1865 --- /dev/null +++ b/backend/src/models/mod.rs @@ -0,0 +1 @@ +pub mod status; \ No newline at end of file diff --git a/backend/src/models/status.rs b/backend/src/models/status.rs new file mode 100644 index 0000000..3d1e94d --- /dev/null +++ b/backend/src/models/status.rs @@ -0,0 +1,18 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct DaemonStatus { + pub version: String, + pub runtime: i64, +} + +#[derive(Debug, Serialize)] +pub struct WebServerStatus { + pub version: String, +} + +#[derive(Debug, Serialize)] +pub struct StatusResponse { + pub daemon: DaemonStatus, + pub web: WebServerStatus, +} diff --git a/backend/src/rpc.rs b/backend/src/rpc.rs index 627e984..bf1309e 100644 --- a/backend/src/rpc.rs +++ b/backend/src/rpc.rs @@ -1,15 +1,9 @@ +use crate::models::status::DaemonStatus; use jsonrpc_core::Result; use jsonrpc_derive::rpc; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Deserialize, Serialize)] -pub struct Status { - pub version: String, - pub runtime: i64, -} #[rpc] pub trait WeywotRpc { #[rpc(name = "status")] - fn status(&self) -> Result; + fn status(&self) -> Result; } diff --git a/backend/src/server/config.rs b/backend/src/server/config.rs index 57854ca..d9f601e 100644 --- a/backend/src/server/config.rs +++ b/backend/src/server/config.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; pub struct Config { pub address: String, pub port: u16, + pub socket: String } impl Default for Config { @@ -11,6 +12,7 @@ impl Default for Config { Config { address: "::".into(), port: 8998, + socket: "weywot.sock".into(), } } } diff --git a/backend/src/server/context.rs b/backend/src/server/context.rs new file mode 100644 index 0000000..8015a55 --- /dev/null +++ b/backend/src/server/context.rs @@ -0,0 +1,31 @@ +use crate::rpc::gen_client::Client; +use crate::server::error::{ApiError, ErrorResponse}; +use jsonrpc_core_client::transports::ipc::connect; +use rocket::http::Status as HttpStatus; +use rocket::serde::json::Json; +use std::path::PathBuf; + +pub struct Context { + socket_path: PathBuf, +} + +impl Context { + pub fn new>(socket: P) -> Self { + Self { + socket_path: socket.into(), + } + } + + pub async fn client(&self) -> Result { + match connect(&self.socket_path).await { + Ok(client) => Ok(client), + Err(e) => Err(( + HttpStatus::ServiceUnavailable, + Json(ErrorResponse::new(format!( + "Cannot connect to daemon: {}", + e + ))), + )), + } + } +} diff --git a/backend/src/server/error.rs b/backend/src/server/error.rs new file mode 100644 index 0000000..d974d45 --- /dev/null +++ b/backend/src/server/error.rs @@ -0,0 +1,18 @@ +use rocket::http::Status as HttpStatus; +use rocket::serde::json::Json; +use rocket::serde::Serialize; + +#[derive(Serialize)] +pub struct ErrorResponse { + pub error: String, +} + +pub type ApiError = (HttpStatus, Json); + +impl ErrorResponse { + pub fn new>(message: S) -> Self { + Self { + error: message.into(), + } + } +} diff --git a/backend/src/server/mod.rs b/backend/src/server/mod.rs index e4aa81b..d947b72 100644 --- a/backend/src/server/mod.rs +++ b/backend/src/server/mod.rs @@ -1,8 +1,11 @@ mod config; +mod context; +mod error; mod routes; use config::Config; use argh::FromArgs; +use context::Context; use rocket; use rocket::fairing::AdHoc; use rocket::figment::providers::{Env, Format, Serialized, Toml}; @@ -24,6 +27,10 @@ struct Arguments { /// configuration file #[argh(option, short = 'c')] config: Option, + + /// weywot daemon socket path + #[argh(option, short = 's')] + socket: Option, } #[rocket::main] @@ -42,10 +49,22 @@ pub async fn main() -> Result<(), rocket::Error> { if let Some(port) = args.port { figment = figment.merge(("port", port)); } + if let Some(socket) = args.socket { + figment = figment.merge(("socket", socket)); + } + + let context = Context::new( + figment + .find_value("socket") + .expect("No daemon socket path configured") + .as_str() + .expect("Invalid daemon socket path"), + ); rocket::custom(figment) - .mount("/", rocket::routes![routes::hello::hello]) + .mount("/", rocket::routes![routes::status::get_status]) .attach(AdHoc::config::()) + .manage(context) .ignite() .await? .launch() diff --git a/backend/src/server/routes/hello.rs b/backend/src/server/routes/hello.rs deleted file mode 100644 index ca8411c..0000000 --- a/backend/src/server/routes/hello.rs +++ /dev/null @@ -1,6 +0,0 @@ -use rocket; - -#[rocket::get("/")] -pub async fn hello() -> &'static str { - "Hello, world!" -} diff --git a/backend/src/server/routes/mod.rs b/backend/src/server/routes/mod.rs index 152adf1..822c729 100644 --- a/backend/src/server/routes/mod.rs +++ b/backend/src/server/routes/mod.rs @@ -1 +1 @@ -pub mod hello; +pub mod status; diff --git a/backend/src/server/routes/status.rs b/backend/src/server/routes/status.rs index e69de29..4ef6e7f 100644 --- a/backend/src/server/routes/status.rs +++ b/backend/src/server/routes/status.rs @@ -0,0 +1,18 @@ +use crate::models::status::{StatusResponse, WebServerStatus}; +use crate::server::context::Context; +use crate::server::error::ApiError; +use rocket::serde::json::Json; +use rocket::State; + +#[rocket::get("/status")] +pub async fn get_status( + state: &State, +) -> Result, ApiError> { + let client = state.client().await?; + Ok(Json(StatusResponse { + daemon: client.status().await.unwrap(), + web: WebServerStatus { + version: env!("CARGO_PKG_VERSION").into(), + }, + })) +}