Compare commits

..

2 Commits

Author SHA1 Message Date
Dustin 96ede2a407 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.
2022-01-09 14:58:59 -06:00
Dustin 7806b67531 backend: daemon: Begin JSON-RPC implementation
Beginning the implementation of the JSON-RPC server in the privileged
daemon.  We're using *jsonrpc-core* for the JSON-RPC implementation,
which includes serialization, connection handling, and method dispatch.

The first RPC method is a simple status query, which returns the daemon
version and the number of seconds the daemon process has been running.
2022-01-09 12:49:19 -06:00
8 changed files with 70 additions and 41 deletions

View File

@ -22,4 +22,4 @@ features = ["chrono"]
[dependencies.rocket]
version = "^0.5.0-rc.1"
features = ["json", "secrets", "tls"]
features = ["json", "secrets", "tls"]

View File

@ -1,36 +1,27 @@
use crate::models::status::Status;
use crate::models::status::DaemonStatus;
use crate::rpc::WeywotRpc;
use chrono::{DateTime, Local};
use chrono::Local;
use jsonrpc_core::Result as JsonRpcResult;
use procfs::process::Process;
use std::convert::TryInto;
use std::num::TryFromIntError;
use std::error::Error;
use std::process;
pub struct RpcDaemon;
impl WeywotRpc for RpcDaemon {
fn status(&self) -> JsonRpcResult<Status> {
let pid: Result<i32, TryFromIntError> = process::id().try_into();
let runtime: i64 = if let Ok(pid) = pid {
if let Ok(proc) = Process::new(pid) {
let now: DateTime<Local> = Local::now();
if let Ok(starttime) = proc.stat.starttime() {
let etime = now - starttime;
etime.num_seconds()
} else {
0
}
} else {
0
}
} else {
0
};
Ok(Status {
fn status(&self) -> JsonRpcResult<DaemonStatus> {
Ok(DaemonStatus {
version: env!("CARGO_PKG_VERSION").into(),
runtime: runtime,
runtime: proc_runtime().unwrap_or(0),
})
}
}
fn proc_runtime() -> Result<i64, Box<dyn Error>> {
let pid: i32 = process::id().try_into()?;
let proc = Process::new(pid)?;
let starttime = proc.stat.starttime()?;
let now = Local::now();
Ok((now - starttime).num_seconds())
}

View File

@ -1,7 +1,18 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
pub struct Status {
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,9 +1,9 @@
use crate::models::status::Status;
use crate::models::status::DaemonStatus;
use jsonrpc_core::Result;
use jsonrpc_derive::rpc;
#[rpc]
pub trait WeywotRpc {
#[rpc(name = "status")]
fn status(&self) -> Result<Status>;
fn status(&self) -> Result<DaemonStatus>;
}

View File

@ -1,5 +1,8 @@
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 {
@ -13,13 +16,16 @@ impl Context {
}
}
pub async fn client(&self) -> Option<Client> {
pub async fn client(&self) -> Result<Client, ApiError> {
match connect(&self.socket_path).await {
Ok(client) => Some(client),
Err(e) => {
eprintln!("{}", e);
None
}
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,5 +1,6 @@
mod config;
mod context;
mod error;
mod routes;
use config::Config;

View File

@ -1,16 +1,18 @@
use crate::models::status::Status;
use crate::models::status::{StatusResponse, WebServerStatus};
use crate::server::context::Context;
use rocket::http::Status as HttpStatus;
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<Status>, HttpStatus> {
if let Some(client) = state.client().await {
Ok(Json(client.status().await.unwrap()))
} else {
Err(HttpStatus::ServiceUnavailable)
}
) -> 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(),
},
}))
}