Page MenuHomePhorge

No OneTemporary

Size
68 KB
Referenced Files
None
Subscribers
None
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index a39980a..8373dd4 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,49 +1,49 @@
stages: [build, release]
image: 'themackabu/rust:zigbuild-1.75.0'
before_script:
- mkdir binary
- apt-get update -yqq
- apt-get install -yqq zip clang llvm pkg-config libssl-dev
- export CC="/usr/bin/clang"
- export CXX="/usr/bin/clang++"
build_linux_amd64:
stage: build
tags: [fedora]
only: [/\d+\.\d+\.\d+.*$/]
script:
- - cargo zigbuild -r -vv --color always
+ - cargo zigbuild -r --color always
- zip binary/pmc_${CI_COMMIT_TAG}_linux_amd64.zip target/release/pmc -j
artifacts:
paths: [binary/]
build_linux_aarch64:
stage: build
tags: [fedora]
only: [/\d+\.\d+\.\d+.*$/]
script:
- - cargo zigbuild -r -vv --target aarch64-unknown-linux-gnu --color always
+ - cargo zigbuild -r --target aarch64-unknown-linux-gnu --color always
- zip binary/pmc_${CI_COMMIT_TAG}_linux_aarch64.zip target/aarch64-unknown-linux-gnu/release/pmc -j
artifacts:
paths: [binary/]
build_darwin_amd64:
stage: build
tags: [fedora]
only: [/\d+\.\d+\.\d+.*$/]
script:
- - cargo zigbuild -r -vv --target x86_64-apple-darwin --color always
+ - cargo zigbuild -r --target x86_64-apple-darwin --color always
- zip binary/pmc_${CI_COMMIT_TAG}_darwin_amd64.zip target/x86_64-apple-darwin/release/pmc -j
artifacts:
paths: [binary/]
build_darwin_aarch64:
stage: build
tags: [fedora]
only: [/\d+\.\d+\.\d+.*$/]
script:
- - cargo zigbuild -r -vv --target aarch64-apple-darwin --color always
+ - cargo zigbuild -r --target aarch64-apple-darwin --color always
- zip binary/pmc_${CI_COMMIT_TAG}_darwin_arm.zip target/aarch64-apple-darwin/release/pmc -j
artifacts:
paths: [binary/]
diff --git a/.maid/build.toml b/.maid/build.toml
index 2851e92..e3ec4cc 100644
--- a/.maid/build.toml
+++ b/.maid/build.toml
@@ -1,44 +1,44 @@
[env]
VERSION='1.7.1'
[tasks.build_all]
info = "build all"
script = [
# install packages
"apt-get update -yqq",
"apt-get install zip clang llvm -yqq",
# setup build
"mkdir build",
"export CC=\"/usr/bin/clang\"",
"export CXX=\"/usr/bin/clang++\"",
# build linux (x86_64)
"cargo zigbuild -r --color always",
"mv target/release/pmc build/pmc",
"zip build/pmc_%{env.VERSION}_linux_amd64.zip build/pmc",
"rm build/pmc",
- # build macos (x86_64)
- "cargo zigbuild -r --target x86_64-apple-darwin --color always",
- "mv target/x86_64-apple-darwin/release/pmc build/pmc",
- "zip build/pmc_%{env.VERSION}_darwin_amd64.zip build/pmc",
- "rm build/pmc",
-
- # build macos (aarch64)
- "cargo zigbuild -r --target aarch64-apple-darwin --color always",
- "mv target/aarch64-apple-darwin/release/pmc build/pmc",
- "zip build/pmc_%{env.VERSION}_darwin_arm.zip build/pmc",
- "rm build/pmc",
+ # # build macos (x86_64)
+ # "cargo zigbuild -r --target x86_64-apple-darwin --color always",
+ # "mv target/x86_64-apple-darwin/release/pmc build/pmc",
+ # "zip build/pmc_%{env.VERSION}_darwin_amd64.zip build/pmc",
+ # "rm build/pmc",
+ #
+ # # build macos (aarch64)
+ # "cargo zigbuild -r --target aarch64-apple-darwin --color always",
+ # "mv target/aarch64-apple-darwin/release/pmc build/pmc",
+ # "zip build/pmc_%{env.VERSION}_darwin_arm.zip build/pmc",
+ # "rm build/pmc",
# post build
"ls -sh build",
]
[tasks.build_all.remote]
silent = false
exclusive = true
shell = "/bin/bash"
image = "themackabu/rust:zigbuild-1.75.0"
push = ["src", "lib", "Cargo.toml", "Cargo.lock", "build.rs"]
pull = "build"
diff --git a/Cargo.toml b/Cargo.toml
index 2bb65bb..e9b73f7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,52 +1,52 @@
[package]
name = "pmc"
version = "1.7.1"
edition = "2021"
license = "MIT"
repository = "https://lab.themackabu.dev/self/pmc"
description = "PMC is a simple and easy to use PM2 alternative"
[dependencies]
log = "0.4.20"
toml = "0.8.8"
home = "0.5.9"
ryu = "1.0.16"
clap = "4.4.12"
cxx = "1.0.112"
bytes = "1.5.0"
tera = "1.19.1"
regex = "1.10.2"
libc = "0.2.151"
anyhow = "1.0.78"
colored = "2.1.0"
macros-rs = "0.5.0"
termcolor = "1.4.0"
rmp-serde = "1.1.2"
once_cell = "1.19.0"
env_logger = "0.10.1"
merkle_hash = "3.5.0"
lazy_static = "1.4.0"
prometheus = "0.13.3"
include_dir = "0.7.3"
serde_json = "1.0.109"
simple-logging = "2.0.2"
utoipa-rapidoc = "2.0.0"
pretty_env_logger = "0.5.0"
utoipa-swagger-ui = "5.0.0"
clap-verbosity-flag = "2.1.1"
global_placeholders = "0.1.0"
tokio = { version = "1.35.1", features = ["full"] }
rocket = { version = "0.5.0", features = ["json"] }
psutil = { version = "3.2.2", features = ["serde"] }
tabled = { version = "0.15.0", features = ["ansi"] }
chrono = { version = "0.4.31", features = ["serde"] }
serde = { version = "1.0.193", features = ["derive"] }
-utoipa = { version = "4.1.0", features = ["serde_yaml"] }
reqwest = { version = "0.11.23", features = ["blocking", "json"] }
+utoipa = { version = "4.1.0", features = ["serde_yaml", "non_strict_integers"] }
[build-dependencies]
tar = "0.4.40"
chrono = "0.4.31"
flate2 = "1.0.28"
cxx-build = "1.0.112"
reqwest = { version = "0.11.23", features = ["blocking"] }
diff --git a/src/daemon/api/docs/index.html b/src/daemon/api/docs/index.html
index 28e43f3..ec1f2f8 100644
--- a/src/daemon/api/docs/index.html
+++ b/src/daemon/api/docs/index.html
@@ -1,25 +1,25 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
<script type="module" src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"></script>
</head>
<body>
<rapi-doc
spec-url="$specUrl"
allow-search="true"
show-header="false"
render-style="focused"
show-curl-before-try="true"
allow-advanced-search="true"
allow-authentication="true"
- allow-server-selection="false"
+ allow-server-selection="true"
theme="dark"
bg-color="#18181b"
primary-color="#0ea5e9"
regular-font="Inter var"
mono-font="Menlo"
/>
</body>
</html>
diff --git a/src/daemon/api/fairing.rs b/src/daemon/api/fairing.rs
index 13012f7..0b3a7f6 100644
--- a/src/daemon/api/fairing.rs
+++ b/src/daemon/api/fairing.rs
@@ -1,42 +1,64 @@
-use rocket::fairing::{Fairing, Info, Kind};
-use rocket::{http::ContentType, Data, Orbit, Request, Response, Rocket};
+use rocket::{
+ async_trait,
+ fairing::{Fairing, Info, Kind},
+ http::{ContentType, Header},
+ Data, Orbit, Request, Response, Rocket,
+};
-#[rocket::async_trait]
+#[async_trait]
impl Fairing for super::Logger {
fn info(&self) -> Info {
Info {
name: "Logger Fairing",
kind: Kind::Liftoff | Kind::Request | Kind::Response,
}
}
async fn on_liftoff(&self, rocket: &Rocket<Orbit>) {
let config = rocket.config();
log!("[rocket] launched",
"tls" => config.tls_enabled(),
"keep_alive" => config.keep_alive,
"workers" => config.workers,
"profile" => config.profile.to_string(),
);
log!("[rocket] limits", "limits" => config.limits);
log!("[api] server started", "port" => config.port, "host" => config.address);
}
async fn on_request(&self, request: &mut Request<'_>, _: &mut Data<'_>) {
log!("[api] req",
"method" => request.method(),
"uri" => request.uri(),
"content_type" => request.content_type().unwrap_or(&ContentType::Plain),
);
}
async fn on_response<'r>(&self, _request: &'r Request<'_>, response: &mut Response<'r>) {
log!("[api] res",
"status" => response.status(),
"size" => response.body_mut().size().await.unwrap_or(0),
"content_type" => response.content_type().unwrap_or(ContentType::Plain),
);
}
}
+
+#[async_trait]
+impl Fairing for super::AddCORS {
+ fn info(&self) -> Info {
+ Info {
+ name: "Add CORS headers to responses",
+ kind: Kind::Response,
+ }
+ }
+
+ async fn on_response<'r>(&self, _request: &'r Request<'_>, response: &mut Response<'r>) {
+ response.set_header(Header::new("Access-Control-Allow-Origin", "*"));
+ response.set_header(Header::new("Access-Control-Allow-Credentials", "true"));
+ response.set_header(Header::new("Access-Control-Allow-Methods", "POST, GET, OPTIONS"));
+ response.set_header(Header::new("Access-Control-Allow-Headers", "token, Content-Type, Accept"));
+ response.set_header(Header::new("Access-Control-Expose-Headers", "Content-Encoding, Content-Type"));
+ }
+}
diff --git a/src/daemon/api/helpers.rs b/src/daemon/api/helpers.rs
index cb22161..a3203ef 100644
--- a/src/daemon/api/helpers.rs
+++ b/src/daemon/api/helpers.rs
@@ -1,18 +1,16 @@
use super::structs::ErrorMessage;
use rocket::{http::Status, response::status, serde::json::Json};
pub(crate) type NotFound = status::NotFound<Json<ErrorMessage>>;
+pub(crate) type GenericError = status::Custom<Json<ErrorMessage>>;
-pub(crate) fn create_status(status: Status) -> Json<ErrorMessage> {
- Json(ErrorMessage {
- code: status.code,
- message: status.reason_lossy(),
- })
-}
+pub(crate) fn create_status(code: Status) -> Json<ErrorMessage> { Json(ErrorMessage { code, message: code.to_string() }) }
+
+pub(crate) fn generic_error(code: Status, message: String) -> GenericError { status::Custom(code, Json(ErrorMessage { code, message })) }
-pub(crate) fn not_found(msg: &'static str) -> NotFound {
+pub(crate) fn not_found(msg: &str) -> NotFound {
status::NotFound(Json(ErrorMessage {
- code: Status::NotFound.code,
- message: msg,
+ code: Status::NotFound,
+ message: msg.to_string(),
}))
}
diff --git a/src/daemon/api/mod.rs b/src/daemon/api/mod.rs
index 58852d7..551800c 100644
--- a/src/daemon/api/mod.rs
+++ b/src/daemon/api/mod.rs
@@ -1,218 +1,225 @@
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, 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 structs::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,
+ request::{self, FromRequest, Request},
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),
+ servers(
+ (url = "{ssl}://{address}:{port}/{path}", description = "Remote API",
+ variables(
+ ("ssl" = (default = "http", enum_values("http", "https"))),
+ ("address" = (default = "localhost", description = "Address for API")),
+ ("port" = (default = "5630", description = "Port for API")),
+ ("path" = (default = "", description = "Path for API"))
+ )
+ )
+ ),
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::servers_list,
routes::logs_raw_handler,
routes::metrics_handler,
routes::prometheus_handler,
routes::create_handler,
routes::rename_handler
),
components(schemas(
- 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 AddCORS;
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> 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, ()))
+ Outcome::Error((rocket::http::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, ()))
+ Outcome::Error((rocket::http::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 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_list,
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,
];
let rocket = rocket::custom(config::read().get_address())
.attach(Logger)
+ .attach(AddCORS)
.manage(TeraState { path: tera.1, tera: tera.0 })
.mount(format!("{s_path}/"), routes)
.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("/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 2d67784..1a3ecb2 100644
--- a/src/daemon/api/routes.rs
+++ b/src/daemon/api/routes.rs
@@ -1,590 +1,653 @@
+#![allow(non_snake_case)]
+
use chrono::{DateTime, Utc};
use global_placeholders::global;
-use macros_rs::{string, ternary, then};
+use macros_rs::{fmtstr, 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,
+ http::{ContentType, Status},
post,
serde::{json::Json, Serialize},
State,
};
use super::{
- helpers::{not_found, NotFound},
+ helpers::{generic_error, not_found, GenericError, NotFound},
+ structs::ErrorMessage,
EnableWebUI, TeraState,
};
use pmc::{
config, file, helpers,
- process::{dump, ItemSingle, ProcessItem, Runner},
+ process::{dump, http::client, 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 {
- 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>, _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>, _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>, _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(
(
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)
+ (
+ status = UNAUTHORIZED, description = "Authentication failed or not provided", body = ErrorMessage,
+ example = json!({"code": 401, "message": "Unauthorized"})
+ )
)
)]
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)
+ (status = 200, description = "Get daemon servers successfully", body = [String]),
+ (
+ status = UNAUTHORIZED, description = "Authentication failed or not provided", body = ErrorMessage,
+ example = json!({"code": 401, "message": "Unauthorized"})
+ )
)
)]
-pub async fn servers_handler(_t: Token) -> Json<Servers> {
+pub async fn servers_list(_t: Token) -> Result<Json<Vec<String>>, GenericError> {
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();
+ if let Some(servers) = config::servers().servers {
+ HTTP_COUNTER.inc();
+ timer.observe_duration();
- Json(Servers { servers })
+ Ok(Json(servers.into_keys().collect()))
+ } else {
+ Err(generic_error(Status::BadRequest, string!("No servers have been added")))
+ }
+}
+
+#[get("/daemon/server/<name>/list")]
+#[utoipa::path(get, tag = "Daemon", path = "/daemon/server/{name}/list", security((), ("api_key" = [])),
+ params(("name" = String, Path, description = "Name of remote daemon", example = "example"),),
+ responses(
+ (status = 200, description = "Get list from remote daemon successfully", body = [ProcessItem]),
+ (status = NOT_FOUND, description = "Remote daemon does not exist", body = ErrorMessage),
+ (
+ status = UNAUTHORIZED, description = "Authentication failed or not provided", body = ErrorMessage,
+ example = json!({"code": 401, "message": "Unauthorized"})
+ )
+ )
+)]
+pub async fn servers_handler(name: String, _t: Token) -> Result<Json<Vec<ProcessItem>>, GenericError> {
+ let timer = HTTP_REQ_HISTOGRAM.with_label_values(&["servers"]).start_timer();
+
+ if let Some(servers) = config::servers().servers {
+ let (address, (client, headers)) = match servers.get(&name) {
+ Some(server) => (&server.address, client(&server.token)),
+ None => return Err(generic_error(Status::NotFound, string!("Server was not found"))),
+ };
+
+ HTTP_COUNTER.inc();
+ timer.observe_duration();
+
+ match client.get(fmtstr!("{address}/list")).headers(headers).send() {
+ Ok(data) => {
+ if data.status() != 200 {
+ let err = data.json::<ErrorMessage>().unwrap();
+ Err(generic_error(err.code, err.message))
+ } else {
+ Ok(Json(data.json::<Vec<ProcessItem>>().unwrap()))
+ }
+ }
+ Err(err) => Err(generic_error(Status::InternalServerError, err.to_string())),
+ }
+ } else {
+ Err(generic_error(Status::BadRequest, string!("No servers have been added")))
+ }
}
#[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)
+ (
+ status = UNAUTHORIZED, description = "Authentication failed or not provided", body = ErrorMessage,
+ example = json!({"code": 401, "message": "Unauthorized"})
+ )
)
)]
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)
+ (
+ status = UNAUTHORIZED, description = "Authentication failed or not provided", body = ErrorMessage,
+ example = json!({"code": 401, "message": "Unauthorized"})
+ )
)
)]
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)
+ (
+ status = UNAUTHORIZED, description = "Authentication failed or not provided", body = ErrorMessage,
+ example = json!({"code": 401, "message": "Unauthorized"})
+ )
)
)]
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)
+ (
+ status = UNAUTHORIZED, description = "Authentication failed or not provided", body = ErrorMessage,
+ example = json!({"code": 401, "message": "Unauthorized"})
+ )
)
)]
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,
+ description = "Process logs of {type} fetched raw", body = String, status = 200,
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)
+ (
+ status = UNAUTHORIZED, description = "Authentication failed or not provided", body = ErrorMessage,
+ example = json!({"code": 401, "message": "Unauthorized"})
+ )
)
)]
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)
+ (
+ status = UNAUTHORIZED, description = "Authentication failed or not provided", body = ErrorMessage,
+ example = json!({"code": 401, "message": "Unauthorized"})
+ )
)
)]
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(
(
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)
+ (
+ status = UNAUTHORIZED, description = "Authentication failed or not provided", body = ErrorMessage,
+ example = json!({"code": 401, "message": "Unauthorized"})
+ )
)
)]
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",
security((), ("api_key" = [])),
request_body(content = String, example = json!("example_name")),
params(("id" = usize, Path, description = "Process id to rename", example = 0)),
responses(
(
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)
+ (
+ status = UNAUTHORIZED, description = "Authentication failed or not provided", body = ErrorMessage,
+ example = json!({"code": 401, "message": "Unauthorized"})
+ )
)
)]
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(
(
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)
+ (
+ status = UNAUTHORIZED, description = "Authentication failed or not provided", body = ErrorMessage,
+ example = json!({"code": 401, "message": "Unauthorized"})
+ )
)
)]
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)
+ (
+ status = UNAUTHORIZED, description = "Authentication failed or not provided", body = ErrorMessage,
+ example = json!({"code": 401, "message": "Unauthorized"})
+ )
)
)]
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))
+ responses(
+ (status = 200, description = "Get daemon metrics", body = MetricsRoot),
+ (
+ status = UNAUTHORIZED, description = "Authentication failed or not provided", body = ErrorMessage,
+ example = json!({"code": 401, "message": "Unauthorized"})
+ )
+ )
)]
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 af43de2..1c5155e 100644
--- a/src/daemon/api/structs.rs
+++ b/src/daemon/api/structs.rs
@@ -1,18 +1,26 @@
-use rocket::serde::Serialize;
-use utoipa::ToSchema;
+#![allow(dead_code)]
-#[derive(Serialize, ToSchema)]
+use rocket::http::Status;
+use serde::{Deserialize, Serialize};
+use serde_json::json;
+
+use utoipa::{
+ openapi::{KnownFormat, Object, ObjectBuilder, SchemaFormat, SchemaType},
+ ToSchema,
+};
+
+#[derive(Serialize, Deserialize, ToSchema)]
pub(crate) struct ErrorMessage {
- #[schema(example = 404)]
- pub(crate) code: u16,
+ #[schema(schema_with = status)]
+ pub(crate) code: Status,
#[schema(example = "Not Found")]
- pub(crate) message: &'static str,
+ pub(crate) message: String,
}
-#[derive(Serialize, ToSchema)]
-pub(crate) struct AuthMessage {
- #[schema(example = 401)]
- pub(crate) code: u16,
- #[schema(example = "Unauthorized")]
- pub(crate) message: String,
+fn status() -> Object {
+ ObjectBuilder::new()
+ .schema_type(SchemaType::Integer)
+ .format(Some(SchemaFormat::KnownFormat(KnownFormat::UInt16)))
+ .example(Some(json!(404)))
+ .build()
}
diff --git a/src/process/http.rs b/src/process/http.rs
index 68c4e50..ffdde27 100644
--- a/src/process/http.rs
+++ b/src/process/http.rs
@@ -1,84 +1,84 @@
use crate::process::Remote;
use macros_rs::{fmtstr, str, string};
use reqwest::blocking::{Client, Response};
use reqwest::header::{HeaderMap, HeaderValue};
use serde::Serialize;
use std::path::PathBuf;
#[derive(Serialize)]
struct ActionBody {
pub method: String,
}
pub struct LogResponse {
pub path: &'static str,
pub lines: Vec<String>,
}
#[derive(Serialize)]
struct CreateBody<'c> {
pub name: &'c String,
pub script: &'c String,
pub path: PathBuf,
pub watch: &'c Option<String>,
}
-fn client(token: &Option<String>) -> (Client, HeaderMap) {
+pub fn client(token: &Option<String>) -> (Client, HeaderMap) {
let client = Client::new();
let mut headers = HeaderMap::new();
if let Some(token) = token {
headers.insert("token", HeaderValue::from_static(str!(token.to_owned())));
}
return (client, headers);
}
pub fn info(Remote { address, token, .. }: &Remote, id: usize) -> Result<Response, anyhow::Error> {
let (client, headers) = client(token);
Ok(client.get(fmtstr!("{address}/process/{id}/info")).headers(headers).send()?)
}
pub fn logs(Remote { address, token, .. }: &Remote, id: usize, kind: &str) -> Result<LogResponse, anyhow::Error> {
let (client, headers) = client(token);
let response = client.get(fmtstr!("{address}/process/{id}/logs/{kind}/raw")).headers(headers).send()?;
let log = response.text()?;
Ok(LogResponse {
lines: log.lines().skip(1).map(|line| line.to_string()).collect::<Vec<String>>(),
path: Box::leak(Box::from(log.lines().next().unwrap_or("").split_whitespace().last().unwrap_or(""))),
})
}
pub fn create(Remote { address, token, .. }: &Remote, name: &String, script: &String, path: PathBuf, watch: &Option<String>) -> Result<Response, anyhow::Error> {
let (client, headers) = client(token);
let content = CreateBody { name, script, path, watch };
Ok(client.post(fmtstr!("{address}/process/create")).json(&content).headers(headers).send()?)
}
pub fn restart(Remote { address, token, .. }: &Remote, id: usize) -> Result<Response, anyhow::Error> {
let (client, headers) = client(token);
let content = ActionBody { method: string!("restart") };
Ok(client.post(fmtstr!("{address}/process/{id}/action")).json(&content).headers(headers).send()?)
}
pub fn rename(Remote { address, token, .. }: &Remote, id: usize, name: String) -> Result<Response, anyhow::Error> {
let (client, headers) = client(token);
Ok(client.post(fmtstr!("{address}/process/{id}/rename")).body(name).headers(headers).send()?)
}
pub fn stop(Remote { address, token, .. }: &Remote, id: usize) -> Result<Response, anyhow::Error> {
let (client, headers) = client(token);
let content = ActionBody { method: string!("stop") };
Ok(client.post(fmtstr!("{address}/process/{id}/action")).json(&content).headers(headers).send()?)
}
pub fn remove(Remote { address, token, .. }: &Remote, id: usize) -> Result<Response, anyhow::Error> {
let (client, headers) = client(token);
let content = ActionBody { method: string!("remove") };
Ok(client.post(fmtstr!("{address}/process/{id}/action")).json(&content).headers(headers).send()?)
}
diff --git a/src/process/mod.rs b/src/process/mod.rs
index e03ef74..35dd2e3 100644
--- a/src/process/mod.rs
+++ b/src/process/mod.rs
@@ -1,534 +1,534 @@
use crate::{
config,
config::structs::Server,
file, helpers,
service::{run, stop, ProcessMetadata},
};
use std::{
env,
path::PathBuf,
sync::{Arc, Mutex},
};
use chrono::serde::ts_milliseconds;
use chrono::{DateTime, Utc};
use global_placeholders::global;
use macros_rs::{crashln, string, ternary, then};
use psutil::process;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
use utoipa::ToSchema;
#[derive(Serialize, Deserialize, ToSchema)]
pub struct ItemSingle {
pub info: Info,
pub stats: Stats,
pub watch: Watch,
pub log: Log,
pub raw: Raw,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Info {
pub id: usize,
pub pid: i64,
pub name: String,
pub status: String,
#[schema(value_type = String, example = "/path")]
pub path: PathBuf,
pub uptime: String,
pub command: String,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Stats {
pub restarts: u64,
pub start_time: i64,
pub cpu_percent: Option<f32>,
pub memory_usage: Option<MemoryInfo>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct MemoryInfo {
pub rss: u64,
pub vms: u64,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Log {
pub out: String,
pub error: String,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Raw {
pub running: bool,
pub crashed: bool,
pub crashes: u64,
}
#[derive(Clone)]
pub struct LogInfo {
pub out: String,
pub error: String,
}
-#[derive(Serialize, ToSchema)]
+#[derive(Serialize, Deserialize, ToSchema)]
pub struct ProcessItem {
pid: i64,
id: usize,
cpu: String,
mem: String,
name: String,
restarts: u64,
status: String,
uptime: String,
#[schema(example = "/path")]
watch_path: String,
#[schema(value_type = String, example = "2000-01-01T01:00:00.000Z")]
start_time: DateTime<Utc>,
}
#[derive(Clone)]
pub struct ProcessWrapper {
pub id: usize,
pub runner: Arc<Mutex<Runner>>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Process {
pub id: usize,
pub pid: i64,
pub name: String,
pub path: PathBuf,
pub script: String,
pub env: HashMap<String, String>,
#[serde(with = "ts_milliseconds")]
pub started: DateTime<Utc>,
pub restarts: u64,
pub running: bool,
pub crash: Crash,
pub watch: Watch,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Crash {
pub crashed: bool,
pub value: u64,
}
#[derive(Clone, Debug, Deserialize, Serialize, ToSchema)]
pub struct Watch {
pub enabled: bool,
#[schema(example = "/path")]
pub path: String,
pub hash: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Runner {
pub id: id::Id,
#[serde(skip)]
pub remote: Option<Remote>,
pub list: BTreeMap<usize, Process>,
}
#[derive(Clone, Debug)]
pub struct Remote {
address: String,
token: Option<String>,
pub config: RemoteConfig,
}
#[derive(Clone, Debug, Deserialize)]
pub struct RemoteConfig {
pub shell: String,
pub args: Vec<String>,
pub log_path: String,
}
pub enum Status {
Offline,
Running,
}
impl Status {
pub fn to_bool(&self) -> bool {
match self {
Status::Offline => false,
Status::Running => true,
}
}
}
macro_rules! lock {
($runner:expr) => {{
match $runner.lock() {
Ok(runner) => runner,
Err(err) => crashln!("Unable to lock mutex: {err}"),
}
}};
}
impl Runner {
pub fn new() -> Self { dump::read() }
pub fn connect(name: String, Server { address, token }: Server, verbose: bool) -> Option<Self> {
let remote_config = match config::from(&address, token.as_deref()) {
Ok(config) => config,
Err(err) => {
log::error!("{err}");
return None;
}
};
if let Ok(dump) = dump::from(&address, token.as_deref()) {
then!(verbose, println!("{} Fetched remote (name={name}, address={address})", *helpers::SUCCESS));
Some(Runner {
remote: Some(Remote {
token,
address: string!(address),
config: remote_config,
}),
..dump
})
} else {
None
}
}
pub fn start(&mut self, name: &String, command: &String, path: PathBuf, watch: &Option<String>) -> &mut Self {
if let Some(remote) = &self.remote {
if let Err(err) = http::create(remote, name, command, path, watch) {
crashln!("{} Failed to start create {name}\nError: {:#?}", *helpers::FAIL, err);
};
} else {
let id = self.id.next();
let config = config::read().runner;
let crash = Crash { crashed: false, value: 0 };
let watch = match watch {
Some(watch) => Watch {
enabled: true,
path: string!(watch),
hash: hash::create(file::cwd().join(watch)),
},
None => Watch {
enabled: false,
path: string!(""),
hash: string!(""),
},
};
let pid = run(ProcessMetadata {
args: config.args,
name: name.clone(),
shell: config.shell,
command: command.clone(),
log_path: config.log_path,
});
self.list.insert(
id,
Process {
id,
pid,
path,
watch,
crash,
restarts: 0,
running: true,
name: name.clone(),
started: Utc::now(),
script: command.clone(),
env: env::vars().collect(),
},
);
}
return self;
}
pub fn restart(&mut self, id: usize, dead: bool) -> &mut Self {
if let Some(remote) = &self.remote {
if let Err(err) = http::restart(remote, id) {
crashln!("{} Failed to start process {id}\nError: {:#?}", *helpers::FAIL, err);
};
} else {
let process = self.process(id);
let config = config::read().runner;
let Process { path, script, name, .. } = process.clone();
if let Err(err) = std::env::set_current_dir(&process.path) {
crashln!("{} Failed to set working directory {:?}\nError: {:#?}", *helpers::FAIL, path, err);
};
stop(process.pid);
process.running = false;
process.crash.crashed = false;
process.pid = run(ProcessMetadata {
args: config.args,
name: name.clone(),
shell: config.shell,
log_path: config.log_path,
command: script.to_string(),
});
process.running = true;
process.started = Utc::now();
then!(!dead, process.crash.value = 0);
then!(dead, process.restarts += 1);
}
return self;
}
pub fn remove(&mut self, id: usize) {
if let Some(remote) = &self.remote {
if let Err(err) = http::remove(remote, id) {
crashln!("{} Failed to stop remove {id}\nError: {:#?}", *helpers::FAIL, err);
};
} else {
self.stop(id);
self.list.remove(&id);
dump::write(&self);
}
}
pub fn set_id(&mut self, id: id::Id) {
self.id = id;
self.id.next();
dump::write(&self);
}
pub fn set_status(&mut self, id: usize, status: Status) {
self.process(id).running = status.to_bool();
dump::write(&self);
}
pub fn items(&self) -> BTreeMap<usize, Process> { self.list.clone() }
pub fn items_mut(&mut self) -> &mut BTreeMap<usize, Process> { &mut self.list }
pub fn save(&self) { then!(self.remote.is_none(), dump::write(&self)) }
pub fn count(&mut self) -> usize { self.list().count() }
pub fn is_empty(&self) -> bool { self.list.is_empty() }
pub fn exists(&self, id: usize) -> bool { self.list.contains_key(&id) }
pub fn info(&self, id: usize) -> Option<&Process> { self.list.get(&id) }
pub fn list<'l>(&'l mut self) -> impl Iterator<Item = (&'l usize, &'l mut Process)> { self.list.iter_mut().map(|(k, v)| (k, v)) }
pub fn process(&mut self, id: usize) -> &mut Process { self.list.get_mut(&id).unwrap_or_else(|| crashln!("{} Process ({id}) not found", *helpers::FAIL)) }
pub fn get(self, id: usize) -> ProcessWrapper {
ProcessWrapper {
id,
runner: Arc::new(Mutex::new(self)),
}
}
pub fn set_crashed(&mut self, id: usize) -> &mut Self {
self.process(id).crash.crashed = true;
return self;
}
pub fn new_crash(&mut self, id: usize) -> &mut Self {
self.process(id).crash.value += 1;
return self;
}
pub fn stop(&mut self, id: usize) -> &mut Self {
if let Some(remote) = &self.remote {
if let Err(err) = http::stop(remote, id) {
crashln!("{} Failed to stop process {id}\nError: {:#?}", *helpers::FAIL, err);
};
} else {
let process = self.process(id);
stop(process.pid);
process.running = false;
process.crash.crashed = false;
process.crash.value = 0;
}
return self;
}
pub fn rename(&mut self, id: usize, name: String) -> &mut Self {
if let Some(remote) = &self.remote {
if let Err(err) = http::rename(remote, id, name) {
crashln!("{} Failed to rename process {id}\nError: {:#?}", *helpers::FAIL, err);
};
} else {
self.process(id).name = name;
}
return self;
}
pub fn watch(&mut self, id: usize, path: &str, enabled: bool) -> &mut Self {
let process = self.process(id);
process.watch = Watch {
enabled,
path: string!(path),
hash: ternary!(enabled, hash::create(process.path.join(path)), string!("")),
};
return self;
}
pub fn fetch(&self) -> Vec<ProcessItem> {
let mut processes: Vec<ProcessItem> = Vec::new();
for (id, item) in self.items() {
let mut memory_usage: Option<MemoryInfo> = None;
let mut cpu_percent: Option<f32> = None;
if let Ok(mut process) = process::Process::new(item.pid as u32) {
let mem_info_psutil = process.memory_info().ok();
cpu_percent = process.cpu_percent().ok();
memory_usage = Some(MemoryInfo {
rss: mem_info_psutil.as_ref().unwrap().rss(),
vms: mem_info_psutil.as_ref().unwrap().vms(),
});
}
let cpu_percent = match cpu_percent {
Some(percent) => format!("{:.2}%", percent),
None => string!("0.00%"),
};
let memory_usage = match memory_usage {
Some(usage) => helpers::format_memory(usage.rss),
None => string!("0b"),
};
let status = if item.running {
string!("online")
} else {
match item.crash.crashed {
true => string!("crashed"),
false => string!("stopped"),
}
};
processes.push(ProcessItem {
id,
status,
pid: item.pid,
cpu: cpu_percent,
mem: memory_usage,
restarts: item.restarts,
name: item.name.clone(),
start_time: item.started,
watch_path: item.watch.path.clone(),
uptime: helpers::format_duration(item.started),
});
}
return processes;
}
}
impl Process {
/// Get a log paths of the process item
pub fn logs(&self) -> LogInfo {
let name = self.name.replace(" ", "_");
LogInfo {
out: global!("pmc.logs.out", name.as_str()),
error: global!("pmc.logs.error", name.as_str()),
}
}
}
impl ProcessWrapper {
/// Stop the process item
pub fn stop(&mut self) { lock!(self.runner).stop(self.id).save(); }
/// Restart the process item
pub fn restart(&mut self) { lock!(self.runner).restart(self.id, false).save(); }
/// Rename the process item
pub fn rename(&mut self, name: String) { lock!(self.runner).rename(self.id, name).save(); }
/// Enable watching a path on the process item
pub fn watch(&mut self, path: &str) { lock!(self.runner).watch(self.id, path, true).save(); }
/// Disable watching on the process item
pub fn disable_watch(&mut self) { lock!(self.runner).watch(self.id, "", false).save(); }
/// Set the process item as crashed
pub fn crashed(&mut self) {
let mut runner = lock!(self.runner);
runner.new_crash(self.id).save();
runner.restart(self.id, true).save();
}
/// Get a json dump of the process item
pub fn fetch(&self) -> ItemSingle {
let mut runner = lock!(self.runner);
let item = runner.process(self.id);
let config = config::read().runner;
let mut memory_usage: Option<MemoryInfo> = None;
let mut cpu_percent: Option<f32> = None;
if let Ok(mut process) = process::Process::new(item.pid as u32) {
let mem_info_psutil = process.memory_info().ok();
cpu_percent = process.cpu_percent().ok();
memory_usage = Some(MemoryInfo {
rss: mem_info_psutil.as_ref().unwrap().rss(),
vms: mem_info_psutil.as_ref().unwrap().vms(),
});
}
let status = if item.running {
string!("online")
} else {
match item.crash.crashed {
true => string!("crashed"),
false => string!("stopped"),
}
};
ItemSingle {
info: Info {
status,
id: item.id,
pid: item.pid,
name: item.name.clone(),
path: item.path.clone(),
uptime: helpers::format_duration(item.started),
command: format!("{} {} '{}'", config.shell, config.args.join(" "), item.script.clone()),
},
stats: Stats {
cpu_percent,
memory_usage,
restarts: item.restarts,
start_time: item.started.timestamp_millis(),
},
watch: Watch {
enabled: item.watch.enabled,
hash: item.watch.hash.clone(),
path: item.watch.path.clone(),
},
log: Log {
out: item.logs().out,
error: item.logs().error,
},
raw: Raw {
running: item.running,
crashed: item.crash.crashed,
crashes: item.crash.value,
},
}
}
}
pub mod dump;
pub mod hash;
pub mod http;
pub mod id;
diff --git a/src/webui/src/components/react/index.tsx b/src/webui/src/components/react/index.tsx
index bd58d4a..6cee879 100644
--- a/src/webui/src/components/react/index.tsx
+++ b/src/webui/src/components/react/index.tsx
@@ -1,129 +1,140 @@
import { api } from '@/api';
+import { $settings } from '@/store';
import Rename from '@/components/react/rename';
import { useEffect, useState, Fragment } from 'react';
import { Menu, Transition } from '@headlessui/react';
import { EllipsisVerticalIcon } from '@heroicons/react/20/solid';
const Index = (props: { base: string }) => {
+ const servers = JSON.parse($settings.get().servers);
const [items, setItems] = useState([]);
const badge = {
online: 'bg-emerald-400',
stopped: 'bg-red-500',
crashed: 'bg-amber-400',
};
const fetch = () => {
api
.get(props.base + '/list')
.json()
.then((res) => setItems(res));
+
+ api
+ .get(props.base + '/daemon/servers')
+ .json()
+ .then((res) => $settings.setKey('servers', JSON.stringify(res.servers)));
+
+ servers.forEach((s) => {
+ api.get(s.address + '/list').json();
+ });
};
const classNames = (...classes: Array<any>) => classes.filter(Boolean).join(' ');
const isRunning = (status: string): bool => (status == 'stopped' ? false : status == 'crashed' ? false : true);
const action = (id: number, name: string) => api.post(`${props.base}/process/${id}/action`, { json: { method: name } }).then(() => fetch());
useEffect(() => fetch(), []);
return (
<ul role="list" className="grid grid-cols-1 gap-x-6 gap-y-8 lg:grid-cols-4 xl:gap-x-8">
{items.map((item) => (
<li key={item.id} className="rounded-lg border border-zinc-700/50 bg-zinc-900/10 hover:bg-zinc-900/40 hover:border-zinc-700">
<div className="flex items-center gap-x-4 border-b border-zinc-800/80 bg-zinc-900/20 px-4 py-3">
<span className="text-md font-bold text-zinc-200 truncate">
{item.name}
<div className="text-xs font-medium text-zinc-400">{isRunning(item.status) ? item.pid : 'none'}</div>
</span>
<span className="relative flex h-2 w-2 -mt-3.5 -ml-2">
<span className={`${badge[item.status]} relative inline-flex rounded-full h-2 w-2`}></span>
</span>
<Menu as="div" className="relative ml-auto">
<Menu.Button className="transition border focus:outline-none focus:ring-0 focus:ring-offset-0 z-50 shrink-0 border-zinc-700/50 bg-transparent hover:bg-zinc-800 p-2 text-sm font-semibold rounded-lg ml-3">
<EllipsisVerticalIcon className="h-5 w-5 text-zinc-50" aria-hidden="true" />
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95">
<Menu.Items className="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-lg bg-zinc-900 border border-zinc-800 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none text-base divide-y divide-zinc-800/50">
<div className="p-1.5">
<Menu.Item>
{({ active }) => (
<a
onClick={() => action(item.id, 'restart')}
className={classNames(
active ? 'bg-blue-700/10 text-blue-500' : 'text-zinc-200',
'rounded-md block px-2 py-2 w-full text-left cursor-pointer'
)}>
Reload
</a>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<a
onClick={() => action(item.id, 'stop')}
className={classNames(
active ? 'bg-yellow-400/10 text-amber-500' : 'text-zinc-200',
'rounded-md block p-2 w-full text-left cursor-pointer'
)}>
Terminate
</a>
)}
</Menu.Item>
</div>
<div className="p-1.5">
<Menu.Item>
{({ active }) => <Rename base={props.base} process={item.id} callback={fetch} active={active} old={item.name} />}
</Menu.Item>
</div>
<div className="p-1.5">
<Menu.Item>
{({ active }) => (
<a
onClick={() => action(item.id, 'delete')}
className={classNames(
active ? 'bg-red-700/10 text-red-500' : 'text-red-400',
'rounded-md block p-2 w-full text-left cursor-pointer'
)}>
Delete
</a>
)}
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</Menu>
</div>
<a href={`./view/${item.id}`}>
<dl className="-my-3 divide-y divide-zinc-800/30 px-6 py-4 text-sm leading-6">
<div className="flex justify-between gap-x-1 py-1">
<dt className="text-zinc-700">restarts</dt>
<dd className="text-zinc-500">{item.restarts}</dd>
</div>
<div className="flex justify-between gap-x-1 py-1">
<dt className="text-zinc-700">cpu</dt>
<dd className="text-zinc-500">{isRunning(item.status) ? item.cpu : 'offline'}</dd>
</div>
<div className="flex justify-between gap-x-1 py-1">
<dt className="text-zinc-700">memory</dt>
<dd className="text-zinc-500">{isRunning(item.status) ? item.mem : 'offline'}</dd>
</div>
<div className="flex justify-between gap-x-1 py-1">
<dt className="text-zinc-700">uptime</dt>
<dd className="text-zinc-500">{isRunning(item.status) ? item.uptime : 'none'}</dd>
</div>
</dl>
</a>
</li>
))}
</ul>
);
};
export default Index;

File Metadata

Mime Type
text/x-diff
Expires
Sun, Feb 1, 7:48 PM (1 d, 17 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
494901
Default Alt Text
(68 KB)

Event Timeline