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
Dustin 2022-01-09 12:58:45 -06:00
parent 7806b67531
commit 96ede2a407
14 changed files with 123 additions and 20 deletions

3
backend/Cargo.lock generated
View File

@ -807,9 +807,12 @@ dependencies = [
"futures 0.3.19", "futures 0.3.19",
"jsonrpc-core", "jsonrpc-core",
"jsonrpc-pubsub", "jsonrpc-pubsub",
"jsonrpc-server-utils",
"log", "log",
"parity-tokio-ipc",
"serde", "serde",
"serde_json", "serde_json",
"tokio",
"url", "url",
] ]

View File

@ -10,9 +10,12 @@ figment = "^0.10"
jsonrpc-core = "~18.0" jsonrpc-core = "~18.0"
jsonrpc-derive = "~18.0" jsonrpc-derive = "~18.0"
jsonrpc-ipc-server = "~18.0" jsonrpc-ipc-server = "~18.0"
jsonrpc-core-client = "~18.0"
serde = "^1.0" serde = "^1.0"
[dependencies.jsonrpc-core-client]
version = "~18.0"
features = ["ipc"]
[dependencies.procfs] [dependencies.procfs]
version = "^0.12" version = "^0.12"
features = ["chrono"] features = ["chrono"]

View File

@ -1,4 +1,5 @@
use crate::rpc::{Status, WeywotRpc}; use crate::models::status::DaemonStatus;
use crate::rpc::WeywotRpc;
use chrono::Local; use chrono::Local;
use jsonrpc_core::Result as JsonRpcResult; use jsonrpc_core::Result as JsonRpcResult;
use procfs::process::Process; use procfs::process::Process;
@ -9,8 +10,8 @@ use std::process;
pub struct RpcDaemon; pub struct RpcDaemon;
impl WeywotRpc for RpcDaemon { impl WeywotRpc for RpcDaemon {
fn status(&self) -> JsonRpcResult<Status> { fn status(&self) -> JsonRpcResult<DaemonStatus> {
Ok(Status { Ok(DaemonStatus {
version: env!("CARGO_PKG_VERSION").into(), version: env!("CARGO_PKG_VERSION").into(),
runtime: proc_runtime().unwrap_or(0), runtime: proc_runtime().unwrap_or(0),
}) })

View File

@ -1,4 +1,5 @@
mod daemon; mod daemon;
mod models;
mod rpc; mod rpc;
mod server; mod server;

View File

@ -0,0 +1 @@
pub mod status;

View File

@ -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,
}

View File

@ -1,15 +1,9 @@
use crate::models::status::DaemonStatus;
use jsonrpc_core::Result; use jsonrpc_core::Result;
use jsonrpc_derive::rpc; use jsonrpc_derive::rpc;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
pub struct Status {
pub version: String,
pub runtime: i64,
}
#[rpc] #[rpc]
pub trait WeywotRpc { pub trait WeywotRpc {
#[rpc(name = "status")] #[rpc(name = "status")]
fn status(&self) -> Result<Status>; fn status(&self) -> Result<DaemonStatus>;
} }

View File

@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};
pub struct Config { pub struct Config {
pub address: String, pub address: String,
pub port: u16, pub port: u16,
pub socket: String
} }
impl Default for Config { impl Default for Config {
@ -11,6 +12,7 @@ impl Default for Config {
Config { Config {
address: "::".into(), address: "::".into(),
port: 8998, port: 8998,
socket: "weywot.sock".into(),
} }
} }
} }

View File

@ -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<P: Into<PathBuf>>(socket: P) -> Self {
Self {
socket_path: socket.into(),
}
}
pub async fn client(&self) -> Result<Client, ApiError> {
match connect(&self.socket_path).await {
Ok(client) => Ok(client),
Err(e) => Err((
HttpStatus::ServiceUnavailable,
Json(ErrorResponse::new(format!(
"Cannot connect to daemon: {}",
e
))),
)),
}
}
}

View File

@ -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<ErrorResponse>);
impl ErrorResponse {
pub fn new<S: Into<String>>(message: S) -> Self {
Self {
error: message.into(),
}
}
}

View File

@ -1,8 +1,11 @@
mod config; mod config;
mod context;
mod error;
mod routes; mod routes;
use config::Config; use config::Config;
use argh::FromArgs; use argh::FromArgs;
use context::Context;
use rocket; use rocket;
use rocket::fairing::AdHoc; use rocket::fairing::AdHoc;
use rocket::figment::providers::{Env, Format, Serialized, Toml}; use rocket::figment::providers::{Env, Format, Serialized, Toml};
@ -24,6 +27,10 @@ struct Arguments {
/// configuration file /// configuration file
#[argh(option, short = 'c')] #[argh(option, short = 'c')]
config: Option<String>, config: Option<String>,
/// weywot daemon socket path
#[argh(option, short = 's')]
socket: Option<String>,
} }
#[rocket::main] #[rocket::main]
@ -42,10 +49,22 @@ pub async fn main() -> Result<(), rocket::Error> {
if let Some(port) = args.port { if let Some(port) = args.port {
figment = figment.merge(("port", 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) rocket::custom(figment)
.mount("/", rocket::routes![routes::hello::hello]) .mount("/", rocket::routes![routes::status::get_status])
.attach(AdHoc::config::<Config>()) .attach(AdHoc::config::<Config>())
.manage(context)
.ignite() .ignite()
.await? .await?
.launch() .launch()

View File

@ -1,6 +0,0 @@
use rocket;
#[rocket::get("/")]
pub async fn hello() -> &'static str {
"Hello, world!"
}

View File

@ -1 +1 @@
pub mod hello; pub mod status;

View File

@ -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<Context>,
) -> Result<Json<StatusResponse>, ApiError> {
let client = state.client().await?;
Ok(Json(StatusResponse {
daemon: client.status().await.unwrap(),
web: WebServerStatus {
version: env!("CARGO_PKG_VERSION").into(),
},
}))
}