Page MenuHomePhorge

No OneTemporary

Size
30 KB
Referenced Files
None
Subscribers
None
diff --git a/src/config/structs.rs b/src/config/structs.rs
index 484db7a..c20ab31 100644
--- a/src/config/structs.rs
+++ b/src/config/structs.rs
@@ -1,62 +1,60 @@
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
-use utoipa::{schema, ToSchema};
#[derive(Debug, Deserialize, Serialize)]
pub struct Config {
pub runner: Runner,
pub daemon: Daemon,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Runner {
pub shell: String,
pub args: Vec<String>,
pub node: String,
pub log_path: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Daemon {
pub restarts: u64,
pub interval: u64,
pub kind: String,
pub web: Web,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Web {
pub ui: bool,
pub api: bool,
pub address: String,
pub port: u64,
pub secure: Secure,
pub path: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Secure {
pub enabled: bool,
pub token: String,
}
-#[derive(Debug, Deserialize, Serialize, ToSchema)]
+#[derive(Debug, Deserialize, Serialize)]
pub struct Servers {
- #[schema(example = json!({"example": {"address": "http://127.0.0.1:5630", "token": "test_token"}}))]
pub servers: Option<BTreeMap<String, Server>>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Server {
pub address: String,
pub token: Option<String>,
}
impl Server {
pub fn get(&self) -> Self {
Self {
token: self.token.clone(),
address: self.address.trim_end_matches('/').to_string(),
}
}
}
diff --git a/src/daemon/api/mod.rs b/src/daemon/api/mod.rs
index 625ede8..58852d7 100644
--- a/src/daemon/api/mod.rs
+++ b/src/daemon/api/mod.rs
@@ -1,205 +1,218 @@
mod fairing;
mod helpers;
mod routes;
mod structs;
use crate::webui::{self, assets::NamedFile};
use helpers::create_status;
use include_dir::{include_dir, Dir};
use lazy_static::lazy_static;
use macros_rs::fmtstr;
-use pmc::{config, config::structs::Servers, process};
+use pmc::{config, process};
use prometheus::{opts, register_counter, register_gauge, register_histogram, register_histogram_vec};
use prometheus::{Counter, Gauge, Histogram, HistogramVec};
use serde_json::{json, Value};
+use std::sync::atomic::{AtomicBool, Ordering};
use structs::{AuthMessage, ErrorMessage};
use utoipa_rapidoc::RapiDoc;
+use rocket::request::{self, FromRequest, Request};
+
use utoipa::{
openapi::security::{ApiKey, ApiKeyValue, SecurityScheme},
Modify, OpenApi,
};
use rocket::{
catch,
http::{ContentType, Status},
outcome::Outcome,
serde::json::Json,
};
lazy_static! {
pub static ref HTTP_COUNTER: Counter = register_counter!(opts!("http_requests_total", "Number of HTTP requests made.")).unwrap();
pub static ref DAEMON_START_TIME: Gauge = register_gauge!(opts!("process_start_time_seconds", "The uptime of the daemon.")).unwrap();
pub static ref DAEMON_MEM_USAGE: Histogram = register_histogram!("daemon_memory_usage", "The memory usage graph of the daemon.").unwrap();
pub static ref DAEMON_CPU_PERCENTAGE: Histogram = register_histogram!("daemon_cpu_percentage", "The cpu usage graph of the daemon.").unwrap();
pub static ref HTTP_REQ_HISTOGRAM: HistogramVec = register_histogram_vec!("http_request_duration_seconds", "The HTTP request latencies in seconds.", &["route"]).unwrap();
}
#[derive(OpenApi)]
#[openapi(
modifiers(&SecurityAddon),
paths(
routes::action_handler,
routes::env_handler,
routes::info_handler,
routes::dump_handler,
routes::servers_handler,
routes::config_handler,
routes::list_handler,
routes::logs_handler,
routes::logs_raw_handler,
routes::metrics_handler,
routes::prometheus_handler,
routes::create_handler,
routes::rename_handler
),
components(schemas(
- Servers,
AuthMessage,
ErrorMessage,
process::Log,
process::Raw,
process::Info,
process::Stats,
process::Watch,
process::ItemSingle,
process::ProcessItem,
routes::Stats,
routes::Server,
routes::Daemon,
+ routes::Servers,
routes::Version,
routes::ActionBody,
routes::ConfigBody,
routes::CreateBody,
routes::MetricsRoot,
routes::LogResponse,
routes::DocMemoryInfo,
routes::ActionResponse,
))
)]
struct ApiDoc;
struct Logger;
+struct EnableWebUI;
+
struct SecurityAddon;
struct TeraState {
path: String,
tera: tera::Tera,
}
impl Modify for SecurityAddon {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
let components = openapi.components.as_mut().unwrap();
components.add_security_scheme("api_key", SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("token"))))
}
}
#[catch(500)]
fn internal_error<'m>() -> Json<ErrorMessage> { create_status(Status::InternalServerError) }
#[catch(405)]
fn not_allowed<'m>() -> Json<ErrorMessage> { create_status(Status::MethodNotAllowed) }
#[catch(404)]
fn not_found<'m>() -> Json<ErrorMessage> { create_status(Status::NotFound) }
#[catch(401)]
fn unauthorized<'m>() -> Json<ErrorMessage> { create_status(Status::Unauthorized) }
#[rocket::async_trait]
-impl<'r> rocket::request::FromRequest<'r> for routes::Token {
+impl<'r> FromRequest<'r> for EnableWebUI {
+ type Error = ();
+
+ async fn from_request(_req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
+ let webui = IS_WEBUI.load(Ordering::Acquire);
+
+ if webui {
+ Outcome::Success(EnableWebUI)
+ } else {
+ Outcome::Error((Status::NotFound, ()))
+ }
+ }
+}
+
+#[rocket::async_trait]
+impl<'r> FromRequest<'r> for routes::Token {
type Error = ();
async fn from_request(request: &'r rocket::Request<'_>) -> rocket::request::Outcome<Self, Self::Error> {
let config = config::read().daemon.web;
if !config.secure.enabled {
return Outcome::Success(routes::Token);
}
if let Some(header_value) = request.headers().get_one("token") {
if header_value == config.secure.token {
return Outcome::Success(routes::Token);
}
}
Outcome::Error((Status::Unauthorized, ()))
}
}
+static IS_WEBUI: AtomicBool = AtomicBool::new(false);
+
pub async fn start(webui: bool) {
+ IS_WEBUI.store(webui, Ordering::Release);
+
let tera = webui::create_templates();
let s_path = config::read().get_path().trim_end_matches('/').to_string();
- let mut routes = rocket::routes![
- index,
+ let routes = rocket::routes![
+ docs,
+ health,
assets,
+ docs_json,
routes::login,
routes::dashboard,
routes::view_process,
routes::action_handler,
routes::env_handler,
routes::info_handler,
routes::dump_handler,
routes::servers_handler,
routes::config_handler,
routes::list_handler,
routes::logs_handler,
routes::logs_raw_handler,
routes::metrics_handler,
routes::prometheus_handler,
routes::create_handler,
routes::rename_handler,
- docs_json,
- docs,
];
- if webui {
- routes.remove(0);
- } else {
- routes.remove(1);
- routes.remove(2);
- routes.remove(3);
- routes.remove(4);
-
- log::debug!("{:?} {:?} {:?} {:?}", routes[1], routes[2], routes[3], routes[4]);
- }
-
let rocket = rocket::custom(config::read().get_address())
.attach(Logger)
.manage(TeraState { path: tera.1, tera: tera.0 })
.mount(format!("{s_path}/"), routes)
- .register(format!("{s_path}/"), rocket::catchers![internal_error, not_allowed, not_found, unauthorized])
+ .register("/", rocket::catchers![internal_error, not_allowed, not_found, unauthorized])
.launch()
.await;
if let Err(err) = rocket {
log::error!("failed to launch!\n{err}")
}
}
#[rocket::get("/assets/<name>")]
pub async fn assets(name: String) -> Option<NamedFile> {
static DIR: Dir = include_dir!("src/webui/dist/assets");
let file = DIR.get_file(&name)?;
NamedFile::send(name, file.contents_utf8()).await.ok()
}
#[rocket::get("/docs")]
pub async fn docs() -> (ContentType, String) {
const DOCS: &str = include_str!("docs/index.html");
let s_path = config::read().get_path().trim_end_matches('/').to_string();
let docs_path = fmtstr!("{}/docs.json", s_path);
(ContentType::HTML, RapiDoc::new(docs_path).custom_html(DOCS).to_html())
}
-#[rocket::get("/")]
-pub async fn index() -> Value { json!({"healthy": true}) }
+#[rocket::get("/health")]
+pub async fn health() -> Value { json!({"healthy": true}) }
#[rocket::get("/docs.json")]
pub async fn docs_json() -> Value { json!(ApiDoc::openapi()) }
diff --git a/src/daemon/api/routes.rs b/src/daemon/api/routes.rs
index 5c1d962..9b0ad9c 100644
--- a/src/daemon/api/routes.rs
+++ b/src/daemon/api/routes.rs
@@ -1,556 +1,591 @@
use chrono::{DateTime, Utc};
use global_placeholders::global;
use macros_rs::{string, ternary, then};
use prometheus::{Encoder, TextEncoder};
use psutil::process::{MemoryInfo, Process};
use serde::Deserialize;
use tera::{Context, Tera};
use utoipa::ToSchema;
use rocket::{
get,
http::ContentType,
post,
serde::{json::Json, Serialize},
State,
};
use super::{
helpers::{not_found, NotFound},
- TeraState,
+ EnableWebUI, TeraState,
};
use pmc::{
- config::{self, structs::Servers},
- file, helpers,
+ config, file, helpers,
process::{dump, ItemSingle, ProcessItem, Runner},
};
use crate::daemon::{
api::{HTTP_COUNTER, HTTP_REQ_HISTOGRAM},
pid,
};
use std::{
collections::HashMap,
env,
fs::{self, File},
io::{self, BufRead, BufReader},
path::PathBuf,
};
pub(crate) struct Token;
type EnvList = Json<HashMap<String, String>>;
#[allow(dead_code)]
#[derive(ToSchema)]
#[schema(as = MemoryInfo)]
pub(crate) struct DocMemoryInfo {
rss: u64,
vms: u64,
#[cfg(target_os = "linux")]
shared: u64,
#[cfg(target_os = "linux")]
text: u64,
#[cfg(target_os = "linux")]
data: u64,
#[cfg(target_os = "macos")]
page_faults: u64,
#[cfg(target_os = "macos")]
pageins: u64,
}
#[derive(Deserialize, ToSchema)]
pub(crate) struct ActionBody {
#[schema(example = "restart")]
method: String,
}
#[derive(Serialize, ToSchema)]
pub(crate) struct ConfigBody {
#[schema(example = "bash")]
shell: String,
#[schema(min_items = 1, example = json!(["-c"]))]
args: Vec<String>,
#[schema(example = "/home/user/.pmc/logs")]
log_path: String,
}
#[derive(Deserialize, ToSchema)]
pub(crate) struct CreateBody {
#[schema(example = "app")]
name: Option<String>,
#[schema(example = "node index.js")]
script: String,
#[schema(value_type = String, example = "/projects/app")]
path: PathBuf,
#[schema(example = "src")]
watch: Option<String>,
}
#[derive(Serialize, ToSchema)]
pub(crate) struct ActionResponse {
#[schema(example = true)]
done: bool,
#[schema(example = "name")]
action: &'static str,
}
#[derive(Serialize, ToSchema)]
pub(crate) struct Server {
+ pub name: String,
pub address: String,
pub token: Option<String>,
}
#[derive(Serialize, ToSchema)]
pub(crate) struct LogResponse {
logs: Vec<String>,
}
#[derive(Serialize, ToSchema)]
pub struct MetricsRoot {
pub version: Version,
pub daemon: Daemon,
}
#[derive(Serialize, ToSchema)]
pub struct Version {
#[schema(example = "v1.0.0")]
pub pkg: String,
pub hash: &'static str,
#[schema(example = "2000-01-01")]
pub build_date: &'static str,
#[schema(example = "release")]
pub target: &'static str,
}
#[derive(Serialize, ToSchema)]
pub struct Daemon {
pub pid: Option<i32>,
#[schema(example = true)]
pub running: bool,
pub uptime: String,
pub process_count: usize,
#[schema(example = "default")]
pub daemon_type: String,
pub stats: Stats,
}
#[derive(Serialize, ToSchema)]
pub struct Stats {
pub memory_usage: String,
pub cpu_percent: String,
}
+#[derive(Serialize, ToSchema)]
+pub struct Servers {
+ #[schema(example = json!([{"name": "example", "address": "http://127.0.0.1:5630", "token": "test_token"}]))]
+ pub servers: Vec<Server>,
+}
+
fn attempt(done: bool, method: &str) -> ActionResponse {
ActionResponse {
done,
action: ternary!(done, Box::leak(Box::from(method)), "DOES_NOT_EXIST"),
}
}
fn render(name: &str, tmpl: &Tera, ctx: &Context) -> Result<String, NotFound> { tmpl.render(name, &ctx).or(Err(not_found("Page was not found"))) }
#[get("/")]
-pub async fn dashboard(state: &State<TeraState>) -> Result<(ContentType, String), NotFound> {
+pub async fn dashboard(state: &State<TeraState>, _webui: EnableWebUI) -> Result<(ContentType, String), NotFound> {
let mut ctx = Context::new();
ctx.insert("base_path", &state.path);
let payload = render("dashboard", &state.tera, &ctx)?;
Ok((ContentType::HTML, payload))
}
#[get("/login")]
-pub async fn login(state: &State<TeraState>) -> Result<(ContentType, String), NotFound> {
+pub async fn login(state: &State<TeraState>, _webui: EnableWebUI) -> Result<(ContentType, String), NotFound> {
let mut ctx = Context::new();
ctx.insert("base_path", &state.path);
let payload = render("login", &state.tera, &ctx)?;
Ok((ContentType::HTML, payload))
}
#[get("/view/<id>")]
-pub async fn view_process(id: usize, state: &State<TeraState>) -> Result<(ContentType, String), NotFound> {
+pub async fn view_process(id: usize, state: &State<TeraState>, _webui: EnableWebUI) -> Result<(ContentType, String), NotFound> {
let mut ctx = Context::new();
ctx.insert("base_path", &state.path);
ctx.insert("process_id", &id);
let payload = render("view", &state.tera, &ctx)?;
Ok((ContentType::HTML, payload))
}
#[get("/daemon/prometheus")]
#[utoipa::path(get, tag = "Daemon", path = "/daemon/prometheus", security((), ("api_key" = [])),
responses(
- (status = 200, description = "Get prometheus metrics", body = String),
+ (
+ description = "Get prometheus metrics", body = String, status = 200,
+ example = json!("# HELP daemon_cpu_percentage The cpu usage graph of the daemon.\n# TYPE daemon_cpu_percentage histogram\ndaemon_cpu_percentage_bucket{le=\"0.005\"} 0"),
+ ),
(status = UNAUTHORIZED, description = "Authentication failed or not provided", body = AuthMessage)
)
)]
pub async fn prometheus_handler(_t: Token) -> String {
let encoder = TextEncoder::new();
let mut buffer = Vec::<u8>::new();
let metric_families = prometheus::gather();
encoder.encode(&metric_families, &mut buffer).unwrap();
String::from_utf8(buffer.clone()).unwrap()
}
#[get("/daemon/servers")]
#[utoipa::path(get, tag = "Daemon", path = "/daemon/servers", security((), ("api_key" = [])),
responses(
(status = 200, description = "Get daemon servers successfully", body = Servers),
(status = UNAUTHORIZED, description = "Authentication failed or not provided", body = AuthMessage)
)
)]
-pub async fn servers_handler(_t: Token) -> Json<Servers> {
+pub async fn servers_handler(_t: Token) -> Json<Vec<Server>> {
let timer = HTTP_REQ_HISTOGRAM.with_label_values(&["servers"]).start_timer();
+ let server_list = config::servers();
+
+ let servers = server_list
+ .servers
+ .unwrap()
+ .iter()
+ .map(|(name, item)| Server {
+ name: name.to_string(),
+ token: item.token.to_owned(),
+ address: item.address.to_owned(),
+ })
+ .collect::<Vec<Server>>();
HTTP_COUNTER.inc();
timer.observe_duration();
- Json(config::servers())
+ Json(servers)
}
#[get("/daemon/dump")]
#[utoipa::path(get, tag = "Daemon", path = "/daemon/dump", security((), ("api_key" = [])),
responses(
(status = 200, description = "Dump processes successfully", body = [u8]),
(status = UNAUTHORIZED, description = "Authentication failed or not provided", body = AuthMessage)
)
)]
pub async fn dump_handler(_t: Token) -> Vec<u8> {
let timer = HTTP_REQ_HISTOGRAM.with_label_values(&["dump"]).start_timer();
HTTP_COUNTER.inc();
timer.observe_duration();
dump::raw()
}
#[get("/daemon/config")]
#[utoipa::path(get, tag = "Daemon", path = "/daemon/config", security((), ("api_key" = [])),
responses(
(status = 200, description = "Get daemon config successfully", body = ConfigBody),
(status = UNAUTHORIZED, description = "Authentication failed or not provided", body = AuthMessage)
)
)]
pub async fn config_handler(_t: Token) -> Json<ConfigBody> {
let timer = HTTP_REQ_HISTOGRAM.with_label_values(&["dump"]).start_timer();
let config = config::read().runner;
HTTP_COUNTER.inc();
timer.observe_duration();
Json(ConfigBody {
shell: config.shell,
args: config.args,
log_path: config.log_path,
})
}
#[get("/list")]
#[utoipa::path(get, path = "/list", tag = "Process", security((), ("api_key" = [])),
responses(
(status = 200, description = "List processes successfully", body = [ProcessItem]),
(status = UNAUTHORIZED, description = "Authentication failed or not provided", body = AuthMessage)
)
)]
pub async fn list_handler(_t: Token) -> Json<Vec<ProcessItem>> {
let timer = HTTP_REQ_HISTOGRAM.with_label_values(&["list"]).start_timer();
let data = Runner::new().fetch();
HTTP_COUNTER.inc();
timer.observe_duration();
Json(data)
}
#[get("/process/<id>/logs/<kind>")]
#[utoipa::path(get, tag = "Process", path = "/process/{id}/logs/{kind}",
security((), ("api_key" = [])),
params(
("id" = usize, Path, description = "Process id to get logs for", example = 0),
("kind" = String, Path, description = "Log output type", example = "out")
),
responses(
(status = 200, description = "Process logs of {type} fetched", body = LogResponse),
(status = NOT_FOUND, description = "Process was not found", body = ErrorMessage),
(status = UNAUTHORIZED, description = "Authentication failed or not provided", body = AuthMessage)
)
)]
pub async fn logs_handler(id: usize, kind: String, _t: Token) -> Result<Json<LogResponse>, NotFound> {
let timer = HTTP_REQ_HISTOGRAM.with_label_values(&["log"]).start_timer();
HTTP_COUNTER.inc();
match Runner::new().info(id) {
Some(item) => {
let log_file = match kind.as_str() {
"out" | "stdout" => item.logs().out,
"error" | "stderr" => item.logs().error,
_ => item.logs().out,
};
match File::open(log_file) {
Ok(data) => {
let reader = BufReader::new(data);
let logs: Vec<String> = reader.lines().collect::<io::Result<_>>().unwrap();
timer.observe_duration();
Ok(Json(LogResponse { logs }))
}
Err(_) => Ok(Json(LogResponse { logs: vec![] })),
}
}
None => {
timer.observe_duration();
Err(not_found("Process was not found"))
}
}
}
#[get("/process/<id>/logs/<kind>/raw")]
#[utoipa::path(get, tag = "Process", path = "/process/{id}/logs/{kind}/raw",
security((), ("api_key" = [])),
params(
("id" = usize, Path, description = "Process id to get logs for", example = 0),
("kind" = String, Path, description = "Log output type", example = "out")
),
responses(
- (status = 200, description = "Process logs of {type} fetched raw", body = String),
+ (
+ status = 200,
+ description = "Process logs of {type} fetched raw", body = String,
+ example = json!("# PATH path/of/file.log\nserver started on port 3000")
+ ),
(status = NOT_FOUND, description = "Process was not found", body = ErrorMessage),
(status = UNAUTHORIZED, description = "Authentication failed or not provided", body = AuthMessage)
)
)]
pub async fn logs_raw_handler(id: usize, kind: String, _t: Token) -> Result<String, NotFound> {
let timer = HTTP_REQ_HISTOGRAM.with_label_values(&["log"]).start_timer();
HTTP_COUNTER.inc();
match Runner::new().info(id) {
Some(item) => {
let log_file = match kind.as_str() {
"out" | "stdout" => item.logs().out,
"error" | "stderr" => item.logs().error,
_ => item.logs().out,
};
let data = match fs::read_to_string(&log_file) {
Ok(data) => format!("# PATH {log_file}\n{data}"),
Err(err) => err.to_string(),
};
timer.observe_duration();
Ok(data)
}
None => {
timer.observe_duration();
Err(not_found("Process was not found"))
}
}
}
#[get("/process/<id>/info")]
#[utoipa::path(get, tag = "Process", path = "/process/{id}/info", security((), ("api_key" = [])),
params(("id" = usize, Path, description = "Process id to get information for", example = 0)),
responses(
(status = 200, description = "Current process info retrieved", body = ItemSingle),
(status = NOT_FOUND, description = "Process was not found", body = ErrorMessage),
(status = UNAUTHORIZED, description = "Authentication failed or not provided", body = AuthMessage)
)
)]
pub async fn info_handler(id: usize, _t: Token) -> Result<Json<ItemSingle>, NotFound> {
let timer = HTTP_REQ_HISTOGRAM.with_label_values(&["info"]).start_timer();
let runner = Runner::new();
if runner.exists(id) {
let item = runner.get(id);
HTTP_COUNTER.inc();
timer.observe_duration();
Ok(Json(item.fetch()))
} else {
Err(not_found("Process was not found"))
}
}
#[post("/process/create", format = "json", data = "<body>")]
#[utoipa::path(post, tag = "Process", path = "/process/create", request_body(content = CreateBody),
security((), ("api_key" = [])),
responses(
- (status = 200, description = "Create process successful", body = ActionResponse),
+ (
+ description = "Create process successful", body = ActionResponse,
+ example = json!({"action": "create", "done": true }), status = 200,
+ ),
(status = INTERNAL_SERVER_ERROR, description = "Failed to create process", body = ErrorMessage),
(status = UNAUTHORIZED, description = "Authentication failed or not provided", body = AuthMessage)
)
)]
pub async fn create_handler(body: Json<CreateBody>, _t: Token) -> Result<Json<ActionResponse>, ()> {
let timer = HTTP_REQ_HISTOGRAM.with_label_values(&["create"]).start_timer();
let mut runner = Runner::new();
HTTP_COUNTER.inc();
let name = match &body.name {
Some(name) => string!(name),
None => string!(body.script.split_whitespace().next().unwrap_or_default()),
};
runner.start(&name, &body.script, body.path.clone(), &body.watch).save();
timer.observe_duration();
Ok(Json(attempt(true, "create")))
}
#[post("/process/<id>/rename", format = "text", data = "<body>")]
-#[utoipa::path(post, tag = "Process", path = "/process/{id}/rename", request_body(content = String),
+#[utoipa::path(post, tag = "Process", path = "/process/{id}/rename",
security((), ("api_key" = [])),
+ request_body(content = String, example = json!("example_name")),
params(("id" = usize, Path, description = "Process id to rename", example = 0)),
responses(
- (status = 200, description = "Rename process successful", body = ActionResponse),
+ (
+ description = "Rename process successful", body = ActionResponse,
+ example = json!({"action": "rename", "done": true }), status = 200,
+ ),
(status = NOT_FOUND, description = "Process was not found", body = ErrorMessage),
(status = UNAUTHORIZED, description = "Authentication failed or not provided", body = AuthMessage)
)
)]
pub async fn rename_handler(id: usize, body: String, _t: Token) -> Result<Json<ActionResponse>, NotFound> {
let timer = HTTP_REQ_HISTOGRAM.with_label_values(&["rename"]).start_timer();
let runner = Runner::new();
match runner.clone().info(id) {
Some(process) => {
HTTP_COUNTER.inc();
let mut item = runner.get(id);
item.rename(body.trim().replace("\n", ""));
then!(process.running, item.restart());
timer.observe_duration();
Ok(Json(attempt(true, "rename")))
}
None => {
timer.observe_duration();
Err(not_found("Process was not found"))
}
}
}
#[get("/process/<id>/env")]
#[utoipa::path(get, tag = "Process", path = "/process/{id}/env",
params(("id" = usize, Path, description = "Process id to fetch env from", example = 0)),
responses(
- (status = 200, description = "Current process env", body = HashMap<String, String>),
+ (
+ description = "Current process env", body = HashMap<String, String>,
+ example = json!({"ENV_TEST_VALUE": "example_value"}), status = 200
+ ),
(status = NOT_FOUND, description = "Process was not found", body = ErrorMessage),
(status = UNAUTHORIZED, description = "Authentication failed or not provided", body = AuthMessage)
)
)]
pub async fn env_handler(id: usize, _t: Token) -> Result<EnvList, NotFound> {
let timer = HTTP_REQ_HISTOGRAM.with_label_values(&["env"]).start_timer();
HTTP_COUNTER.inc();
match Runner::new().info(id) {
Some(item) => {
timer.observe_duration();
Ok(Json(item.clone().env))
}
None => {
timer.observe_duration();
Err(not_found("Process was not found"))
}
}
}
#[post("/process/<id>/action", format = "json", data = "<body>")]
#[utoipa::path(post, tag = "Process", path = "/process/{id}/action", request_body = ActionBody,
security((), ("api_key" = [])),
params(("id" = usize, Path, description = "Process id to run action on", example = 0)),
responses(
(status = 200, description = "Run action on process successful", body = ActionResponse),
(status = NOT_FOUND, description = "Process/action was not found", body = ErrorMessage),
(status = UNAUTHORIZED, description = "Authentication failed or not provided", body = AuthMessage)
)
)]
pub async fn action_handler(id: usize, body: Json<ActionBody>, _t: Token) -> Result<Json<ActionResponse>, NotFound> {
let timer = HTTP_REQ_HISTOGRAM.with_label_values(&["action"]).start_timer();
let mut runner = Runner::new();
let method = body.method.as_str();
if runner.exists(id) {
HTTP_COUNTER.inc();
match method {
"start" | "restart" => {
runner.get(id).restart();
timer.observe_duration();
Ok(Json(attempt(true, method)))
}
"stop" | "kill" => {
runner.get(id).stop();
timer.observe_duration();
Ok(Json(attempt(true, method)))
}
"remove" | "delete" => {
runner.remove(id);
timer.observe_duration();
Ok(Json(attempt(true, method)))
}
_ => {
timer.observe_duration();
Err(not_found("Process was not found"))
}
}
} else {
Err(not_found("Process was not found"))
}
}
#[get("/daemon/metrics")]
#[utoipa::path(get, tag = "Daemon", path = "/daemon/metrics", security((), ("api_key" = [])),
responses((status = 200, description = "Get daemon metrics", body = MetricsRoot),
(status = UNAUTHORIZED, description = "Authentication failed or not provided", body = AuthMessage))
)]
pub async fn metrics_handler(_t: Token) -> Json<MetricsRoot> {
let timer = HTTP_REQ_HISTOGRAM.with_label_values(&["metrics"]).start_timer();
let mut pid: Option<i32> = None;
let mut cpu_percent: Option<f32> = None;
let mut uptime: Option<DateTime<Utc>> = None;
let mut memory_usage: Option<MemoryInfo> = None;
let mut runner: Runner = file::read_rmp(global!("pmc.dump"));
HTTP_COUNTER.inc();
if pid::exists() {
if let Ok(process_id) = pid::read() {
if let Ok(mut process) = Process::new(process_id as u32) {
pid = Some(process_id);
uptime = Some(pid::uptime().unwrap());
memory_usage = process.memory_info().ok();
cpu_percent = process.cpu_percent().ok();
}
}
}
let memory_usage = match memory_usage {
Some(usage) => helpers::format_memory(usage.rss()),
None => string!("0b"),
};
let cpu_percent = match cpu_percent {
Some(percent) => format!("{:.2}%", percent),
None => string!("0%"),
};
let uptime = match uptime {
Some(uptime) => helpers::format_duration(uptime),
None => string!("none"),
};
timer.observe_duration();
Json(MetricsRoot {
version: Version {
pkg: format!("v{}", env!("CARGO_PKG_VERSION")),
hash: env!("GIT_HASH_FULL"),
build_date: env!("BUILD_DATE"),
target: env!("PROFILE"),
},
daemon: Daemon {
pid,
uptime,
running: pid::exists(),
process_count: runner.count(),
daemon_type: global!("pmc.daemon.kind"),
stats: Stats { memory_usage, cpu_percent },
},
})
}
diff --git a/src/daemon/api/structs.rs b/src/daemon/api/structs.rs
index f6495be..af43de2 100644
--- a/src/daemon/api/structs.rs
+++ b/src/daemon/api/structs.rs
@@ -1,18 +1,18 @@
use rocket::serde::Serialize;
use utoipa::ToSchema;
#[derive(Serialize, ToSchema)]
pub(crate) struct ErrorMessage {
#[schema(example = 404)]
pub(crate) code: u16,
- #[schema(example = "NOT_FOUND")]
+ #[schema(example = "Not Found")]
pub(crate) message: &'static str,
}
#[derive(Serialize, ToSchema)]
pub(crate) struct AuthMessage {
#[schema(example = 401)]
pub(crate) code: u16,
- #[schema(example = "UNAUTHORIZED")]
+ #[schema(example = "Unauthorized")]
pub(crate) message: String,
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Feb 1, 1:49 PM (1 d, 37 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
494771
Default Alt Text
(30 KB)

Event Timeline