From d3e88df2813e1a49643757c0279104179e14db78 Mon Sep 17 00:00:00 2001 From: theMackabu Date: Sun, 7 Jul 2024 20:19:04 -0700 Subject: [PATCH] hotfix: show correct cpu usage --- .gitignore | 1 + Cargo.lock | 121 +++++++++++++---- Cargo.toml | 1 + build.rs | 1 + lib/include/psutil.h | 8 ++ lib/process.cc | 2 +- lib/psutil.cc | 77 +++++++++++ src/cli/internal.rs | 14 +- src/config/mod.rs | 2 +- src/daemon/api/mod.rs | 8 +- src/daemon/api/routes.rs | 93 ++++++++----- src/daemon/mod.rs | 16 +-- src/daemon/pid.rs | 25 +++- src/lib.rs | 2 + src/process/mod.rs | 14 +- src/webui/mod.rs | 2 + src/webui/src/components/navbar.astro | 3 +- src/webui/src/components/react/servers.tsx | 151 +++++++++++++++++++++ src/webui/src/components/react/status.tsx | 0 src/webui/src/components/react/view.tsx | 4 +- src/webui/src/pages/index.astro | 4 +- src/webui/src/pages/login.astro | 2 +- src/webui/src/pages/servers.astro | 13 ++ src/webui/src/pages/status.astro | 13 ++ src/webui/src/pages/view.astro | 4 +- 25 files changed, 485 insertions(+), 96 deletions(-) create mode 100644 lib/include/psutil.h create mode 100644 lib/psutil.cc create mode 100644 src/webui/src/components/react/servers.tsx create mode 100644 src/webui/src/components/react/status.tsx create mode 100644 src/webui/src/pages/servers.astro create mode 100644 src/webui/src/pages/status.astro diff --git a/.gitignore b/.gitignore index 5c21490..dd8c742 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ pnpm-debug.log* # jetbrains .idea .fleet +test \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 33cca57..7a9203b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -605,6 +605,12 @@ dependencies = [ "libc", ] +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + [[package]] name = "deranged" version = "0.3.11" @@ -984,7 +990,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.11", "indexmap", "slab", "tokio", @@ -1069,6 +1075,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.6" @@ -1076,7 +1093,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http", + "http 0.2.11", "pin-project-lite", ] @@ -1118,7 +1135,7 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", + "http 0.2.11", "http-body", "httparse", "httpdate", @@ -1138,7 +1155,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", - "http", + "http 0.2.11", "hyper", "rustls 0.21.10", "tokio", @@ -1255,12 +1272,12 @@ checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "is-terminal" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" +checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" dependencies = [ "hermit-abi", - "rustix", + "libc", "windows-sys 0.52.0", ] @@ -1297,9 +1314,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.152" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libm" @@ -1451,16 +1468,15 @@ dependencies = [ [[package]] name = "multer" -version = "2.1.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" dependencies = [ "bytes", "encoding_rs", "futures-util", - "http", + "http 1.1.0", "httparse", - "log", "memchr", "mime", "spin", @@ -1776,6 +1792,7 @@ dependencies = [ "regex", "reqwest", "rocket", + "rocket_ws", "ron", "ryu", "serde", @@ -2063,7 +2080,7 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", + "http 0.2.11", "http-body", "hyper", "hyper-rustls", @@ -2107,9 +2124,9 @@ dependencies = [ [[package]] name = "rocket" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e7bb57ccb26670d73b6a47396c83139447b9e7878cab627fdfe9ea8da489150" +checksum = "a516907296a31df7dc04310e7043b61d71954d703b603cc6867a026d7e72d73f" dependencies = [ "async-stream", "async-trait", @@ -2145,9 +2162,9 @@ dependencies = [ [[package]] name = "rocket_codegen" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2238066abf75f21be6cd7dc1a09d5414a671f4246e384e49fe3f8a4936bd04c" +checksum = "575d32d7ec1a9770108c879fc7c47815a80073f96ca07ff9525a94fcede1dd46" dependencies = [ "devise", "glob", @@ -2162,14 +2179,14 @@ dependencies = [ [[package]] name = "rocket_http" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37a1663694d059fe5f943ea5481363e48050acedd241d46deb2e27f71110389e" +checksum = "e274915a20ee3065f611c044bd63c40757396b6dbc057d6046aec27f14f882b9" dependencies = [ "cookie", "either", "futures", - "http", + "http 0.2.11", "hyper", "indexmap", "log", @@ -2187,6 +2204,16 @@ dependencies = [ "uncased", ] +[[package]] +name = "rocket_ws" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f1877668c937b701177c349f21383c556cd3bb4ba8fa1d07fa96ccb3a8782e" +dependencies = [ + "rocket", + "tokio-tungstenite", +] + [[package]] name = "ron" version = "0.8.1" @@ -2435,6 +2462,17 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.8" @@ -2848,6 +2886,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + [[package]] name = "tokio-util" version = "0.7.10" @@ -2969,6 +3019,25 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.1.0", + "httparse", + "log", + "rand", + "sha1", + "thiserror", + "url", + "utf-8", +] + [[package]] name = "typenum" version = "1.17.0" @@ -3160,6 +3229,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8parse" version = "0.2.1" @@ -3597,9 +3672,9 @@ dependencies = [ [[package]] name = "yansi" -version = "1.0.0-rc.1" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1367295b8f788d371ce2dbc842c7b709c73ee1364d30351dd300ec2203b12377" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" dependencies = [ "is-terminal", ] diff --git a/Cargo.toml b/Cargo.toml index 5614e74..a055005 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,7 @@ chrono = { version = "0.4.31", features = ["serde"] } serde = { version = "1.0.193", features = ["derive"] } nix = { version = "0.27.1", features = ["process", "signal"] } utoipa = { version = "4.1.0", features = ["serde_yaml", "non_strict_integers"] } +rocket_ws = "0.1.1" [dependencies.reqwest] version = "0.11.23" diff --git a/build.rs b/build.rs index b629391..42d34a3 100644 --- a/build.rs +++ b/build.rs @@ -147,6 +147,7 @@ fn main() { .file("lib/bridge.cc") .file("lib/process.cc") .file("lib/fork.cc") + .file("lib/psutil.cc") .include("lib/include") .flag_if_supported("-std=c++17") .compile("bridge"); diff --git a/lib/include/psutil.h b/lib/include/psutil.h new file mode 100644 index 0000000..abefd9e --- /dev/null +++ b/lib/include/psutil.h @@ -0,0 +1,8 @@ +#ifndef PSUTIL_H +#define PSUTIL_H + +#include +using namespace rust; + +extern "C++" double get_process_cpu_usage_percentage(int64_t pid); +#endif diff --git a/lib/process.cc b/lib/process.cc index c9c53e7..38543ec 100644 --- a/lib/process.cc +++ b/lib/process.cc @@ -93,7 +93,7 @@ int64_t Runner::Run(const std::string &command, const std::string &shell, Vec(argsArray.data()), const_cast(envArray.data())) == -1) { + if (execve(shell.c_str(), const_cast(argsArray.data()), const_cast(envArray.data())) == -1) { std::cerr << "[PMC] (cc) Unable to execute the command\n"; perror("execvp"); exit(EXIT_FAILURE); diff --git a/lib/psutil.cc b/lib/psutil.cc new file mode 100644 index 0000000..e001c84 --- /dev/null +++ b/lib/psutil.cc @@ -0,0 +1,77 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef __APPLE__ +#include +#include +#endif + +double get_process_cpu_usage_percentage(int64_t pid) { + auto get_cpu_time = [](int64_t pid) -> double { +#ifdef __APPLE__ + struct proc_taskinfo pti; + int ret = proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &pti, sizeof(pti)); + if (ret <= 0) { + std::cerr << "Error: Unable to get process info" << std::endl; + return -1.0; + } + return (pti.pti_total_user + pti.pti_total_system) / 100000000.0; // Convert nanoseconds to seconds +#else + std::string stat_path = "/proc/" + std::to_string(pid) + "/stat"; + std::ifstream stat_file(stat_path); + + if (!stat_file.is_open()) { + std::cerr << "Error: Unable to open " << stat_path << std::endl; + return -1.0; + } + + std::string line; + std::getline(stat_file, line); + stat_file.close(); + + std::istringstream iss(line); + std::string token; + std::vector tokens; + + while (std::getline(iss, token, ' ')) { + tokens.push_back(token); + } + + if (tokens.size() < 15) { + std::cerr << "Error: Invalid stat file format" << std::endl; + return -1.0; + } + + unsigned long long utime = std::stoull(tokens[13]); + unsigned long long stime = std::stoull(tokens[14]); + + return (utime + stime) / sysconf(_SC_CLK_TCK); +#endif + }; + + double cpu_time1 = get_cpu_time(pid); + if (cpu_time1 < 0) return -1.0; + + auto start = std::chrono::high_resolution_clock::now(); + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + auto end = std::chrono::high_resolution_clock::now(); + + double cpu_time2 = get_cpu_time(pid); + if (cpu_time2 < 0) return -1.0; + + std::chrono::duration elapsed = end - start; + double elapsed_seconds = elapsed.count(); + double cpu_time_diff = cpu_time2 - cpu_time1; + + long num_cores = sysconf(_SC_NPROCESSORS_ONLN); + double cpu_usage_percentage = (cpu_time_diff / elapsed_seconds) * 100.0 * num_cores; + + return std::min(cpu_usage_percentage, 100.0 * num_cores); +} diff --git a/src/cli/internal.rs b/src/cli/internal.rs index d41cada..70718a3 100644 --- a/src/cli/internal.rs +++ b/src/cli/internal.rs @@ -271,14 +271,14 @@ impl<'i> Internal<'i> { let item = runner.process(self.id); let mut memory_usage: Option = None; - let mut cpu_percent: Option = None; + let mut cpu_percent: Option = None; let path = file::make_relative(&item.path, &home).to_string_lossy().into_owned(); let children = if item.children.is_empty() { "none".to_string() } else { format!("{:?}", item.children) }; - if let Ok(mut process) = Process::new(item.pid as u32) { + if let Ok(process) = Process::new(item.pid as u32) { memory_usage = process.memory_info().ok(); - cpu_percent = process.cpu_percent().ok(); + cpu_percent = Some(pmc::service::get_process_cpu_usage_percentage(item.pid as i64)); } let cpu_percent = match cpu_percent { @@ -536,14 +536,14 @@ impl<'i> Internal<'i> { let mut memory_usage: String = string!("0b"); if internal { - let mut usage_internals: (Option, Option) = (None, None); + let mut usage_internals: (Option, Option) = (None, None); - if let Ok(mut process) = Process::new(item.pid as u32) { - usage_internals = (process.cpu_percent().ok(), process.memory_info().ok()); + if let Ok(process) = Process::new(item.pid as u32) { + usage_internals = (Some(pmc::service::get_process_cpu_usage_percentage(item.pid as i64)), process.memory_info().ok()); } cpu_percent = match usage_internals.0 { - Some(percent) => format!("{:.0}%", percent), + Some(percent) => format!("{:.2}%", percent), None => string!("0%"), }; diff --git a/src/config/mod.rs b/src/config/mod.rs index eb3af49..ff0a77d 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -39,7 +39,7 @@ pub fn read() -> Config { let config = Config { default: string!("local"), runner: Runner { - shell: string!("bash"), + shell: string!("/bin/sh"), args: vec![string!("-c")], node: string!("node"), log_path: format!("{path}/.pmc/logs"), diff --git a/src/daemon/api/mod.rs b/src/daemon/api/mod.rs index 5f5ea56..dd09acd 100644 --- a/src/daemon/api/mod.rs +++ b/src/daemon/api/mod.rs @@ -113,6 +113,9 @@ impl Modify for SecurityAddon { #[catch(500)] fn internal_error<'m>() -> Json { create_status(Status::InternalServerError) } +#[catch(400)] +fn bad_request<'m>() -> Json { create_status(Status::BadRequest) } + #[catch(405)] fn not_allowed<'m>() -> Json { create_status(Status::MethodNotAllowed) } @@ -177,8 +180,10 @@ pub async fn start(webui: bool) { assets, docs_json, routes::login, + routes::servers, routes::dashboard, routes::view_process, + routes::server_status, routes::action_handler, routes::env_handler, routes::info_handler, @@ -194,6 +199,7 @@ pub async fn start(webui: bool) { routes::logs_handler, routes::logs_raw_handler, routes::metrics_handler, + routes::stream_metrics, routes::prometheus_handler, routes::create_handler, routes::rename_handler, @@ -204,7 +210,7 @@ pub async fn start(webui: bool) { .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]) + .register("/", rocket::catchers![internal_error, bad_request, not_allowed, not_found, unauthorized]) .launch() .await; diff --git a/src/daemon/api/routes.rs b/src/daemon/api/routes.rs index b1b7db4..e76b3d7 100644 --- a/src/daemon/api/routes.rs +++ b/src/daemon/api/routes.rs @@ -6,11 +6,13 @@ use macros_rs::{fmtstr, string, ternary, then}; use prometheus::{Encoder, TextEncoder}; use psutil::process::{MemoryInfo, Process}; use reqwest::header::HeaderValue; +use rocket_ws::{Channel, Message, WebSocket}; use serde::Deserialize; -use tera::{Context, Tera}; +use tera::Context; use utoipa::ToSchema; use rocket::{ + futures::{SinkExt, StreamExt}, get, http::{ContentType, Status}, post, @@ -31,7 +33,7 @@ use pmc::{ use crate::daemon::{ api::{HTTP_COUNTER, HTTP_REQ_HISTOGRAM}, - pid, + pid::{self, Pid}, }; use std::{ @@ -123,7 +125,7 @@ pub struct Version { #[derive(Serialize, ToSchema)] pub struct Daemon { - pub pid: Option, + pub pid: Option, #[schema(example = true)] pub running: bool, pub uptime: String, @@ -146,35 +148,34 @@ fn attempt(done: bool, method: &str) -> ActionResponse { } } -fn render(name: &str, tmpl: &Tera, ctx: &Context) -> Result { tmpl.render(name, &ctx).or(Err(not_found("Page was not found"))) } +fn render(name: &str, state: &State, ctx: &mut Context) -> Result { + ctx.insert("base_path", &state.path); + ctx.insert("build_version", env!("CARGO_PKG_VERSION")); + + state.tera.render(name, &ctx).or(Err(not_found("Page was not found"))) +} #[get("/")] -pub async fn dashboard(state: &State, _webui: EnableWebUI) -> Result<(ContentType, String), NotFound> { - let mut ctx = Context::new(); +pub async fn dashboard(state: &State, _webui: EnableWebUI) -> Result<(ContentType, String), NotFound> { Ok((ContentType::HTML, render("dashboard", &state, &mut Context::new())?)) } - ctx.insert("base_path", &state.path); - let payload = render("dashboard", &state.tera, &ctx)?; - Ok((ContentType::HTML, payload)) -} +#[get("/servers")] +pub async fn servers(state: &State, _webui: EnableWebUI) -> Result<(ContentType, String), NotFound> { Ok((ContentType::HTML, render("servers", &state, &mut Context::new())?)) } #[get("/login")] -pub async fn login(state: &State, _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)) -} +pub async fn login(state: &State, _webui: EnableWebUI) -> Result<(ContentType, String), NotFound> { Ok((ContentType::HTML, render("login", &state, &mut Context::new())?)) } #[get("/view/")] pub async fn view_process(id: usize, state: &State, _webui: EnableWebUI) -> Result<(ContentType, String), NotFound> { let mut ctx = Context::new(); - - ctx.insert("base_path", &state.path); ctx.insert("process_id", &id); + Ok((ContentType::HTML, render("view", &state, &mut ctx)?)) +} - let payload = render("view", &state.tera, &ctx)?; - Ok((ContentType::HTML, payload)) +#[get("/status/")] +pub async fn server_status(id: usize, state: &State, _webui: EnableWebUI) -> Result<(ContentType, String), NotFound> { + let mut ctx = Context::new(); + ctx.insert("process_id", &id); + Ok((ContentType::HTML, render("status", &state, &mut ctx)?)) } #[get("/daemon/prometheus")] @@ -779,20 +780,10 @@ pub async fn action_handler(id: usize, body: Json, _t: Token) -> Res } } -#[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 = ErrorMessage, - example = json!({"code": 401, "message": "Unauthorized"}) - ) - ) -)] -pub async fn metrics_handler(_t: Token) -> Json { +pub async fn get_metrics() -> MetricsRoot { let timer = HTTP_REQ_HISTOGRAM.with_label_values(&["metrics"]).start_timer(); - let mut pid: Option = None; - let mut cpu_percent: Option = None; + let mut pid: Option = None; + let mut cpu_percent: Option = None; let mut uptime: Option> = None; let mut memory_usage: Option = None; let mut runner: Runner = file::read_object(global!("pmc.dump")); @@ -800,11 +791,11 @@ pub async fn metrics_handler(_t: Token) -> Json { HTTP_COUNTER.inc(); if pid::exists() { if let Ok(process_id) = pid::read() { - if let Ok(mut process) = Process::new(process_id as u32) { + if let Ok(process) = Process::new(process_id.get()) { pid = Some(process_id); uptime = Some(pid::uptime().unwrap()); memory_usage = process.memory_info().ok(); - cpu_percent = process.cpu_percent().ok(); + cpu_percent = Some(pmc::service::get_process_cpu_usage_percentage(process_id.get::())); } } } @@ -825,7 +816,7 @@ pub async fn metrics_handler(_t: Token) -> Json { }; timer.observe_duration(); - Json(MetricsRoot { + MetricsRoot { version: Version { target: env!("PROFILE"), build_date: env!("BUILD_DATE"), @@ -840,5 +831,33 @@ pub async fn metrics_handler(_t: Token) -> Json { daemon_type: global!("pmc.daemon.kind"), stats: Stats { memory_usage, cpu_percent }, }, + } +} + +#[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 = ErrorMessage, + example = json!({"code": 401, "message": "Unauthorized"}) + ) + ) +)] +pub async fn metrics_handler(_t: Token) -> Json { Json(get_metrics().await) } + +#[get("/daemon/stream_metrics")] +pub async fn stream_metrics(ws: WebSocket, _t: Token) -> Channel<'static> { + ws.channel(move |mut stream| { + Box::pin(async move { + while let Some(message) = stream.next().await { + let response = get_metrics().await; + let json = serde_json::to_string(&response); + + let _ = stream.send(Message::from(json.unwrap())).await; + } + + Ok(()) + }) }) } diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs index d2b1c21..d5ff15f 100644 --- a/src/daemon/mod.rs +++ b/src/daemon/mod.rs @@ -83,7 +83,7 @@ fn restart_process() { pub fn health(format: &String) { let mut pid: Option = None; - let mut cpu_percent: Option = None; + let mut cpu_percent: Option = None; let mut uptime: Option> = None; let mut memory_usage: Option = None; let mut runner: Runner = file::read_object(global!("pmc.dump")); @@ -126,11 +126,11 @@ pub fn health(format: &String) { 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); + if let Ok(process) = Process::new(process_id.get::()) { + pid = Some(process.pid() as i32); uptime = Some(pid::uptime().unwrap()); memory_usage = process.memory_info().ok(); - cpu_percent = process.cpu_percent().ok(); + cpu_percent = Some(pmc::service::get_process_cpu_usage_percentage(process_id.get::())); } } } @@ -194,7 +194,7 @@ pub fn stop() { match pid::read() { Ok(pid) => { - pmc::service::stop(pid as i64); + pmc::service::stop(pid.get()); pid::remove(); log!("[daemon] stopped", "pid" => pid); println!("{} PMC daemon stopped", *helpers::SUCCESS); @@ -228,7 +228,7 @@ pub fn start(verbose: bool) { if pid::exists() { match pid::read() { - Ok(pid) => then!(!pid::running(pid), pid::remove()), + Ok(pid) => then!(!pid::running(pid.get()), pid::remove()), Err(_) => crashln!("{} The daemon is already running", *helpers::FAIL), } } @@ -255,8 +255,8 @@ pub fn start(verbose: bool) { loop { if api_enabled { - if let Ok(mut process) = Process::new(process::id()) { - DAEMON_CPU_PERCENTAGE.observe(process.cpu_percent().ok().unwrap() as f64); + if let Ok(process) = Process::new(process::id()) { + DAEMON_CPU_PERCENTAGE.observe(pmc::service::get_process_cpu_usage_percentage(process.pid() as i64)); DAEMON_MEM_USAGE.observe(process.memory_info().ok().unwrap().rss() as f64); } } diff --git a/src/daemon/pid.rs b/src/daemon/pid.rs index 128772b..305efea 100644 --- a/src/daemon/pid.rs +++ b/src/daemon/pid.rs @@ -1,9 +1,28 @@ use anyhow::{anyhow, Result}; use chrono::{DateTime, Utc}; +use core::fmt; use global_placeholders::global; use macros_rs::crashln; use pmc::{file::Exists, helpers}; -use std::{fs, io}; +use serde::Serialize; +use std::{convert::TryFrom, fs, io}; + +#[derive(Copy, Clone, Serialize)] +pub struct Pid(i32); + +impl Pid { + pub fn get(&self) -> T + where + T: TryFrom, + T::Error: std::fmt::Debug, + { + T::try_from(self.0).unwrap() + } +} + +impl fmt::Display for Pid { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } +} pub fn exists() -> bool { fs::metadata(global!("pmc.pid")).is_ok() } pub fn running(pid: i32) -> bool { unsafe { libc::kill(pid, 0) == 0 } } @@ -16,13 +35,13 @@ pub fn uptime() -> io::Result> { Ok(creation_time) } -pub fn read() -> Result { +pub fn read() -> Result { let pid = fs::read_to_string(global!("pmc.pid")).map_err(|err| anyhow!(err))?; let trimmed_pid = pid.trim(); let parsed_pid = trimmed_pid.parse::().map_err(|err| anyhow!(err))?; - Ok(parsed_pid) + Ok(Pid(parsed_pid)) } pub fn write(pid: u32) { diff --git a/src/lib.rs b/src/lib.rs index f7b8000..2418679 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,6 +31,7 @@ pub mod service { unsafe extern "C++" { include!("pmc/lib/include/process.h"); + include!("pmc/lib/include/psutil.h"); include!("pmc/lib/include/bridge.h"); include!("pmc/lib/include/fork.h"); type Callback = crate::Callback; @@ -40,6 +41,7 @@ pub mod service { pub fn get_child_pid(parentPID: i64) -> i64; pub fn run(metadata: ProcessMetadata) -> i64; pub fn find_chidren(parentPID: i64) -> Vec; + pub fn get_process_cpu_usage_percentage(pid: i64) -> f64; pub fn try_fork(nochdir: bool, noclose: bool, callback: Callback) -> i32; } } diff --git a/src/process/mod.rs b/src/process/mod.rs index d3e2c36..ec14055 100644 --- a/src/process/mod.rs +++ b/src/process/mod.rs @@ -54,7 +54,7 @@ pub struct Info { pub struct Stats { pub restarts: u64, pub start_time: i64, - pub cpu_percent: Option, + pub cpu_percent: Option, pub memory_usage: Option, } @@ -483,12 +483,12 @@ impl Runner { for (id, item) in self.items() { let mut memory_usage: Option = None; - let mut cpu_percent: Option = None; + let mut cpu_percent: Option = None; - if let Ok(mut process) = process::Process::new(item.pid as u32) { + if let Ok(process) = process::Process::new(item.pid as u32) { let mem_info_psutil = process.memory_info().ok(); - cpu_percent = process.cpu_percent().ok(); + cpu_percent = Some(super::service::get_process_cpu_usage_percentage(item.pid as i64)); memory_usage = Some(MemoryInfo { rss: mem_info_psutil.as_ref().unwrap().rss(), vms: mem_info_psutil.as_ref().unwrap().vms(), @@ -594,12 +594,12 @@ impl ProcessWrapper { let config = config::read().runner; let mut memory_usage: Option = None; - let mut cpu_percent: Option = None; + let mut cpu_percent: Option = None; - if let Ok(mut process) = process::Process::new(item.pid as u32) { + if let Ok(process) = process::Process::new(item.pid as u32) { let mem_info_psutil = process.memory_info().ok(); - cpu_percent = process.cpu_percent().ok(); + cpu_percent = Some(super::service::get_process_cpu_usage_percentage(item.pid as i64)); memory_usage = Some(MemoryInfo { rss: mem_info_psutil.as_ref().unwrap().rss(), vms: mem_info_psutil.as_ref().unwrap().vms(), diff --git a/src/webui/mod.rs b/src/webui/mod.rs index 9ee5f76..88fa6cf 100644 --- a/src/webui/mod.rs +++ b/src/webui/mod.rs @@ -9,6 +9,8 @@ pub fn create_templates() -> (Tera, String) { ("view", include_str!("dist/view.html")), ("login", include_str!("dist/login.html")), ("dashboard", include_str!("dist/index.html")), + ("status", include_str!("dist/status.html")), + ("servers", include_str!("dist/servers.html")), ]) .unwrap(); diff --git a/src/webui/src/components/navbar.astro b/src/webui/src/components/navbar.astro index e1f5075..d4666ca 100644 --- a/src/webui/src/components/navbar.astro +++ b/src/webui/src/components/navbar.astro @@ -11,7 +11,8 @@ import favicon from '@/public/favicon.svg?url' PMC diff --git a/src/webui/src/components/react/servers.tsx b/src/webui/src/components/react/servers.tsx new file mode 100644 index 0000000..417ee21 --- /dev/null +++ b/src/webui/src/components/react/servers.tsx @@ -0,0 +1,151 @@ +import { api } from '@/api'; +import Rename from '@/components/react/rename'; +import { useEffect, useState, Fragment } from 'react'; +import { EllipsisVerticalIcon } from '@heroicons/react/20/solid'; +import { Menu, MenuItem, MenuItems, MenuButton, Transition } from '@headlessui/react'; + +const Index = (props: { base: string }) => { + const [items, setItems] = useState([]); + + const badge = { + online: 'bg-emerald-400', + stopped: 'bg-red-500', + crashed: 'bg-amber-400' + }; + + async function fetch() { + const items = await api.get(props.base + '/list').json(); + setItems(items.map((s) => ({ ...s, server: 'Internal' }))); + + try { + const servers = await api.get(props.base + '/daemon/servers').json(); + await servers.forEach(async (name) => { + const remote = await api.get(props.base + `/remote/${name}/list`).json(); + setItems((s) => [...s, ...remote.map((i) => ({ ...i, server: name }))]); + }); + } catch {} + } + + const classNames = (...classes: Array) => classes.filter(Boolean).join(' '); + const isRemote = (item: any): bool => (item.server == 'Internal' ? false : true); + 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 ( + + ); +}; + +export default Index; diff --git a/src/webui/src/components/react/status.tsx b/src/webui/src/components/react/status.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/webui/src/components/react/view.tsx b/src/webui/src/components/react/view.tsx index 7839112..08837ea 100644 --- a/src/webui/src/components/react/view.tsx +++ b/src/webui/src/components/react/view.tsx @@ -274,8 +274,8 @@ const View = (props: { id: string; base: string }) => { const stats = [ { name: 'Status', value: item.info.status }, { name: 'Uptime', value: online ? uptime : 'none', unit: online ? upunit : '' }, - { name: 'Memory', value: online ? memory : 'offline', unit: online ? memunit : '' }, - { name: 'CPU', value: online ? item.stats.cpu_percent : 'offline', unit: online ? '%' : '' } + { name: 'Memory', value: online ? memory.toFixed(2) : 'offline', unit: online ? memunit : '' }, + { name: 'CPU', value: online ? item.stats.cpu_percent.toFixed(2) : 'offline', unit: online ? '%' : '' } ]; return ( diff --git a/src/webui/src/pages/index.astro b/src/webui/src/pages/index.astro index f3a100e..ee9aca7 100644 --- a/src/webui/src/pages/index.astro +++ b/src/webui/src/pages/index.astro @@ -6,8 +6,8 @@ import { SITE_TITLE, SITE_DESCRIPTION } from '@/consts'; --- - +
- +
diff --git a/src/webui/src/pages/login.astro b/src/webui/src/pages/login.astro index 8427746..a420881 100644 --- a/src/webui/src/pages/login.astro +++ b/src/webui/src/pages/login.astro @@ -6,6 +6,6 @@ import { SITE_TITLE, SITE_DESCRIPTION } from '@/consts';
- +
diff --git a/src/webui/src/pages/servers.astro b/src/webui/src/pages/servers.astro new file mode 100644 index 0000000..544ed33 --- /dev/null +++ b/src/webui/src/pages/servers.astro @@ -0,0 +1,13 @@ +--- +import Layout from '@/components/base.astro'; +import Navbar from '@/components/navbar.astro'; +import ServersPage from '@/components/react/servers'; +import { SITE_TITLE, SITE_DESCRIPTION } from '@/consts'; +--- + + + +
+ +
+
diff --git a/src/webui/src/pages/status.astro b/src/webui/src/pages/status.astro new file mode 100644 index 0000000..428dd3e --- /dev/null +++ b/src/webui/src/pages/status.astro @@ -0,0 +1,13 @@ +--- +import Layout from '@/components/base.astro'; +import Navbar from '@/components/navbar.astro'; +import ViewPage from '@/components/react/view'; +import { SITE_TITLE, SITE_DESCRIPTION } from '@/consts'; +--- + + + +
+ +
+
diff --git a/src/webui/src/pages/view.astro b/src/webui/src/pages/view.astro index 9462514..fb48a3c 100644 --- a/src/webui/src/pages/view.astro +++ b/src/webui/src/pages/view.astro @@ -6,8 +6,8 @@ import { SITE_TITLE, SITE_DESCRIPTION } from '@/consts'; --- - +
- +
-- GitLab